From 48254b0b704c54f95cf75f4887b0404abda1a1a2 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 11 Mar 2025 10:02:31 +0100 Subject: [PATCH 01/77] Experiment with `WidgetLayout` --- crates/egui/src/lib.rs | 1 + crates/egui/src/widget_layout.rs | 99 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 crates/egui/src/widget_layout.rs diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 0d845980846..3aff7bc0665 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -444,6 +444,7 @@ pub mod widgets; #[cfg(feature = "callstack")] #[cfg(debug_assertions)] mod callstack; +mod widget_layout; #[cfg(feature = "accesskit")] pub use accesskit; diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs new file mode 100644 index 00000000000..59a76fa1ab4 --- /dev/null +++ b/crates/egui/src/widget_layout.rs @@ -0,0 +1,99 @@ +use crate::{Frame, ImageSource, Response, Sense, Ui, WidgetText}; +use emath::Vec2; +use epaint::Galley; + +enum WidgetLayoutItem<'a> { + Text(WidgetText), + Image(ImageSource<'a>), + Custom(Vec2), + Grow, +} + +enum SizedWidgetLayoutItem<'a> { + Text(Galley), + Image(ImageSource<'a>, Vec2), + Custom(Vec2), + Grow, +} + +struct WidgetLayout<'a> { + items: Vec>, + gap: f32, + frame: Frame, + sense: Sense, +} + +impl<'a> WidgetLayout<'a> { + pub fn new() -> Self { + Self { + items: Vec::new(), + gap: 0.0, + frame: Frame::default(), + sense: Sense::hover(), + } + } + + pub fn add(mut self, item: impl Into>) -> Self { + self.items.push(item.into()); + self + } + + pub fn gap(mut self, gap: f32) -> Self { + self.gap = gap; + self + } + + pub fn frame(mut self, frame: Frame) -> Self { + self.frame = frame; + self + } + + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = sense; + self + } + + pub fn show(self, ui: &mut Ui) -> Response { + let available_width = ui.available_width(); + + let mut desired_width = 0.0; + let mut preferred_width = 0.0; + + let mut height = 0.0; + + let mut sized_items = Vec::new(); + + let (rect, response) = ui.allocate_at_least(Vec2::new(desired_width, height), self.sense); + + response + } +} + +struct WLButton<'a> { + wl: WidgetLayout<'a>, +} + +impl<'a> WLButton<'a> { + pub fn new(text: impl Into) -> Self { + Self { + wl: WidgetLayout::new().add(text), + } + } + + pub fn ui(mut self, ui: &mut Ui) -> Response { + let response = ui.ctx().read_response(ui.next_auto_id()); + + let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { + ui.style().interact(&response) + }); + + self.wl.frame = self + .wl + .frame + .fill(visuals.bg_fill) + .stroke(visuals.bg_stroke) + .corner_radius(visuals.corner_radius); + + self.wl.show(ui) + } +} From fd78ea9cc4cdaf5276849ad719793a53a47fa61c Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 12 Mar 2025 11:50:18 +0100 Subject: [PATCH 02/77] Finish WidgetLayout prototype --- Cargo.lock | 2 + crates/egui/src/lib.rs | 1 + crates/egui/src/widget_layout.rs | 169 +++++++++++++++++++++--- examples/hello_world_simple/Cargo.toml | 2 + examples/hello_world_simple/src/main.rs | 33 +++++ 5 files changed, 190 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4411be0c6ac..5d0497e912d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,7 +2065,9 @@ name = "hello_world_simple" version = "0.1.0" dependencies = [ "eframe", + "egui_extras", "env_logger", + "image", ] [[package]] diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 3aff7bc0665..512caac990a 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -508,6 +508,7 @@ pub use self::{ ui_builder::UiBuilder, ui_stack::*, viewport::*, + widget_layout::*, widget_rect::{WidgetRect, WidgetRects}, widget_text::{RichText, WidgetText}, widgets::*, diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 59a76fa1ab4..580691ad744 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -1,23 +1,47 @@ -use crate::{Frame, ImageSource, Response, Sense, Ui, WidgetText}; -use emath::Vec2; +use crate::{Frame, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText}; +use emath::{Align2, Vec2}; use epaint::Galley; +use std::sync::Arc; -enum WidgetLayoutItem<'a> { +enum WidgetLayoutItemType<'a> { Text(WidgetText), - Image(ImageSource<'a>), + Image(Image<'a>), Custom(Vec2), Grow, } -enum SizedWidgetLayoutItem<'a> { - Text(Galley), - Image(ImageSource<'a>, Vec2), +enum SizedWidgetLayoutItemType<'a> { + Text(Arc), + Image(Image<'a>, Vec2), Custom(Vec2), Grow, } +struct Item { + align2: Align2, +} + +impl Default for Item { + fn default() -> Self { + Self { + align2: Align2::LEFT_CENTER, + } + } +} + +impl SizedWidgetLayoutItemType<'_> { + pub fn size(&self) -> Vec2 { + match self { + SizedWidgetLayoutItemType::Text(galley) => galley.size(), + SizedWidgetLayoutItemType::Image(_, size) => *size, + SizedWidgetLayoutItemType::Custom(size) => *size, + SizedWidgetLayoutItemType::Grow => Vec2::ZERO, + } + } +} + struct WidgetLayout<'a> { - items: Vec>, + items: Vec<(Item, WidgetLayoutItemType<'a>)>, gap: f32, frame: Frame, sense: Sense, @@ -27,14 +51,14 @@ impl<'a> WidgetLayout<'a> { pub fn new() -> Self { Self { items: Vec::new(), - gap: 0.0, + gap: 4.0, frame: Frame::default(), sense: Sense::hover(), } } - pub fn add(mut self, item: impl Into>) -> Self { - self.items.push(item.into()); + pub fn add(mut self, item: Item, kind: impl Into>) -> Self { + self.items.push((item, kind.into())); self } @@ -54,33 +78,143 @@ impl<'a> WidgetLayout<'a> { } pub fn show(self, ui: &mut Ui) -> Response { - let available_width = ui.available_width(); + let available_size = ui.available_size(); + let available_width = available_size.x; let mut desired_width = 0.0; let mut preferred_width = 0.0; - let mut height = 0.0; + let mut height: f32 = 0.0; let mut sized_items = Vec::new(); - let (rect, response) = ui.allocate_at_least(Vec2::new(desired_width, height), self.sense); + let mut grow_count = 0; + + for (item, kind) in self.items { + let (preferred_size, sized) = match kind { + WidgetLayoutItemType::Text(text) => { + let galley = text.into_galley(ui, None, available_width, TextStyle::Button); + ( + galley.size(), // TODO + SizedWidgetLayoutItemType::Text(galley), + ) + } + WidgetLayoutItemType::Image(image) => { + let size = + image.load_and_calc_size(ui, Vec2::min(available_size, Vec2::splat(16.0))); + let size = size.unwrap_or_default(); + (size, SizedWidgetLayoutItemType::Image(image, size)) + } + WidgetLayoutItemType::Custom(size) => { + (size, SizedWidgetLayoutItemType::Custom(size)) + } + WidgetLayoutItemType::Grow => { + grow_count += 1; + (Vec2::ZERO, SizedWidgetLayoutItemType::Grow) + } + }; + let size = sized.size(); + + desired_width += size.x; + preferred_width += preferred_size.x; + + height = height.max(size.y); + + sized_items.push((item, sized)); + } + + if sized_items.len() > 1 { + let gap_space = self.gap * (sized_items.len() as f32 - 1.0); + desired_width += gap_space; + preferred_width += gap_space; + } + + let margin = self.frame.total_margin(); + let content_size = Vec2::new(desired_width, height); + let frame_size = content_size + margin.sum(); + + let (rect, response) = ui.allocate_at_least(frame_size, self.sense); + + let content_rect = rect - margin; + ui.painter().add(self.frame.paint(content_rect)); + + let width_to_fill = content_rect.width(); + let extra_space = f32::max(width_to_fill - desired_width, 0.0); + let grow_width = f32::max(extra_space / grow_count as f32, 0.0); + + let mut cursor = content_rect.left(); + + for (item, sized) in sized_items { + let size = sized.size(); + let width = match sized { + SizedWidgetLayoutItemType::Grow => grow_width, + _ => size.x, + }; + + let frame = content_rect.with_min_x(cursor).with_max_x(cursor + width); + cursor = frame.right() + self.gap; + + let rect = item.align2.align_size_within_rect(size, frame); + + match sized { + SizedWidgetLayoutItemType::Text(galley) => { + ui.painter() + .galley(rect.min, galley, ui.visuals().text_color()); + } + SizedWidgetLayoutItemType::Image(image, _) => { + image.paint_at(ui, rect); + } + SizedWidgetLayoutItemType::Custom(_) => {} + SizedWidgetLayoutItemType::Grow => {} + } + } response } } -struct WLButton<'a> { +pub struct WLButton<'a> { wl: WidgetLayout<'a>, } impl<'a> WLButton<'a> { pub fn new(text: impl Into) -> Self { Self { - wl: WidgetLayout::new().add(text), + wl: WidgetLayout::new() + .sense(Sense::click()) + .add(Item::default(), WidgetLayoutItemType::Text(text.into())), + } + } + + pub fn image(image: impl Into>) -> Self { + Self { + wl: WidgetLayout::new().sense(Sense::click()).add( + Item::default(), + WidgetLayoutItemType::Image(image.into().max_size(Vec2::splat(16.0))), + ), } } - pub fn ui(mut self, ui: &mut Ui) -> Response { + pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { + Self { + wl: WidgetLayout::new() + .sense(Sense::click()) + .add(Item::default(), WidgetLayoutItemType::Image(image.into())) + .add(Item::default(), WidgetLayoutItemType::Text(text.into())), + } + } + + pub fn right_text(mut self, text: impl Into) -> Self { + self.wl = self + .wl + .add(Item::default(), WidgetLayoutItemType::Grow) + .add(Item::default(), WidgetLayoutItemType::Text(text.into())); + self + } +} + +impl<'a> Widget for WLButton<'a> { + fn ui(mut self, ui: &mut Ui) -> Response { let response = ui.ctx().read_response(ui.next_auto_id()); let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { @@ -90,6 +224,7 @@ impl<'a> WLButton<'a> { self.wl.frame = self .wl .frame + .inner_margin(ui.style().spacing.button_padding) .fill(visuals.bg_fill) .stroke(visuals.bg_stroke) .corner_radius(visuals.corner_radius); diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index db1d7906de5..e8d08456bbc 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -20,3 +20,5 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } +egui_extras = {workspace = true, features = ["image", "all_loaders"]} +image = {workspace = true, features = ["png"]} diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 4fe49a89d68..72174f05114 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -2,6 +2,10 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; +use eframe::egui::{ + include_image, Image, Key, KeyboardShortcut, ModifierNames, Modifiers, Popup, RichText, + WLButton, Widget, +}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -17,6 +21,7 @@ fn main() -> eframe::Result { eframe::run_simple_native("My egui App", options, move |ctx, _frame| { egui::CentralPanel::default().show(ctx, |ui| { + egui_extras::install_image_loaders(ctx); ui.heading("My egui Application"); ui.horizontal(|ui| { let name_label = ui.label("Your name: "); @@ -28,6 +33,34 @@ fn main() -> eframe::Result { age += 1; } ui.label(format!("Hello '{name}', age {age}")); + + if WLButton::new("WL Button").ui(ui).clicked() { + age += 1; + }; + + let source = include_image!("../../../crates/eframe/data/icon.png"); + let response = WLButton::image_and_text(source, "Hello World").ui(ui); + + Popup::menu(&response).show(|ui| { + WLButton::new("Print") + .right_text( + RichText::new( + KeyboardShortcut::new(Modifiers::COMMAND, Key::P) + .format(&ModifierNames::SYMBOLS, true), + ) + .weak(), + ) + .ui(ui); + WLButton::new("A very long button") + .right_text( + RichText::new( + KeyboardShortcut::new(Modifiers::COMMAND, Key::O) + .format(&ModifierNames::SYMBOLS, true), + ) + .weak(), + ) + .ui(ui); + }); }); }) } From 69dc8e043da28c7df6dc694711fc7d64030b7c9e Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 20 Mar 2025 13:05:45 +0100 Subject: [PATCH 03/77] Add todos --- crates/egui/src/widget_layout.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 580691ad744..a98fe223db8 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -3,6 +3,7 @@ use emath::{Align2, Vec2}; use epaint::Galley; use std::sync::Arc; +/// Naming: AtimicItem enum WidgetLayoutItemType<'a> { Text(WidgetText), Image(Image<'a>), @@ -40,7 +41,9 @@ impl SizedWidgetLayoutItemType<'_> { } } +/// AtomicLayout struct WidgetLayout<'a> { + /// TODO: SmallVec? items: Vec<(Item, WidgetLayoutItemType<'a>)>, gap: f32, frame: Frame, From ea2bff4f2c56fb340e8fe8081404a3fb170415cd Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 31 Mar 2025 18:32:54 +0200 Subject: [PATCH 04/77] Add Atomics, IntoAtomics, Atomic, AtomicKind, implement Button with new Atomics --- crates/egui/src/containers/menu.rs | 13 +- crates/egui/src/style.rs | 2 +- crates/egui/src/widget_layout.rs | 325 ++++++++++++----- crates/egui/src/widgets/button.rs | 461 ++++++++++++------------ examples/hello_world_simple/src/main.rs | 14 +- 5 files changed, 481 insertions(+), 334 deletions(-) diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 7511d07d133..36c25b667c4 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -1,7 +1,8 @@ use crate::style::StyleModifier; use crate::{ - Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior, - Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, WidgetText, + Button, Color32, Context, Frame, Id, InnerResponse, IntoAtomics, Layout, Popup, + PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, + WidgetText, }; use emath::{vec2, Align, RectAlign, Vec2}; use epaint::Stroke; @@ -242,8 +243,8 @@ pub struct MenuButton<'a> { } impl<'a> MenuButton<'a> { - pub fn new(text: impl Into) -> Self { - Self::from_button(Button::new(text)) + pub fn new(text: impl IntoAtomics<'a>) -> Self { + Self::from_button(Button::new(text.into_atomics())) } /// Set the config for the menu. @@ -292,8 +293,8 @@ impl<'a> SubMenuButton<'a> { /// The default right arrow symbol: `"⏵"` pub const RIGHT_ARROW: &'static str = "⏵"; - pub fn new(text: impl Into) -> Self { - Self::from_button(Button::new(text).right_text("⏵")) + pub fn new(text: impl IntoAtomics<'a>) -> Self { + Self::from_button(Button::new(text.into_atomics()).right_text("⏵")) } /// Create a new submenu button from a [`Button`]. diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 38d403749f5..194de500569 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1514,7 +1514,7 @@ impl Widgets { inactive: WidgetVisuals { weak_bg_fill: Color32::from_gray(230), // button background bg_fill: Color32::from_gray(230), // checkbox background - bg_stroke: Default::default(), + bg_stroke: Stroke::new(1.0, Color32::default()), fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text corner_radius: CornerRadius::same(2), expansion: 0.0, diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index a98fe223db8..d0b5b6980cd 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -3,65 +3,44 @@ use emath::{Align2, Vec2}; use epaint::Galley; use std::sync::Arc; -/// Naming: AtimicItem -enum WidgetLayoutItemType<'a> { - Text(WidgetText), - Image(Image<'a>), - Custom(Vec2), - Grow, -} - -enum SizedWidgetLayoutItemType<'a> { +pub enum SizedAtomicKind<'a> { Text(Arc), Image(Image<'a>, Vec2), Custom(Vec2), Grow, } -struct Item { - align2: Align2, -} - -impl Default for Item { - fn default() -> Self { - Self { - align2: Align2::LEFT_CENTER, - } - } -} - -impl SizedWidgetLayoutItemType<'_> { +impl SizedAtomicKind<'_> { pub fn size(&self) -> Vec2 { match self { - SizedWidgetLayoutItemType::Text(galley) => galley.size(), - SizedWidgetLayoutItemType::Image(_, size) => *size, - SizedWidgetLayoutItemType::Custom(size) => *size, - SizedWidgetLayoutItemType::Grow => Vec2::ZERO, + SizedAtomicKind::Text(galley) => galley.size(), + SizedAtomicKind::Image(_, size) => *size, + SizedAtomicKind::Custom(size) => *size, + SizedAtomicKind::Grow => Vec2::ZERO, } } } /// AtomicLayout -struct WidgetLayout<'a> { - /// TODO: SmallVec? - items: Vec<(Item, WidgetLayoutItemType<'a>)>, +pub struct WidgetLayout<'a> { + pub atomics: Atomics<'a>, gap: f32, - frame: Frame, - sense: Sense, + pub(crate) frame: Frame, + pub(crate) sense: Sense, } impl<'a> WidgetLayout<'a> { - pub fn new() -> Self { + pub fn new(atomics: impl IntoAtomics<'a>) -> Self { Self { - items: Vec::new(), + atomics: atomics.into_atomics(), gap: 4.0, frame: Frame::default(), sense: Sense::hover(), } } - pub fn add(mut self, item: Item, kind: impl Into>) -> Self { - self.items.push((item, kind.into())); + pub fn add(mut self, atomic: impl Into>) -> Self { + self.atomics.add(atomic.into()); self } @@ -93,27 +72,25 @@ impl<'a> WidgetLayout<'a> { let mut grow_count = 0; - for (item, kind) in self.items { - let (preferred_size, sized) = match kind { - WidgetLayoutItemType::Text(text) => { + for (item) in self.atomics.0 { + let (preferred_size, sized) = match item.kind { + AtomicKind::Text(text) => { let galley = text.into_galley(ui, None, available_width, TextStyle::Button); ( galley.size(), // TODO - SizedWidgetLayoutItemType::Text(galley), + SizedAtomicKind::Text(galley), ) } - WidgetLayoutItemType::Image(image) => { + AtomicKind::Image(image) => { let size = image.load_and_calc_size(ui, Vec2::min(available_size, Vec2::splat(16.0))); let size = size.unwrap_or_default(); - (size, SizedWidgetLayoutItemType::Image(image, size)) - } - WidgetLayoutItemType::Custom(size) => { - (size, SizedWidgetLayoutItemType::Custom(size)) + (size, SizedAtomicKind::Image(image, size)) } - WidgetLayoutItemType::Grow => { + AtomicKind::Custom(size) => (size, SizedAtomicKind::Custom(size)), + AtomicKind::Grow => { grow_count += 1; - (Vec2::ZERO, SizedWidgetLayoutItemType::Grow) + (Vec2::ZERO, SizedAtomicKind::Grow) } }; let size = sized.size(); @@ -123,7 +100,7 @@ impl<'a> WidgetLayout<'a> { height = height.max(size.y); - sized_items.push((item, sized)); + sized_items.push(sized); } if sized_items.len() > 1 { @@ -147,28 +124,29 @@ impl<'a> WidgetLayout<'a> { let mut cursor = content_rect.left(); - for (item, sized) in sized_items { + for sized in sized_items { let size = sized.size(); let width = match sized { - SizedWidgetLayoutItemType::Grow => grow_width, + SizedAtomicKind::Grow => grow_width, _ => size.x, }; let frame = content_rect.with_min_x(cursor).with_max_x(cursor + width); cursor = frame.right() + self.gap; - let rect = item.align2.align_size_within_rect(size, frame); + let align = Align2::CENTER_CENTER; + let rect = align.align_size_within_rect(size, frame); match sized { - SizedWidgetLayoutItemType::Text(galley) => { + SizedAtomicKind::Text(galley) => { ui.painter() .galley(rect.min, galley, ui.visuals().text_color()); } - SizedWidgetLayoutItemType::Image(image, _) => { + SizedAtomicKind::Image(image, _) => { image.paint_at(ui, rect); } - SizedWidgetLayoutItemType::Custom(_) => {} - SizedWidgetLayoutItemType::Grow => {} + SizedAtomicKind::Custom(_) => {} + SizedAtomicKind::Grow => {} } } @@ -176,62 +154,217 @@ impl<'a> WidgetLayout<'a> { } } -pub struct WLButton<'a> { - wl: WidgetLayout<'a>, +// pub struct WLButton<'a> { +// wl: WidgetLayout<'a>, +// } +// +// impl<'a> WLButton<'a> { +// pub fn new(text: impl Into) -> Self { +// Self { +// wl: WidgetLayout::new() +// .sense(Sense::click()) +// .add(Item::default(), WidgetLayoutItemType::Text(text.into())), +// } +// } +// +// pub fn image(image: impl Into>) -> Self { +// Self { +// wl: WidgetLayout::new().sense(Sense::click()).add( +// Item::default(), +// WidgetLayoutItemType::Image(image.into().max_size(Vec2::splat(16.0))), +// ), +// } +// } +// +// pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { +// Self { +// wl: WidgetLayout::new() +// .sense(Sense::click()) +// .add(Item::default(), WidgetLayoutItemType::Image(image.into())) +// .add(Item::default(), WidgetLayoutItemType::Text(text.into())), +// } +// } +// +// pub fn right_text(mut self, text: impl Into) -> Self { +// self.wl = self +// .wl +// .add(Item::default(), WidgetLayoutItemType::Grow) +// .add(Item::default(), WidgetLayoutItemType::Text(text.into())); +// self +// } +// } +// +// impl<'a> Widget for WLButton<'a> { +// fn ui(mut self, ui: &mut Ui) -> Response { +// let response = ui.ctx().read_response(ui.next_auto_id()); +// +// let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { +// ui.style().interact(&response) +// }); +// +// self.wl.frame = self +// .wl +// .frame +// .inner_margin(ui.style().spacing.button_padding) +// .fill(visuals.bg_fill) +// .stroke(visuals.bg_stroke) +// .corner_radius(visuals.corner_radius); +// +// self.wl.show(ui) +// } +// } + +pub enum AtomicKind<'a> { + Text(WidgetText), + Image(Image<'a>), + Custom(Vec2), + Grow, } -impl<'a> WLButton<'a> { - pub fn new(text: impl Into) -> Self { - Self { - wl: WidgetLayout::new() - .sense(Sense::click()) - .add(Item::default(), WidgetLayoutItemType::Text(text.into())), - } +pub struct Atomic<'a> { + size: Option, + grow: bool, + pub kind: AtomicKind<'a>, +} + +pub fn a<'a>(i: impl Into>) -> Atomic<'a> { + Atomic { + size: None, + grow: false, + kind: i.into(), } +} - pub fn image(image: impl Into>) -> Self { - Self { - wl: WidgetLayout::new().sense(Sense::click()).add( - Item::default(), - WidgetLayoutItemType::Image(image.into().max_size(Vec2::splat(16.0))), - ), - } +impl Atomic<'_> { + // pub fn size(mut self, size: Vec2) -> Self { + // self.size = Some(size); + // self + // } + // + // pub fn grow(mut self, grow: bool) -> Self { + // self.grow = grow; + // self + // } +} + +trait AtomicExt<'a> { + fn a_size(self, size: Vec2) -> Atomic<'a>; + fn a_grow(self, grow: bool) -> Atomic<'a>; +} + +impl<'a, T> AtomicExt<'a> for T +where + T: Into> + Sized, +{ + fn a_size(self, size: Vec2) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.size = Some(size); + atomic } - pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { - Self { - wl: WidgetLayout::new() - .sense(Sense::click()) - .add(Item::default(), WidgetLayoutItemType::Image(image.into())) - .add(Item::default(), WidgetLayoutItemType::Text(text.into())), + fn a_grow(self, grow: bool) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.grow = grow; + atomic + } +} + +impl<'a, T> From for Atomic<'a> +where + T: Into>, +{ + fn from(value: T) -> Self { + Atomic { + size: None, + grow: false, + kind: value.into(), } } +} - pub fn right_text(mut self, text: impl Into) -> Self { - self.wl = self - .wl - .add(Item::default(), WidgetLayoutItemType::Grow) - .add(Item::default(), WidgetLayoutItemType::Text(text.into())); - self +impl<'a> From> for AtomicKind<'a> { + fn from(value: Image<'a>) -> Self { + AtomicKind::Image(value) } } -impl<'a> Widget for WLButton<'a> { - fn ui(mut self, ui: &mut Ui) -> Response { - let response = ui.ctx().read_response(ui.next_auto_id()); +// impl<'a> From<&str> for AtomicKind<'a> { +// fn from(value: &str) -> Self { +// AtomicKind::Text(value.into()) +// } +// } + +impl<'a, T> From for AtomicKind<'a> +where + T: Into, +{ + fn from(value: T) -> Self { + AtomicKind::Text(value.into()) + } +} - let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { - ui.style().interact(&response) - }); +pub struct Atomics<'a>(Vec>); - self.wl.frame = self - .wl - .frame - .inner_margin(ui.style().spacing.button_padding) - .fill(visuals.bg_fill) - .stroke(visuals.bg_stroke) - .corner_radius(visuals.corner_radius); +impl<'a> Atomics<'a> { + pub fn add(&mut self, atomic: impl Into>) { + self.0.push(atomic.into()); + } - self.wl.show(ui) + pub fn add_front(&mut self, atomic: impl Into>) { + self.0.insert(0, atomic.into()); + } + + pub fn iter_mut(&mut self) -> impl Iterator> { + self.0.iter_mut() } } + +impl<'a, T> IntoAtomics<'a> for T +where + T: Into>, +{ + fn collect(self, atomics: &mut Atomics<'a>) { + atomics.add(self); + } +} + +pub trait IntoAtomics<'a> { + fn collect(self, atomics: &mut Atomics<'a>); + + fn into_atomics(self) -> Atomics<'a> + where + Self: Sized, + { + let mut atomics = Atomics(Vec::new()); + self.collect(&mut atomics); + atomics + } +} + +impl<'a> IntoAtomics<'a> for Atomics<'a> { + fn collect(self, atomics: &mut Atomics<'a>) { + atomics.0.extend(self.0); + } +} + +macro_rules! all_the_atomics { + ($($T:ident),*) => { + impl<'a, $($T),*> IntoAtomics<'a> for ($($T),*) + where + $($T: IntoAtomics<'a>),* + { + fn collect(self, atomics: &mut Atomics<'a>) { + #[allow(non_snake_case)] + let ($($T),*) = self; + $($T.collect(atomics);)* + } + } + }; +} + +all_the_atomics!(); +all_the_atomics!(T0, T1); +all_the_atomics!(T0, T1, T2); +all_the_atomics!(T0, T1, T2, T3); +all_the_atomics!(T0, T1, T2, T3, T4); +all_the_atomics!(T0, T1, T2, T3, T4, T5); diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index c0701c1939a..50feabfac60 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,6 +1,7 @@ use crate::{ - widgets, Align, Color32, CornerRadius, FontSelection, Image, NumExt, Rect, Response, Sense, - Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + widgets, Align, Atomic, AtomicKind, Color32, CornerRadius, Frame, Image, IntoAtomics, NumExt, + Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, + WidgetLayout, WidgetText, WidgetType, }; /// Clickable button with text. @@ -23,26 +24,35 @@ use crate::{ /// ``` #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Button<'a> { - image: Option>, - text: Option, - right_text: WidgetText, wrap_mode: Option, /// None means default for interact fill: Option, stroke: Option, - sense: Sense, small: bool, frame: Option, min_size: Vec2, corner_radius: Option, selected: bool, image_tint_follows_text_color: bool, + + wl: WidgetLayout<'a>, } impl<'a> Button<'a> { - pub fn new(text: impl Into) -> Self { - Self::opt_image_and_text(None, Some(text.into())) + pub fn new(text: impl IntoAtomics<'a>) -> Self { + Self { + wrap_mode: None, + fill: None, + stroke: None, + small: false, + frame: None, + min_size: Vec2::ZERO, + corner_radius: None, + selected: false, + image_tint_follows_text_color: false, + wl: WidgetLayout::new(text.into_atomics()).sense(Sense::click()), + } } /// Creates a button with an image. The size of the image as displayed is defined by the provided size. @@ -58,21 +68,14 @@ impl<'a> Button<'a> { } pub fn opt_image_and_text(image: Option>, text: Option) -> Self { - Self { - text, - image, - right_text: Default::default(), - wrap_mode: None, - fill: None, - stroke: None, - sense: Sense::click(), - small: false, - frame: None, - min_size: Vec2::ZERO, - corner_radius: None, - selected: false, - image_tint_follows_text_color: false, + let mut button = Self::new(()); + if let Some(image) = image { + button.wl.atomics.add(image); + } + if let Some(text) = text { + button.wl.atomics.add(text); } + button } /// Set the wrap mode for the text. @@ -106,7 +109,6 @@ impl<'a> Button<'a> { #[inline] pub fn fill(mut self, fill: impl Into) -> Self { self.fill = Some(fill.into()); - self.frame = Some(true); self } @@ -122,9 +124,6 @@ impl<'a> Button<'a> { /// Make this a small button, suitable for embedding into text. #[inline] pub fn small(mut self) -> Self { - if let Some(text) = self.text { - self.text = Some(text.text_style(TextStyle::Body)); - } self.small = true; self } @@ -140,7 +139,7 @@ impl<'a> Button<'a> { /// Change this to a drag-button with `Sense::drag()`. #[inline] pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; + self.wl.sense = sense; self } @@ -184,15 +183,15 @@ impl<'a> Button<'a> { /// /// See also [`Self::right_text`]. #[inline] - pub fn shortcut_text(mut self, shortcut_text: impl Into) -> Self { - self.right_text = shortcut_text.into().weak(); + pub fn shortcut_text(mut self, shortcut_text: impl Into>) -> Self { + self.wl = self.wl.add(shortcut_text); self } /// Show some text on the right side of the button. #[inline] - pub fn right_text(mut self, right_text: impl Into) -> Self { - self.right_text = right_text.into(); + pub fn right_text(mut self, right_text: impl Into>) -> Self { + self.wl = self.wl.add(right_text.into()); self } @@ -205,225 +204,237 @@ impl<'a> Button<'a> { } impl Widget for Button<'_> { - fn ui(self, ui: &mut Ui) -> Response { + fn ui(mut self, ui: &mut Ui) -> Response { let Button { - text, - image, - right_text, wrap_mode, fill, stroke, - sense, small, frame, min_size, corner_radius, selected, image_tint_follows_text_color, + mut wl, } = self; - let frame = frame.unwrap_or_else(|| ui.visuals().button_frame); - - let default_font_height = || { - let font_selection = FontSelection::default(); - let font_id = font_selection.resolve(ui.style()); - ui.fonts(|f| f.row_height(&font_id)) - }; - - let text_font_height = ui - .fonts(|fonts| text.as_ref().map(|wt| wt.font_height(fonts, ui.style()))) - .unwrap_or_else(default_font_height); + let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); - let mut button_padding = if frame { + let mut button_padding = if has_frame { ui.spacing().button_padding } else { Vec2::ZERO }; if small { button_padding.y = 0.0; + wl.atomics.iter_mut().for_each(|a| match &mut a.kind { + AtomicKind::Text(text) => { + *text = std::mem::take(text).small(); + } + _ => {} + }) } - let (space_available_for_image, right_text_font_height) = if let Some(text) = &text { - let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style())); - ( - Vec2::splat(font_height), // Reasonable? - font_height, - ) - } else { - ( - ui.available_size() - 2.0 * button_padding, - default_font_height(), - ) - }; - - let image_size = if let Some(image) = &image { - image - .load_and_calc_size(ui, space_available_for_image) - .unwrap_or(space_available_for_image) - } else { - Vec2::ZERO - }; - - let gap_before_right_text = ui.spacing().item_spacing.x; - - let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x; - if image.is_some() { - text_wrap_width -= image_size.x + ui.spacing().icon_spacing; - } - - // Note: we don't wrap the right text - let right_galley = (!right_text.is_empty()).then(|| { - right_text.into_galley( - ui, - Some(TextWrapMode::Extend), - f32::INFINITY, - TextStyle::Button, - ) - }); - - if let Some(right_galley) = &right_galley { - // Leave space for the right text: - text_wrap_width -= gap_before_right_text + right_galley.size().x; - } - - let galley = - text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button)); + let response = ui.ctx().read_response(ui.next_auto_id()); - let mut desired_size = Vec2::ZERO; - if image.is_some() { - desired_size.x += image_size.x; - desired_size.y = desired_size.y.max(image_size.y); - } - if image.is_some() && galley.is_some() { - desired_size.x += ui.spacing().icon_spacing; - } - if let Some(galley) = &galley { - desired_size.x += galley.size().x; - desired_size.y = desired_size.y.max(galley.size().y).max(text_font_height); - } - if let Some(right_galley) = &right_galley { - desired_size.x += gap_before_right_text + right_galley.size().x; - desired_size.y = desired_size - .y - .max(right_galley.size().y) - .max(right_text_font_height); - } - desired_size += 2.0 * button_padding; - if !small { - desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); - } - desired_size = desired_size.at_least(min_size); - - let (rect, mut response) = ui.allocate_at_least(desired_size, sense); - response.widget_info(|| { - if let Some(galley) = &galley { - WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) - } else { - WidgetInfo::new(WidgetType::Button) - } + let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { + ui.style().interact(&response) }); - if ui.is_rect_visible(rect) { - let visuals = ui.style().interact(&response); - - let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected { - let selection = ui.visuals().selection; - ( - Vec2::ZERO, - CornerRadius::ZERO, - selection.bg_fill, - selection.stroke, - ) - } else if frame { - let expansion = Vec2::splat(visuals.expansion); - ( - expansion, - visuals.corner_radius, - visuals.weak_bg_fill, - visuals.bg_stroke, - ) - } else { - Default::default() - }; - let frame_cr = corner_radius.unwrap_or(frame_cr); - let frame_fill = fill.unwrap_or(frame_fill); - let frame_stroke = stroke.unwrap_or(frame_stroke); - ui.painter().rect( - rect.expand2(frame_expansion), - frame_cr, - frame_fill, - frame_stroke, - epaint::StrokeKind::Inside, - ); - - let mut cursor_x = rect.min.x + button_padding.x; - - if let Some(image) = &image { - let mut image_pos = ui - .layout() - .align_size_within_rect(image_size, rect.shrink2(button_padding)) - .min; - if galley.is_some() || right_galley.is_some() { - image_pos.x = cursor_x; - } - let image_rect = Rect::from_min_size(image_pos, image_size); - cursor_x += image_size.x; - let tlr = image.load_for_size(ui.ctx(), image_size); - let mut image_options = image.image_options().clone(); - if image_tint_follows_text_color { - image_options.tint = image_options.tint * visuals.text_color(); - } - widgets::image::paint_texture_load_result( - ui, - &tlr, - image_rect, - image.show_loading_spinner, - &image_options, - None, - ); - response = widgets::image::texture_load_result_response( - &image.source(ui.ctx()), - &tlr, - response, - ); - } - - if image.is_some() && galley.is_some() { - cursor_x += ui.spacing().icon_spacing; - } - - if let Some(galley) = galley { - let mut text_pos = ui - .layout() - .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) - .min; - if image.is_some() || right_galley.is_some() { - text_pos.x = cursor_x; - } - ui.painter().galley(text_pos, galley, visuals.text_color()); - } - - if let Some(right_galley) = right_galley { - // Always align to the right - let layout = if ui.layout().is_horizontal() { - ui.layout().with_main_align(Align::Max) - } else { - ui.layout().with_cross_align(Align::Max) - }; - let right_text_pos = layout - .align_size_within_rect(right_galley.size(), rect.shrink2(button_padding)) - .min; - - ui.painter() - .galley(right_text_pos, right_galley, visuals.text_color()); - } - } + wl.frame = if has_frame { + wl.frame + .inner_margin(button_padding) + .fill(fill.unwrap_or(visuals.bg_fill)) + .stroke(stroke.unwrap_or(visuals.bg_stroke)) + .corner_radius(corner_radius.unwrap_or(visuals.corner_radius)) + } else { + Frame::new() + }; - if let Some(cursor) = ui.visuals().interact_cursor { - if response.hovered() { - ui.ctx().set_cursor_icon(cursor); - } - } + let response = wl.show(ui); + + // TODO: How to get text? + // response.widget_info(|| { + // if let Some(galley) = &galley { + // WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) + // } else { + // WidgetInfo::new(WidgetType::Button) + // } + // }); + + // + // let space_available_for_image = if let Some(text) = &text { + // let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style())); + // Vec2::splat(font_height) // Reasonable? + // } else { + // ui.available_size() - 2.0 * button_padding + // }; + // + // let image_size = if let Some(image) = &image { + // image + // .load_and_calc_size(ui, space_available_for_image) + // .unwrap_or(space_available_for_image) + // } else { + // Vec2::ZERO + // }; + // + // let gap_before_right_text = ui.spacing().item_spacing.x; + // + // let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x; + // if image.is_some() { + // text_wrap_width -= image_size.x + ui.spacing().icon_spacing; + // } + // + // // Note: we don't wrap the right text + // let right_galley = (!right_text.is_empty()).then(|| { + // right_text.into_galley( + // ui, + // Some(TextWrapMode::Extend), + // f32::INFINITY, + // TextStyle::Button, + // ) + // }); + // + // if let Some(right_galley) = &right_galley { + // // Leave space for the right text: + // text_wrap_width -= gap_before_right_text + right_galley.size().x; + // } + // + // let galley = + // text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button)); + // + // let mut desired_size = Vec2::ZERO; + // if image.is_some() { + // desired_size.x += image_size.x; + // desired_size.y = desired_size.y.max(image_size.y); + // } + // if image.is_some() && galley.is_some() { + // desired_size.x += ui.spacing().icon_spacing; + // } + // if let Some(galley) = &galley { + // desired_size.x += galley.size().x; + // desired_size.y = desired_size.y.max(galley.size().y); + // } + // if let Some(right_galley) = &right_galley { + // desired_size.x += gap_before_right_text + right_galley.size().x; + // desired_size.y = desired_size.y.max(right_galley.size().y); + // } + // desired_size += 2.0 * button_padding; + // if !small { + // desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); + // } + // desired_size = desired_size.at_least(min_size); + // + // let (rect, mut response) = ui.allocate_at_least(desired_size, sense); + // response.widget_info(|| { + // if let Some(galley) = &galley { + // WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) + // } else { + // WidgetInfo::new(WidgetType::Button) + // } + // }); + // + // if ui.is_rect_visible(rect) { + // let visuals = ui.style().interact(&response); + // + // let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected { + // let selection = ui.visuals().selection; + // ( + // Vec2::ZERO, + // CornerRadius::ZERO, + // selection.bg_fill, + // selection.stroke, + // ) + // } else if frame { + // let expansion = Vec2::splat(visuals.expansion); + // ( + // expansion, + // visuals.corner_radius, + // visuals.weak_bg_fill, + // visuals.bg_stroke, + // ) + // } else { + // Default::default() + // }; + // let frame_cr = corner_radius.unwrap_or(frame_cr); + // let frame_fill = fill.unwrap_or(frame_fill); + // let frame_stroke = stroke.unwrap_or(frame_stroke); + // ui.painter().rect( + // rect.expand2(frame_expansion), + // frame_cr, + // frame_fill, + // frame_stroke, + // epaint::StrokeKind::Inside, + // ); + // + // let mut cursor_x = rect.min.x + button_padding.x; + // + // if let Some(image) = &image { + // let mut image_pos = ui + // .layout() + // .align_size_within_rect(image_size, rect.shrink2(button_padding)) + // .min; + // if galley.is_some() || right_galley.is_some() { + // image_pos.x = cursor_x; + // } + // let image_rect = Rect::from_min_size(image_pos, image_size); + // cursor_x += image_size.x; + // let tlr = image.load_for_size(ui.ctx(), image_size); + // let mut image_options = image.image_options().clone(); + // if image_tint_follows_text_color { + // image_options.tint = image_options.tint * visuals.text_color(); + // } + // widgets::image::paint_texture_load_result( + // ui, + // &tlr, + // image_rect, + // image.show_loading_spinner, + // &image_options, + // None, + // ); + // response = widgets::image::texture_load_result_response( + // &image.source(ui.ctx()), + // &tlr, + // response, + // ); + // } + // + // if image.is_some() && galley.is_some() { + // cursor_x += ui.spacing().icon_spacing; + // } + // + // if let Some(galley) = galley { + // let mut text_pos = ui + // .layout() + // .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) + // .min; + // if image.is_some() || right_galley.is_some() { + // text_pos.x = cursor_x; + // } + // ui.painter().galley(text_pos, galley, visuals.text_color()); + // } + // + // if let Some(right_galley) = right_galley { + // // Always align to the right + // let layout = if ui.layout().is_horizontal() { + // ui.layout().with_main_align(Align::Max) + // } else { + // ui.layout().with_cross_align(Align::Max) + // }; + // let right_text_pos = layout + // .align_size_within_rect(right_galley.size(), rect.shrink2(button_padding)) + // .min; + // + // ui.painter() + // .galley(right_text_pos, right_galley, visuals.text_color()); + // } + // } + + // if let Some(cursor) = ui.visuals().interact_cursor { + // if response.hovered() { + // ui.ctx().set_cursor_icon(cursor); + // } + // } response } diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 72174f05114..b0d62a8481c 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -3,8 +3,8 @@ use eframe::egui; use eframe::egui::{ - include_image, Image, Key, KeyboardShortcut, ModifierNames, Modifiers, Popup, RichText, - WLButton, Widget, + include_image, Button, Image, Key, KeyboardShortcut, ModifierNames, Modifiers, Popup, RichText, + Widget, }; fn main() -> eframe::Result { @@ -34,15 +34,17 @@ fn main() -> eframe::Result { } ui.label(format!("Hello '{name}', age {age}")); - if WLButton::new("WL Button").ui(ui).clicked() { + if Button::new("WL Button").ui(ui).clicked() { age += 1; }; let source = include_image!("../../../crates/eframe/data/icon.png"); - let response = WLButton::image_and_text(source, "Hello World").ui(ui); + let response = Button::image_and_text(source.clone(), "Hello World").ui(ui); + + Button::new((Image::new(source).tint(egui::Color32::RED), "Tuple Button")).ui(ui); Popup::menu(&response).show(|ui| { - WLButton::new("Print") + Button::new("Print") .right_text( RichText::new( KeyboardShortcut::new(Modifiers::COMMAND, Key::P) @@ -51,7 +53,7 @@ fn main() -> eframe::Result { .weak(), ) .ui(ui); - WLButton::new("A very long button") + Button::new("A very long button") .right_text( RichText::new( KeyboardShortcut::new(Modifiers::COMMAND, Key::O) From df79e56ffc3255e8a502cb28d8a81eacb6718b36 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 9 Apr 2025 10:28:49 +0200 Subject: [PATCH 05/77] Button improvements, implement checkbox --- crates/egui/src/widget_layout.rs | 91 +++++++++++++++++++++++------ crates/egui/src/widgets/button.rs | 58 +++++++++--------- crates/egui/src/widgets/checkbox.rs | 60 +++++++------------ 3 files changed, 128 insertions(+), 81 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index d0b5b6980cd..7012d0e1b03 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -1,12 +1,13 @@ -use crate::{Frame, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText}; -use emath::{Align2, Vec2}; -use epaint::Galley; +use crate::{Frame, Id, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText}; +use ahash::HashMap; +use emath::{Align2, Rect, Vec2}; +use epaint::{Color32, Galley}; use std::sync::Arc; pub enum SizedAtomicKind<'a> { Text(Arc), Image(Image<'a>, Vec2), - Custom(Vec2), + Custom(Id, Vec2), Grow, } @@ -15,7 +16,7 @@ impl SizedAtomicKind<'_> { match self { SizedAtomicKind::Text(galley) => galley.size(), SizedAtomicKind::Image(_, size) => *size, - SizedAtomicKind::Custom(size) => *size, + SizedAtomicKind::Custom(_, size) => *size, SizedAtomicKind::Grow => Vec2::ZERO, } } @@ -24,18 +25,20 @@ impl SizedAtomicKind<'_> { /// AtomicLayout pub struct WidgetLayout<'a> { pub atomics: Atomics<'a>, - gap: f32, + gap: Option, pub(crate) frame: Frame, pub(crate) sense: Sense, + fallback_text_color: Option, } impl<'a> WidgetLayout<'a> { pub fn new(atomics: impl IntoAtomics<'a>) -> Self { Self { atomics: atomics.into_atomics(), - gap: 4.0, + gap: None, frame: Frame::default(), sense: Sense::hover(), + fallback_text_color: None, } } @@ -44,8 +47,9 @@ impl<'a> WidgetLayout<'a> { self } + /// Default: `Spacing::icon_spacing` pub fn gap(mut self, gap: f32) -> Self { - self.gap = gap; + self.gap = Some(gap); self } @@ -59,7 +63,17 @@ impl<'a> WidgetLayout<'a> { self } - pub fn show(self, ui: &mut Ui) -> Response { + pub fn fallback_text_color(mut self, color: Color32) -> Self { + self.fallback_text_color = Some(color); + self + } + + pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { + let fallback_text_color = self + .fallback_text_color + .unwrap_or_else(|| ui.style().visuals.text_color()); + let gap = self.gap.unwrap_or(ui.spacing().icon_spacing); + let available_size = ui.available_size(); let available_width = available_size.x; @@ -87,7 +101,7 @@ impl<'a> WidgetLayout<'a> { let size = size.unwrap_or_default(); (size, SizedAtomicKind::Image(image, size)) } - AtomicKind::Custom(size) => (size, SizedAtomicKind::Custom(size)), + AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), AtomicKind::Grow => { grow_count += 1; (Vec2::ZERO, SizedAtomicKind::Grow) @@ -104,7 +118,7 @@ impl<'a> WidgetLayout<'a> { } if sized_items.len() > 1 { - let gap_space = self.gap * (sized_items.len() as f32 - 1.0); + let gap_space = gap * (sized_items.len() as f32 - 1.0); desired_width += gap_space; preferred_width += gap_space; } @@ -115,6 +129,11 @@ impl<'a> WidgetLayout<'a> { let (rect, response) = ui.allocate_at_least(frame_size, self.sense); + let mut response = AtomicLayoutResponse { + response, + custom_rects: HashMap::default(), + }; + let content_rect = rect - margin; ui.painter().add(self.frame.paint(content_rect)); @@ -132,20 +151,21 @@ impl<'a> WidgetLayout<'a> { }; let frame = content_rect.with_min_x(cursor).with_max_x(cursor + width); - cursor = frame.right() + self.gap; + cursor = frame.right() + gap; let align = Align2::CENTER_CENTER; let rect = align.align_size_within_rect(size, frame); match sized { SizedAtomicKind::Text(galley) => { - ui.painter() - .galley(rect.min, galley, ui.visuals().text_color()); + ui.painter().galley(rect.min, galley, fallback_text_color); } SizedAtomicKind::Image(image, _) => { image.paint_at(ui, rect); } - SizedAtomicKind::Custom(_) => {} + SizedAtomicKind::Custom(id, size) => { + response.custom_rects.insert(id, rect); + } SizedAtomicKind::Grow => {} } } @@ -154,6 +174,11 @@ impl<'a> WidgetLayout<'a> { } } +pub struct AtomicLayoutResponse { + pub response: Response, + pub custom_rects: HashMap, +} + // pub struct WLButton<'a> { // wl: WidgetLayout<'a>, // } @@ -217,7 +242,7 @@ impl<'a> WidgetLayout<'a> { pub enum AtomicKind<'a> { Text(WidgetText), Image(Image<'a>), - Custom(Vec2), + Custom(Id, Vec2), Grow, } @@ -247,7 +272,7 @@ impl Atomic<'_> { // } } -trait AtomicExt<'a> { +pub trait AtomicExt<'a> { fn a_size(self, size: Vec2) -> Atomic<'a>; fn a_grow(self, grow: bool) -> Atomic<'a>; } @@ -317,6 +342,21 @@ impl<'a> Atomics<'a> { pub fn iter_mut(&mut self) -> impl Iterator> { self.0.iter_mut() } + + pub fn text(&self) -> Option { + let mut string: Option = None; + for atomic in &self.0 { + if let AtomicKind::Text(text) = &atomic.kind { + if let Some(string) = &mut string { + string.push(' '); + string.push_str(text.text()); + } else { + string = Some(text.text().to_owned()); + } + } + } + string + } } impl<'a, T> IntoAtomics<'a> for T @@ -368,3 +408,20 @@ all_the_atomics!(T0, T1, T2); all_the_atomics!(T0, T1, T2, T3); all_the_atomics!(T0, T1, T2, T3, T4); all_the_atomics!(T0, T1, T2, T3, T4, T5); + +// trait AtomicWidget { +// fn show(&self, ui: &mut Ui) -> WidgetLayout; +// } + +// TODO: This conflicts with the FnOnce Widget impl, is there some way around that? +// impl Widget for T where T: AtomicWidget { +// fn ui(self, ui: &mut Ui) -> Response { +// ui.add(self) +// } +// } + +impl Widget for WidgetLayout<'_> { + fn ui(self, ui: &mut Ui) -> Response { + self.show(ui).response + } +} diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 50feabfac60..691a71bdd95 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ - widgets, Align, Atomic, AtomicKind, Color32, CornerRadius, Frame, Image, IntoAtomics, NumExt, - Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, - WidgetLayout, WidgetText, WidgetType, + widgets, Align, Atomic, AtomicExt, AtomicKind, AtomicLayoutResponse, Color32, CornerRadius, + Frame, Image, IntoAtomics, NumExt, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, + Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType, }; /// Clickable button with text. @@ -184,14 +184,20 @@ impl<'a> Button<'a> { /// See also [`Self::right_text`]. #[inline] pub fn shortcut_text(mut self, shortcut_text: impl Into>) -> Self { - self.wl = self.wl.add(shortcut_text); + self.wl = self + .wl + .add(AtomicKind::Grow.a_grow(true)) + .add(shortcut_text); self } /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl Into>) -> Self { - self.wl = self.wl.add(right_text.into()); + self.wl = self + .wl + .add(AtomicKind::Grow.a_grow(true)) + .add(right_text.into()); self } @@ -201,10 +207,8 @@ impl<'a> Button<'a> { self.selected = selected; self } -} -impl Widget for Button<'_> { - fn ui(mut self, ui: &mut Ui) -> Response { + pub fn atomic_ui(mut self, ui: &mut Ui) -> AtomicLayoutResponse { let Button { wrap_mode, fill, @@ -241,26 +245,37 @@ impl Widget for Button<'_> { ui.style().interact(&response) }); + wl = wl.fallback_text_color(visuals.text_color()); + wl.frame = if has_frame { wl.frame .inner_margin(button_padding) - .fill(fill.unwrap_or(visuals.bg_fill)) + .fill(fill.unwrap_or(visuals.weak_bg_fill)) .stroke(stroke.unwrap_or(visuals.bg_stroke)) .corner_radius(corner_radius.unwrap_or(visuals.corner_radius)) } else { Frame::new() }; + let text = wl.atomics.text(); + let response = wl.show(ui); - // TODO: How to get text? - // response.widget_info(|| { - // if let Some(galley) = &galley { - // WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) - // } else { - // WidgetInfo::new(WidgetType::Button) - // } - // }); + response.response.widget_info(|| { + if let Some(text) = &text { + WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text) + } else { + WidgetInfo::new(WidgetType::Button) + } + }); + + response + } +} + +impl Widget for Button<'_> { + fn ui(mut self, ui: &mut Ui) -> Response { + self.atomic_ui(ui).response // // let space_available_for_image = if let Some(text) = &text { @@ -326,13 +341,6 @@ impl Widget for Button<'_> { // desired_size = desired_size.at_least(min_size); // // let (rect, mut response) = ui.allocate_at_least(desired_size, sense); - // response.widget_info(|| { - // if let Some(galley) = &galley { - // WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) - // } else { - // WidgetInfo::new(WidgetType::Button) - // } - // }); // // if ui.is_rect_visible(rect) { // let visuals = ui.style().interact(&response); @@ -435,7 +443,5 @@ impl Widget for Button<'_> { // ui.ctx().set_cursor_icon(cursor); // } // } - - response } } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 7bdb6c86fe9..b6fb557640e 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,6 +1,7 @@ +use crate::AtomicKind::Custom; use crate::{ - epaint, pos2, vec2, NumExt, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, WidgetInfo, - WidgetText, WidgetType, + epaint, pos2, vec2, Atomics, Id, IntoAtomics, NumExt, Response, Sense, Shape, TextStyle, Ui, + Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType, }; // TODO(emilk): allow checkbox without a text label @@ -19,21 +20,21 @@ use crate::{ #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Checkbox<'a> { checked: &'a mut bool, - text: WidgetText, + atomics: Atomics<'a>, indeterminate: bool, } impl<'a> Checkbox<'a> { - pub fn new(checked: &'a mut bool, text: impl Into) -> Self { + pub fn new(checked: &'a mut bool, atomics: impl IntoAtomics<'a>) -> Self { Checkbox { checked, - text: text.into(), + atomics: atomics.into_atomics(), indeterminate: false, } } pub fn without_text(checked: &'a mut bool) -> Self { - Self::new(checked, WidgetText::default()) + Self::new(checked, ()) } /// Display an indeterminate state (neither checked nor unchecked) @@ -51,56 +52,46 @@ impl Widget for Checkbox<'_> { fn ui(self, ui: &mut Ui) -> Response { let Checkbox { checked, - text, + mut atomics, indeterminate, } = self; let spacing = &ui.spacing(); let icon_width = spacing.icon_width; - let icon_spacing = spacing.icon_spacing; - let (galley, mut desired_size) = if text.is_empty() { - (None, vec2(icon_width, 0.0)) - } else { - let total_extra = vec2(icon_width + icon_spacing, 0.0); + let rect_id = Id::new("checkbox"); + atomics.add_front(Custom(rect_id, Vec2::splat(icon_width))); - let wrap_width = ui.available_width() - total_extra.x; - let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); + let text = atomics.text(); - let mut desired_size = total_extra + galley.size(); - desired_size = desired_size.at_least(spacing.interact_size); + let mut response = WidgetLayout::new(atomics).sense(Sense::click()).show(ui); - (Some(galley), desired_size) - }; - - desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); - desired_size.y = desired_size.y.max(icon_width); - let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); - - if response.clicked() { + if response.response.clicked() { *checked = !*checked; - response.mark_changed(); + response.response.mark_changed(); } - response.widget_info(|| { + response.response.widget_info(|| { if indeterminate { WidgetInfo::labeled( WidgetType::Checkbox, ui.is_enabled(), - galley.as_ref().map_or("", |x| x.text()), + text.clone().unwrap_or("".to_owned()), ) } else { WidgetInfo::selected( WidgetType::Checkbox, ui.is_enabled(), *checked, - galley.as_ref().map_or("", |x| x.text()), + text.clone().unwrap_or("".to_owned()), ) } }); - if ui.is_rect_visible(rect) { + if ui.is_rect_visible(response.response.rect) { // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful - let visuals = ui.style().interact(&response); + let visuals = ui.style().interact(&response.response); + let rect = response.custom_rects.get(&rect_id).unwrap().clone(); + let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); ui.painter().add(epaint::RectShape::new( big_icon_rect.expand(visuals.expansion), @@ -128,15 +119,8 @@ impl Widget for Checkbox<'_> { visuals.fg_stroke, )); } - if let Some(galley) = galley { - let text_pos = pos2( - rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * galley.size().y, - ); - ui.painter().galley(text_pos, galley, visuals.text_color()); - } } - response + response.response } } From cfec476ca2204e310f01e3177fb02f492786b918 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 9 Apr 2025 15:40:58 +0200 Subject: [PATCH 06/77] Implement shrinking --- crates/egui/src/widget_layout.rs | 104 ++++++++++++++++++++++-------- crates/egui/src/widgets/button.rs | 18 ++++++ 2 files changed, 94 insertions(+), 28 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 7012d0e1b03..a1364b03658 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -74,8 +74,8 @@ impl<'a> WidgetLayout<'a> { .unwrap_or_else(|| ui.style().visuals.text_color()); let gap = self.gap.unwrap_or(ui.spacing().icon_spacing); - let available_size = ui.available_size(); - let available_width = available_size.x; + // The size available for the content + let available_inner_size = ui.available_size() - self.frame.total_margin().sum(); let mut desired_width = 0.0; let mut preferred_width = 0.0; @@ -86,27 +86,29 @@ impl<'a> WidgetLayout<'a> { let mut grow_count = 0; - for (item) in self.atomics.0 { - let (preferred_size, sized) = match item.kind { - AtomicKind::Text(text) => { - let galley = text.into_galley(ui, None, available_width, TextStyle::Button); - ( - galley.size(), // TODO - SizedAtomicKind::Text(galley), - ) - } - AtomicKind::Image(image) => { - let size = - image.load_and_calc_size(ui, Vec2::min(available_size, Vec2::splat(16.0))); - let size = size.unwrap_or_default(); - (size, SizedAtomicKind::Image(image, size)) - } - AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), - AtomicKind::Grow => { - grow_count += 1; - (Vec2::ZERO, SizedAtomicKind::Grow) + let mut shrink_item = None; + + if self.atomics.0.len() > 1 { + let gap_space = gap * (self.atomics.0.len() as f32 - 1.0); + desired_width += gap_space; + preferred_width += gap_space; + } + + for ((idx, item)) in self.atomics.0.into_iter().enumerate() { + if item.shrink { + debug_assert!( + shrink_item.is_none(), + "Only one atomic may be marked as shrink" + ); + if shrink_item.is_none() { + shrink_item = Some((idx, item)); + continue; } - }; + } + if item.grow { + grow_count += 1; + } + let (preferred_size, sized) = item.kind.into_sized(ui, available_inner_size); let size = sized.size(); desired_width += size.x; @@ -117,10 +119,21 @@ impl<'a> WidgetLayout<'a> { sized_items.push(sized); } - if sized_items.len() > 1 { - let gap_space = gap * (sized_items.len() as f32 - 1.0); - desired_width += gap_space; - preferred_width += gap_space; + if let Some((index, item)) = shrink_item { + // The `shrink` item gets the remaining space + let mut shrunk_size = Vec2::new( + available_inner_size.x - desired_width, + available_inner_size.y, + ); + let (preferred_size, sized) = item.kind.into_sized(ui, shrunk_size); + let size = sized.size(); + + desired_width += size.x; + preferred_width += preferred_size.x; + + height = height.max(size.y); + + sized_items.insert(index, sized); } let margin = self.frame.total_margin(); @@ -146,6 +159,7 @@ impl<'a> WidgetLayout<'a> { for sized in sized_items { let size = sized.size(); let width = match sized { + // TODO: check for atomic.grow here SizedAtomicKind::Grow => grow_width, _ => size.x, }; @@ -246,9 +260,33 @@ pub enum AtomicKind<'a> { Grow, } +impl<'a> AtomicKind<'a> { + /// First returned argument is the preferred size. + pub fn into_sized(self, ui: &Ui, available_size: Vec2) -> (Vec2, SizedAtomicKind<'a>) { + match self { + AtomicKind::Text(text) => { + let galley = text.into_galley(ui, None, available_size.x, TextStyle::Button); + ( + galley.size(), // TODO + SizedAtomicKind::Text(galley), + ) + } + AtomicKind::Image(image) => { + let size = + image.load_and_calc_size(ui, Vec2::min(available_size, Vec2::splat(16.0))); + let size = size.unwrap_or_default(); + (size, SizedAtomicKind::Image(image, size)) + } + AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), + AtomicKind::Grow => (Vec2::ZERO, SizedAtomicKind::Grow), + } + } +} + pub struct Atomic<'a> { - size: Option, - grow: bool, + pub size: Option, + pub grow: bool, + pub shrink: bool, pub kind: AtomicKind<'a>, } @@ -256,6 +294,7 @@ pub fn a<'a>(i: impl Into>) -> Atomic<'a> { Atomic { size: None, grow: false, + shrink: false, kind: i.into(), } } @@ -275,6 +314,7 @@ impl Atomic<'_> { pub trait AtomicExt<'a> { fn a_size(self, size: Vec2) -> Atomic<'a>; fn a_grow(self, grow: bool) -> Atomic<'a>; + fn a_shrink(self, shrink: bool) -> Atomic<'a>; } impl<'a, T> AtomicExt<'a> for T @@ -292,6 +332,13 @@ where atomic.grow = grow; atomic } + + /// NOTE: Only a single atomic may be marked as shrink + fn a_shrink(self, shrink: bool) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.shrink = shrink; + atomic + } } impl<'a, T> From for Atomic<'a> @@ -302,6 +349,7 @@ where Atomic { size: None, grow: false, + shrink: false, kind: value.into(), } } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 691a71bdd95..18debfe23bc 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -223,6 +223,7 @@ impl<'a> Button<'a> { } = self; let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); + let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); let mut button_padding = if has_frame { ui.spacing().button_padding @@ -239,6 +240,23 @@ impl<'a> Button<'a> { }) } + // TODO: Pass TextWrapMode to AtomicLayout + // TODO: Should this rather be part of AtomicLayout? + // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. + if !matches!(wrap_mode, TextWrapMode::Extend) { + let any_shrink = wl.atomics.iter_mut().any(|a| a.shrink); + if !any_shrink { + let first_text = wl + .atomics + .iter_mut() + .filter(|a| matches!(a.kind, AtomicKind::Text(..))) + .next(); + if let Some(atomic) = first_text { + atomic.shrink = true; + } + } + } + let response = ui.ctx().read_response(ui.next_auto_id()); let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { From 26012cde03889d8d021fc7dd0369d681592b27a4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 16 Apr 2025 18:27:22 +0200 Subject: [PATCH 07/77] Tons of small improvements to make the new button implementation match the current one. - offset frame margin for stroke width - handle expansion - add min_size - calculate image size based on font height - correctly handle alignment / ui layout --- crates/egui/src/ui.rs | 10 ++-- crates/egui/src/widget_layout.rs | 98 ++++++++++++++++++++++++------- crates/egui/src/widgets/button.rs | 25 +++++--- 3 files changed, 99 insertions(+), 34 deletions(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 0e8509bb873..9094c00ed4e 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -25,10 +25,10 @@ use crate::{ color_picker, Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link, RadioButton, SelectableLabel, Separator, Spinner, TextEdit, Widget, }, - Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, LayerId, - Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense, - Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, WidgetRect, - WidgetText, + Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtomics, + LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, + Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, + WidgetRect, WidgetText, }; // ---------------------------------------------------------------------------- @@ -2055,7 +2055,7 @@ impl Ui { /// ``` #[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "] #[inline] - pub fn button(&mut self, text: impl Into) -> Response { + pub fn button<'a>(&mut self, text: impl IntoAtomics<'a>) -> Response { Button::new(text).ui(self) } diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index a1364b03658..6a8bc391284 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -1,7 +1,7 @@ -use crate::{Frame, Id, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText}; +use crate::{Frame, Id, Image, Response, Sense, Style, TextStyle, Ui, Widget, WidgetText}; use ahash::HashMap; -use emath::{Align2, Rect, Vec2}; -use epaint::{Color32, Galley}; +use emath::{Align2, NumExt, Rect, Vec2}; +use epaint::{Color32, Fonts, Galley}; use std::sync::Arc; pub enum SizedAtomicKind<'a> { @@ -29,6 +29,7 @@ pub struct WidgetLayout<'a> { pub(crate) frame: Frame, pub(crate) sense: Sense, fallback_text_color: Option, + min_size: Vec2, } impl<'a> WidgetLayout<'a> { @@ -39,6 +40,7 @@ impl<'a> WidgetLayout<'a> { frame: Frame::default(), sense: Sense::hover(), fallback_text_color: None, + min_size: Vec2::ZERO, } } @@ -68,14 +70,28 @@ impl<'a> WidgetLayout<'a> { self } + pub fn min_size(mut self, size: Vec2) -> Self { + self.min_size = size; + self + } + pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { + let Self { + atomics, + gap, + frame, + sense, + fallback_text_color, + min_size, + } = self; + let fallback_text_color = self .fallback_text_color .unwrap_or_else(|| ui.style().visuals.text_color()); - let gap = self.gap.unwrap_or(ui.spacing().icon_spacing); + let gap = gap.unwrap_or(ui.spacing().icon_spacing); // The size available for the content - let available_inner_size = ui.available_size() - self.frame.total_margin().sum(); + let available_inner_size = ui.available_size() - frame.total_margin().sum(); let mut desired_width = 0.0; let mut preferred_width = 0.0; @@ -88,13 +104,25 @@ impl<'a> WidgetLayout<'a> { let mut shrink_item = None; - if self.atomics.0.len() > 1 { - let gap_space = gap * (self.atomics.0.len() as f32 - 1.0); + let align2 = Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()]); + + if atomics.0.len() > 1 { + let gap_space = gap * (atomics.0.len() as f32 - 1.0); desired_width += gap_space; preferred_width += gap_space; } - for ((idx, item)) in self.atomics.0.into_iter().enumerate() { + let max_font_size = ui + .fonts(|fonts| { + atomics + .0 + .iter() + .filter_map(|a| a.get_min_height_for_image(fonts, ui.style())) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + }) + .unwrap_or_else(|| ui.text_style_height(&TextStyle::Body)); + + for ((idx, item)) in atomics.0.into_iter().enumerate() { if item.shrink { debug_assert!( shrink_item.is_none(), @@ -108,7 +136,9 @@ impl<'a> WidgetLayout<'a> { if item.grow { grow_count += 1; } - let (preferred_size, sized) = item.kind.into_sized(ui, available_inner_size); + let (preferred_size, sized) = + item.kind + .into_sized(ui, available_inner_size, max_font_size); let size = sized.size(); desired_width += size.x; @@ -125,7 +155,7 @@ impl<'a> WidgetLayout<'a> { available_inner_size.x - desired_width, available_inner_size.y, ); - let (preferred_size, sized) = item.kind.into_sized(ui, shrunk_size); + let (preferred_size, sized) = item.kind.into_sized(ui, shrunk_size, max_font_size); let size = sized.size(); desired_width += size.x; @@ -136,25 +166,31 @@ impl<'a> WidgetLayout<'a> { sized_items.insert(index, sized); } - let margin = self.frame.total_margin(); + let margin = frame.total_margin(); let content_size = Vec2::new(desired_width, height); - let frame_size = content_size + margin.sum(); + let frame_size = (content_size + margin.sum()).at_least(min_size); - let (rect, response) = ui.allocate_at_least(frame_size, self.sense); + let (rect, response) = ui.allocate_at_least(frame_size, sense); let mut response = AtomicLayoutResponse { response, custom_rects: HashMap::default(), }; - let content_rect = rect - margin; - ui.painter().add(self.frame.paint(content_rect)); + let inner_rect = rect - margin; + ui.painter().add(frame.paint(inner_rect)); - let width_to_fill = content_rect.width(); + let width_to_fill = inner_rect.width(); let extra_space = f32::max(width_to_fill - desired_width, 0.0); let grow_width = f32::max(extra_space / grow_count as f32, 0.0); - let mut cursor = content_rect.left(); + let aligned_rect = if grow_count > 0 { + align2.align_size_within_rect(Vec2::new(width_to_fill, content_size.y), inner_rect) + } else { + align2.align_size_within_rect(content_size, inner_rect) + }; + + let mut cursor = aligned_rect.left(); for sized in sized_items { let size = sized.size(); @@ -164,7 +200,7 @@ impl<'a> WidgetLayout<'a> { _ => size.x, }; - let frame = content_rect.with_min_x(cursor).with_max_x(cursor + width); + let frame = aligned_rect.with_min_x(cursor).with_max_x(cursor + width); cursor = frame.right() + gap; let align = Align2::CENTER_CENTER; @@ -262,7 +298,12 @@ pub enum AtomicKind<'a> { impl<'a> AtomicKind<'a> { /// First returned argument is the preferred size. - pub fn into_sized(self, ui: &Ui, available_size: Vec2) -> (Vec2, SizedAtomicKind<'a>) { + pub fn into_sized( + self, + ui: &Ui, + available_size: Vec2, + font_size: f32, + ) -> (Vec2, SizedAtomicKind<'a>) { match self { AtomicKind::Text(text) => { let galley = text.into_galley(ui, None, available_size.x, TextStyle::Button); @@ -272,9 +313,9 @@ impl<'a> AtomicKind<'a> { ) } AtomicKind::Image(image) => { - let size = - image.load_and_calc_size(ui, Vec2::min(available_size, Vec2::splat(16.0))); - let size = size.unwrap_or_default(); + let max_size = Vec2::splat(font_size); + let size = image.load_and_calc_size(ui, Vec2::min(available_size, max_size)); + let size = size.unwrap_or(max_size); (size, SizedAtomicKind::Image(image, size)) } AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), @@ -300,6 +341,19 @@ pub fn a<'a>(i: impl Into>) -> Atomic<'a> { } impl Atomic<'_> { + fn get_min_height_for_image(&self, fonts: &Fonts, style: &Style) -> Option { + self.size.map(|s| s.y).or_else(|| { + match &self.kind { + AtomicKind::Text(text) => Some(text.font_height(fonts, style)), + AtomicKind::Custom(_, size) => Some(size.y), + AtomicKind::Grow => None, + // Since this method is used to calculate the best height for an image, we always return + // None for images. + AtomicKind::Image(_) => None, + } + }) + } + // pub fn size(mut self, size: Vec2) -> Self { // self.size = Some(size); // self diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 18debfe23bc..5e10e6ee801 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -184,10 +184,12 @@ impl<'a> Button<'a> { /// See also [`Self::right_text`]. #[inline] pub fn shortcut_text(mut self, shortcut_text: impl Into>) -> Self { - self.wl = self - .wl - .add(AtomicKind::Grow.a_grow(true)) - .add(shortcut_text); + let mut atomic = shortcut_text.into(); + atomic.kind = match atomic.kind { + AtomicKind::Text(text) => AtomicKind::Text(text.weak()), + other => other, + }; + self.wl = self.wl.add(AtomicKind::Grow.a_grow(true)).add(atomic); self } @@ -215,7 +217,7 @@ impl<'a> Button<'a> { stroke, small, frame, - min_size, + mut min_size, corner_radius, selected, image_tint_follows_text_color, @@ -266,15 +268,24 @@ impl<'a> Button<'a> { wl = wl.fallback_text_color(visuals.text_color()); wl.frame = if has_frame { + let stroke = stroke.unwrap_or(visuals.bg_stroke); wl.frame - .inner_margin(button_padding) + .inner_margin( + button_padding + Vec2::splat(visuals.expansion) - Vec2::splat(stroke.width), + ) + .outer_margin(-Vec2::splat(visuals.expansion)) .fill(fill.unwrap_or(visuals.weak_bg_fill)) - .stroke(stroke.unwrap_or(visuals.bg_stroke)) + .stroke(stroke) .corner_radius(corner_radius.unwrap_or(visuals.corner_radius)) } else { Frame::new() }; + if !small { + min_size.y = min_size.y.at_least(ui.spacing().interact_size.y); + } + wl = wl.min_size(min_size); + let text = wl.atomics.text(); let response = wl.show(ui); From c327d99b3d705a5be0aa5b85dbd9e138c05d1295 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 00:00:44 +0200 Subject: [PATCH 08/77] Improve default font height based on https://github.com/emilk/egui/pull/5809 --- crates/egui/src/widget_layout.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 6a8bc391284..b77e4a7df0b 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -1,4 +1,6 @@ -use crate::{Frame, Id, Image, Response, Sense, Style, TextStyle, Ui, Widget, WidgetText}; +use crate::{ + FontSelection, Frame, Id, Image, Response, Sense, Style, TextStyle, Ui, Widget, WidgetText, +}; use ahash::HashMap; use emath::{Align2, NumExt, Rect, Vec2}; use epaint::{Color32, Fonts, Galley}; @@ -112,6 +114,12 @@ impl<'a> WidgetLayout<'a> { preferred_width += gap_space; } + let default_font_height = || { + let font_selection = FontSelection::default(); + let font_id = font_selection.resolve(ui.style()); + ui.fonts(|f| f.row_height(&font_id)) + }; + let max_font_size = ui .fonts(|fonts| { atomics @@ -120,7 +128,7 @@ impl<'a> WidgetLayout<'a> { .filter_map(|a| a.get_min_height_for_image(fonts, ui.style())) .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) }) - .unwrap_or_else(|| ui.text_style_height(&TextStyle::Body)); + .unwrap_or_else(default_font_height); for ((idx, item)) in atomics.0.into_iter().enumerate() { if item.shrink { From 160a1f565b64a2898bf00570e7ffcbb4846499f5 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 00:41:11 +0200 Subject: [PATCH 09/77] Allow customizing id --- crates/egui/src/widget_layout.rs | 13 ++++++++++++- crates/egui/src/widgets/button.rs | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index b77e4a7df0b..12ad2f985a7 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -26,6 +26,7 @@ impl SizedAtomicKind<'_> { /// AtomicLayout pub struct WidgetLayout<'a> { + id: Option, pub atomics: Atomics<'a>, gap: Option, pub(crate) frame: Frame, @@ -37,6 +38,7 @@ pub struct WidgetLayout<'a> { impl<'a> WidgetLayout<'a> { pub fn new(atomics: impl IntoAtomics<'a>) -> Self { Self { + id: None, atomics: atomics.into_atomics(), gap: None, frame: Frame::default(), @@ -77,8 +79,14 @@ impl<'a> WidgetLayout<'a> { self } + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { let Self { + id, atomics, gap, frame, @@ -87,6 +95,8 @@ impl<'a> WidgetLayout<'a> { min_size, } = self; + let id = id.unwrap_or_else(|| ui.next_auto_id()); + let fallback_text_color = self .fallback_text_color .unwrap_or_else(|| ui.style().visuals.text_color()); @@ -178,7 +188,8 @@ impl<'a> WidgetLayout<'a> { let content_size = Vec2::new(desired_width, height); let frame_size = (content_size + margin.sum()).at_least(min_size); - let (rect, response) = ui.allocate_at_least(frame_size, sense); + let (_, rect) = ui.allocate_space(frame_size); + let response = ui.interact(rect, id, sense); let mut response = AtomicLayoutResponse { response, diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 5e10e6ee801..3d15dc2a5a8 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -259,7 +259,9 @@ impl<'a> Button<'a> { } } - let response = ui.ctx().read_response(ui.next_auto_id()); + let id = ui.next_auto_id().with("egui::button"); + wl = wl.id(id); + let response = ui.ctx().read_response(id); let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { ui.style().interact(&response) From 6046323fb36d4526e5cf693e8d61ef498b22a0d0 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 11:50:26 +0200 Subject: [PATCH 10/77] Set preferred size --- crates/egui/src/widget_layout.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 12ad2f985a7..62cfc13c8f7 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -107,6 +107,7 @@ impl<'a> WidgetLayout<'a> { let mut desired_width = 0.0; let mut preferred_width = 0.0; + let mut preferred_height = 0.0; let mut height: f32 = 0.0; @@ -162,7 +163,8 @@ impl<'a> WidgetLayout<'a> { desired_width += size.x; preferred_width += preferred_size.x; - height = height.max(size.y); + height = height.at_least(size.y); + preferred_height = preferred_height.at_least(preferred_size.y); sized_items.push(sized); } @@ -179,7 +181,8 @@ impl<'a> WidgetLayout<'a> { desired_width += size.x; preferred_width += preferred_size.x; - height = height.max(size.y); + height = height.at_least(size.y); + preferred_height = preferred_height.at_least(preferred_size.y); sized_items.insert(index, sized); } @@ -239,6 +242,9 @@ impl<'a> WidgetLayout<'a> { } } + response.response.intrinsic_size = + Some(Vec2::new(preferred_width, preferred_height) + margin.sum()); + response } } From 324d20a87785565d844aac2f638ff1ab2ef07cb6 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 12:35:03 +0200 Subject: [PATCH 11/77] Handle min_size for intrinsic size --- crates/egui/src/widget_layout.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 62cfc13c8f7..3d709ea65cc 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -192,7 +192,10 @@ impl<'a> WidgetLayout<'a> { let frame_size = (content_size + margin.sum()).at_least(min_size); let (_, rect) = ui.allocate_space(frame_size); - let response = ui.interact(rect, id, sense); + let mut response = ui.interact(rect, id, sense); + + response.intrinsic_size = + Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size)); let mut response = AtomicLayoutResponse { response, @@ -242,9 +245,6 @@ impl<'a> WidgetLayout<'a> { } } - response.response.intrinsic_size = - Some(Vec2::new(preferred_width, preferred_height) + margin.sum()); - response } } From 6191e28a1c049b919e505ff6815c74bb332cba10 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 17:47:53 +0200 Subject: [PATCH 12/77] Correctly handle TextWrapMode --- crates/egui/src/widget_layout.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 3d709ea65cc..237913fb537 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -3,6 +3,7 @@ use crate::{ }; use ahash::HashMap; use emath::{Align2, NumExt, Rect, Vec2}; +use epaint::text::TextWrapMode; use epaint::{Color32, Fonts, Galley}; use std::sync::Arc; @@ -33,6 +34,7 @@ pub struct WidgetLayout<'a> { pub(crate) sense: Sense, fallback_text_color: Option, min_size: Vec2, + wrap_mode: Option, } impl<'a> WidgetLayout<'a> { @@ -45,6 +47,7 @@ impl<'a> WidgetLayout<'a> { sense: Sense::hover(), fallback_text_color: None, min_size: Vec2::ZERO, + wrap_mode: None, } } @@ -84,6 +87,11 @@ impl<'a> WidgetLayout<'a> { self } + pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { + self.wrap_mode = Some(wrap_mode); + self + } + pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { let Self { id, @@ -93,6 +101,7 @@ impl<'a> WidgetLayout<'a> { sense, fallback_text_color, min_size, + wrap_mode, } = self; let id = id.unwrap_or_else(|| ui.next_auto_id()); @@ -157,7 +166,7 @@ impl<'a> WidgetLayout<'a> { } let (preferred_size, sized) = item.kind - .into_sized(ui, available_inner_size, max_font_size); + .into_sized(ui, available_inner_size, max_font_size, wrap_mode); let size = sized.size(); desired_width += size.x; @@ -175,7 +184,9 @@ impl<'a> WidgetLayout<'a> { available_inner_size.x - desired_width, available_inner_size.y, ); - let (preferred_size, sized) = item.kind.into_sized(ui, shrunk_size, max_font_size); + let (preferred_size, sized) = + item.kind + .into_sized(ui, shrunk_size, max_font_size, wrap_mode); let size = sized.size(); desired_width += size.x; @@ -328,10 +339,11 @@ impl<'a> AtomicKind<'a> { ui: &Ui, available_size: Vec2, font_size: f32, + wrap_mode: Option, ) -> (Vec2, SizedAtomicKind<'a>) { match self { AtomicKind::Text(text) => { - let galley = text.into_galley(ui, None, available_size.x, TextStyle::Button); + let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); ( galley.size(), // TODO SizedAtomicKind::Text(galley), From f3061ff4a62cc343736e6e4e16038b3c4f99d460 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 19:13:44 +0200 Subject: [PATCH 13/77] Add align2 option --- crates/egui/src/widget_layout.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 237913fb537..c9b1512bf3e 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -35,6 +35,7 @@ pub struct WidgetLayout<'a> { fallback_text_color: Option, min_size: Vec2, wrap_mode: Option, + align2: Option, } impl<'a> WidgetLayout<'a> { @@ -48,6 +49,7 @@ impl<'a> WidgetLayout<'a> { fallback_text_color: None, min_size: Vec2::ZERO, wrap_mode: None, + align2: None, } } @@ -92,6 +94,11 @@ impl<'a> WidgetLayout<'a> { self } + pub fn align2(mut self, align2: Align2) -> Self { + self.align2 = Some(align2); + self + } + pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { let Self { id, @@ -102,6 +109,7 @@ impl<'a> WidgetLayout<'a> { fallback_text_color, min_size, wrap_mode, + align2, } = self; let id = id.unwrap_or_else(|| ui.next_auto_id()); @@ -126,7 +134,9 @@ impl<'a> WidgetLayout<'a> { let mut shrink_item = None; - let align2 = Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()]); + let align2 = align2.unwrap_or_else(|| { + Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()]) + }); if atomics.0.len() > 1 { let gap_space = gap * (atomics.0.len() as f32 - 1.0); From db3e3c77548e14c03b6ee677fb5dcf8d5ffb504f Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 20:00:12 +0200 Subject: [PATCH 14/77] Handle a_size correctly --- crates/egui/src/widget_layout.rs | 51 ++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index c9b1512bf3e..55269853f10 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -174,16 +174,14 @@ impl<'a> WidgetLayout<'a> { if item.grow { grow_count += 1; } - let (preferred_size, sized) = - item.kind - .into_sized(ui, available_inner_size, max_font_size, wrap_mode); - let size = sized.size(); + let sized = item.into_sized(ui, available_inner_size, max_font_size, wrap_mode); + let size = sized.size; desired_width += size.x; - preferred_width += preferred_size.x; + preferred_width += sized.preferred_size.x; height = height.at_least(size.y); - preferred_height = preferred_height.at_least(preferred_size.y); + preferred_height = preferred_height.at_least(sized.preferred_size.y); sized_items.push(sized); } @@ -194,16 +192,14 @@ impl<'a> WidgetLayout<'a> { available_inner_size.x - desired_width, available_inner_size.y, ); - let (preferred_size, sized) = - item.kind - .into_sized(ui, shrunk_size, max_font_size, wrap_mode); - let size = sized.size(); + let sized = item.into_sized(ui, shrunk_size, max_font_size, wrap_mode); + let size = sized.size; desired_width += size.x; - preferred_width += preferred_size.x; + preferred_width += sized.preferred_size.x; height = height.at_least(size.y); - preferred_height = preferred_height.at_least(preferred_size.y); + preferred_height = preferred_height.at_least(sized.preferred_size.y); sized_items.insert(index, sized); } @@ -239,8 +235,8 @@ impl<'a> WidgetLayout<'a> { let mut cursor = aligned_rect.left(); for sized in sized_items { - let size = sized.size(); - let width = match sized { + let size = sized.size; + let width = match sized.kind { // TODO: check for atomic.grow here SizedAtomicKind::Grow => grow_width, _ => size.x, @@ -252,7 +248,7 @@ impl<'a> WidgetLayout<'a> { let align = Align2::CENTER_CENTER; let rect = align.align_size_within_rect(size, frame); - match sized { + match sized.kind { SizedAtomicKind::Text(galley) => { ui.painter().galley(rect.min, galley, fallback_text_color); } @@ -378,6 +374,12 @@ pub struct Atomic<'a> { pub kind: AtomicKind<'a>, } +struct SizedAtomic<'a> { + size: Vec2, + preferred_size: Vec2, + kind: SizedAtomicKind<'a>, +} + pub fn a<'a>(i: impl Into>) -> Atomic<'a> { Atomic { size: None, @@ -387,7 +389,7 @@ pub fn a<'a>(i: impl Into>) -> Atomic<'a> { } } -impl Atomic<'_> { +impl<'a> Atomic<'a> { fn get_min_height_for_image(&self, fonts: &Fonts, style: &Style) -> Option { self.size.map(|s| s.y).or_else(|| { match &self.kind { @@ -401,6 +403,23 @@ impl Atomic<'_> { }) } + fn into_sized( + self, + ui: &Ui, + available_size: Vec2, + font_size: f32, + wrap_mode: Option, + ) -> SizedAtomic<'a> { + let (preferred, kind) = self + .kind + .into_sized(ui, available_size, font_size, wrap_mode); + SizedAtomic { + size: self.size.unwrap_or_else(|| kind.size()), + preferred_size: preferred, + kind, + } + } + // pub fn size(mut self, size: Vec2) -> Self { // self.size = Some(size); // self From 3b13333573f7c8f7e719471e38dee4b3cc428030 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 09:34:24 +0200 Subject: [PATCH 15/77] Minor improvements --- crates/egui/src/widgets/button.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 3d15dc2a5a8..5cfa30ca623 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ - widgets, Align, Atomic, AtomicExt, AtomicKind, AtomicLayoutResponse, Color32, CornerRadius, - Frame, Image, IntoAtomics, NumExt, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, - Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType, + Atomic, AtomicExt, AtomicKind, AtomicLayoutResponse, Color32, CornerRadius, Frame, Image, + IntoAtomics, NumExt, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, + WidgetLayout, WidgetText, WidgetType, }; /// Clickable button with text. @@ -234,12 +234,11 @@ impl<'a> Button<'a> { }; if small { button_padding.y = 0.0; - wl.atomics.iter_mut().for_each(|a| match &mut a.kind { - AtomicKind::Text(text) => { + wl.atomics.iter_mut().for_each(|a| { + if let AtomicKind::Text(text) = &mut a.kind { *text = std::mem::take(text).small(); } - _ => {} - }) + }); } // TODO: Pass TextWrapMode to AtomicLayout From 2a207f7418ad125e47ef1437067051477f47ff7e Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 10:13:00 +0200 Subject: [PATCH 16/77] Implement selectable button --- crates/egui/src/widgets/button.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 5cfa30ca623..f79139fbf92 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -262,20 +262,21 @@ impl<'a> Button<'a> { wl = wl.id(id); let response = ui.ctx().read_response(id); - let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { - ui.style().interact(&response) + let visuals = response.map_or(ui.style().visuals.widgets.inactive, |response| { + ui.style().interact_selectable(&response, selected) }); wl = wl.fallback_text_color(visuals.text_color()); wl.frame = if has_frame { let stroke = stroke.unwrap_or(visuals.bg_stroke); + let fill = fill.unwrap_or(visuals.bg_fill); wl.frame .inner_margin( button_padding + Vec2::splat(visuals.expansion) - Vec2::splat(stroke.width), ) .outer_margin(-Vec2::splat(visuals.expansion)) - .fill(fill.unwrap_or(visuals.weak_bg_fill)) + .fill(fill) .stroke(stroke) .corner_radius(corner_radius.unwrap_or(visuals.corner_radius)) } else { From b33e4d29c70d1d6e59757e639bed0d64b15a72ef Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 10:36:36 +0200 Subject: [PATCH 17/77] Move wrap mode handling to atomics --- crates/egui/src/widget_layout.rs | 28 ++++++++++++++++++++++++---- crates/egui/src/widgets/button.rs | 18 ------------------ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 55269853f10..6e0a357ab23 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -102,7 +102,7 @@ impl<'a> WidgetLayout<'a> { pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { let Self { id, - atomics, + mut atomics, gap, frame, sense, @@ -112,6 +112,22 @@ impl<'a> WidgetLayout<'a> { align2, } = self; + let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); + + // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. + // If none is found, mark the first text item as `shrink`. + if !matches!(wrap_mode, TextWrapMode::Extend) { + let any_shrink = atomics.iter().any(|a| a.shrink); + if !any_shrink { + let first_text = atomics + .iter_mut() + .find(|a| matches!(a.kind, AtomicKind::Text(..))); + if let Some(atomic) = first_text { + atomic.shrink = true; + } + } + } + let id = id.unwrap_or_else(|| ui.next_auto_id()); let fallback_text_color = self @@ -174,7 +190,7 @@ impl<'a> WidgetLayout<'a> { if item.grow { grow_count += 1; } - let sized = item.into_sized(ui, available_inner_size, max_font_size, wrap_mode); + let sized = item.into_sized(ui, available_inner_size, max_font_size, Some(wrap_mode)); let size = sized.size; desired_width += size.x; @@ -188,11 +204,11 @@ impl<'a> WidgetLayout<'a> { if let Some((index, item)) = shrink_item { // The `shrink` item gets the remaining space - let mut shrunk_size = Vec2::new( + let shrunk_size = Vec2::new( available_inner_size.x - desired_width, available_inner_size.y, ); - let sized = item.into_sized(ui, shrunk_size, max_font_size, wrap_mode); + let sized = item.into_sized(ui, shrunk_size, max_font_size, Some(wrap_mode)); let size = sized.size; desired_width += size.x; @@ -507,6 +523,10 @@ impl<'a> Atomics<'a> { self.0.insert(0, atomic.into()); } + pub fn iter(&self) -> impl Iterator> { + self.0.iter() + } + pub fn iter_mut(&mut self) -> impl Iterator> { self.0.iter_mut() } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index f79139fbf92..a32dff49e73 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -225,7 +225,6 @@ impl<'a> Button<'a> { } = self; let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); - let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); let mut button_padding = if has_frame { ui.spacing().button_padding @@ -241,23 +240,6 @@ impl<'a> Button<'a> { }); } - // TODO: Pass TextWrapMode to AtomicLayout - // TODO: Should this rather be part of AtomicLayout? - // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. - if !matches!(wrap_mode, TextWrapMode::Extend) { - let any_shrink = wl.atomics.iter_mut().any(|a| a.shrink); - if !any_shrink { - let first_text = wl - .atomics - .iter_mut() - .filter(|a| matches!(a.kind, AtomicKind::Text(..))) - .next(); - if let Some(atomic) = first_text { - atomic.shrink = true; - } - } - } - let id = ui.next_auto_id().with("egui::button"); wl = wl.id(id); let response = ui.ctx().read_response(id); From c7afe9209fb2df5fa6e776b3baffd960b3a476f5 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 10:53:23 +0200 Subject: [PATCH 18/77] Make button hold Atomics instead of AtomicLayout and rename Kind::Grow to Kind::Empty --- crates/egui/src/widget_layout.rs | 53 ++++++++++++++++--------------- crates/egui/src/widgets/button.rs | 34 +++++++++++--------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 6e0a357ab23..bb2da86df32 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -11,7 +11,7 @@ pub enum SizedAtomicKind<'a> { Text(Arc), Image(Image<'a>, Vec2), Custom(Id, Vec2), - Grow, + Empty, } impl SizedAtomicKind<'_> { @@ -20,7 +20,7 @@ impl SizedAtomicKind<'_> { SizedAtomicKind::Text(galley) => galley.size(), SizedAtomicKind::Image(_, size) => *size, SizedAtomicKind::Custom(_, size) => *size, - SizedAtomicKind::Grow => Vec2::ZERO, + SizedAtomicKind::Empty => Vec2::ZERO, } } } @@ -254,7 +254,7 @@ impl<'a> WidgetLayout<'a> { let size = sized.size; let width = match sized.kind { // TODO: check for atomic.grow here - SizedAtomicKind::Grow => grow_width, + SizedAtomicKind::Empty => grow_width, _ => size.x, }; @@ -274,7 +274,7 @@ impl<'a> WidgetLayout<'a> { SizedAtomicKind::Custom(id, size) => { response.custom_rects.insert(id, rect); } - SizedAtomicKind::Grow => {} + SizedAtomicKind::Empty => {} } } @@ -351,10 +351,22 @@ pub enum AtomicKind<'a> { Text(WidgetText), Image(Image<'a>), Custom(Id, Vec2), - Grow, + Empty, } impl<'a> AtomicKind<'a> { + pub fn text(text: impl Into) -> Self { + AtomicKind::Text(text.into()) + } + + pub fn image(image: impl Into>) -> Self { + AtomicKind::Image(image.into()) + } + + pub fn custom(id: Id, size: Vec2) -> Self { + AtomicKind::Custom(id, size) + } + /// First returned argument is the preferred size. pub fn into_sized( self, @@ -378,7 +390,7 @@ impl<'a> AtomicKind<'a> { (size, SizedAtomicKind::Image(image, size)) } AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), - AtomicKind::Grow => (Vec2::ZERO, SizedAtomicKind::Grow), + AtomicKind::Empty => (Vec2::ZERO, SizedAtomicKind::Empty), } } } @@ -396,22 +408,23 @@ struct SizedAtomic<'a> { kind: SizedAtomicKind<'a>, } -pub fn a<'a>(i: impl Into>) -> Atomic<'a> { - Atomic { - size: None, - grow: false, - shrink: false, - kind: i.into(), +impl<'a> Atomic<'a> { + /// Create an empty [`Atomic`] marked as `grow`. + pub fn grow() -> Self { + Atomic { + size: None, + grow: true, + shrink: false, + kind: AtomicKind::Empty, + } } -} -impl<'a> Atomic<'a> { fn get_min_height_for_image(&self, fonts: &Fonts, style: &Style) -> Option { self.size.map(|s| s.y).or_else(|| { match &self.kind { AtomicKind::Text(text) => Some(text.font_height(fonts, style)), AtomicKind::Custom(_, size) => Some(size.y), - AtomicKind::Grow => None, + AtomicKind::Empty => None, // Since this method is used to calculate the best height for an image, we always return // None for images. AtomicKind::Image(_) => None, @@ -435,16 +448,6 @@ impl<'a> Atomic<'a> { kind, } } - - // pub fn size(mut self, size: Vec2) -> Self { - // self.size = Some(size); - // self - // } - // - // pub fn grow(mut self, grow: bool) -> Self { - // self.grow = grow; - // self - // } } pub trait AtomicExt<'a> { diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index a32dff49e73..4ca460c5372 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,5 +1,5 @@ use crate::{ - Atomic, AtomicExt, AtomicKind, AtomicLayoutResponse, Color32, CornerRadius, Frame, Image, + Atomic, AtomicKind, AtomicLayoutResponse, Atomics, Color32, CornerRadius, Frame, Image, IntoAtomics, NumExt, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType, }; @@ -24,34 +24,34 @@ use crate::{ /// ``` #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Button<'a> { + atomics: Atomics<'a>, wrap_mode: Option, - /// None means default for interact fill: Option, stroke: Option, + sense: Sense, small: bool, frame: Option, min_size: Vec2, corner_radius: Option, selected: bool, image_tint_follows_text_color: bool, - - wl: WidgetLayout<'a>, } impl<'a> Button<'a> { - pub fn new(text: impl IntoAtomics<'a>) -> Self { + pub fn new(content: impl IntoAtomics<'a>) -> Self { Self { + atomics: content.into_atomics(), wrap_mode: None, fill: None, stroke: None, + sense: Sense::click(), small: false, frame: None, min_size: Vec2::ZERO, corner_radius: None, selected: false, image_tint_follows_text_color: false, - wl: WidgetLayout::new(text.into_atomics()).sense(Sense::click()), } } @@ -70,10 +70,10 @@ impl<'a> Button<'a> { pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { - button.wl.atomics.add(image); + button.atomics.add(image); } if let Some(text) = text { - button.wl.atomics.add(text); + button.atomics.add(text); } button } @@ -139,7 +139,7 @@ impl<'a> Button<'a> { /// Change this to a drag-button with `Sense::drag()`. #[inline] pub fn sense(mut self, sense: Sense) -> Self { - self.wl.sense = sense; + self.sense = sense; self } @@ -189,17 +189,16 @@ impl<'a> Button<'a> { AtomicKind::Text(text) => AtomicKind::Text(text.weak()), other => other, }; - self.wl = self.wl.add(AtomicKind::Grow.a_grow(true)).add(atomic); + self.atomics.add(Atomic::grow()); + self.atomics.add(atomic); self } /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl Into>) -> Self { - self.wl = self - .wl - .add(AtomicKind::Grow.a_grow(true)) - .add(right_text.into()); + self.atomics.add(Atomic::grow()); + self.atomics.add(right_text.into()); self } @@ -212,18 +211,23 @@ impl<'a> Button<'a> { pub fn atomic_ui(mut self, ui: &mut Ui) -> AtomicLayoutResponse { let Button { + atomics, wrap_mode, fill, stroke, + sense, small, frame, mut min_size, corner_radius, selected, image_tint_follows_text_color, - mut wl, } = self; + let mut wl = WidgetLayout::new(atomics) + .wrap_mode(wrap_mode.unwrap_or(ui.wrap_mode())) + .sense(sense); + let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); let mut button_padding = if has_frame { From 6bcf455f31f4f59b3e195da199a64f6b52908a91 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 10:56:15 +0200 Subject: [PATCH 19/77] Use weak_bg_fill, fixing menus --- crates/egui/src/widgets/button.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 4ca460c5372..8199c2995c5 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -256,7 +256,7 @@ impl<'a> Button<'a> { wl.frame = if has_frame { let stroke = stroke.unwrap_or(visuals.bg_stroke); - let fill = fill.unwrap_or(visuals.bg_fill); + let fill = fill.unwrap_or(visuals.weak_bg_fill); wl.frame .inner_margin( button_padding + Vec2::splat(visuals.expansion) - Vec2::splat(stroke.width), From 2d24b4845bff3f7cb82e3d8a2c5d6cf8e9385e00 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 11:08:29 +0200 Subject: [PATCH 20/77] Implement image_tint_follows_text_color --- crates/egui/src/widget_layout.rs | 4 +++- crates/egui/src/widgets/button.rs | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index bb2da86df32..f30113b4378 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -347,11 +347,13 @@ pub struct AtomicLayoutResponse { // } // } +#[derive(Clone, Default)] pub enum AtomicKind<'a> { + #[default] + Empty, Text(WidgetText), Image(Image<'a>), Custom(Id, Vec2), - Empty, } impl<'a> AtomicKind<'a> { diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 8199c2995c5..f1a74379df1 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -209,7 +209,7 @@ impl<'a> Button<'a> { self } - pub fn atomic_ui(mut self, ui: &mut Ui) -> AtomicLayoutResponse { + pub fn atomic_ui(self, ui: &mut Ui) -> AtomicLayoutResponse { let Button { atomics, wrap_mode, @@ -252,6 +252,15 @@ impl<'a> Button<'a> { ui.style().interact_selectable(&response, selected) }); + if image_tint_follows_text_color { + wl.atomics.iter_mut().for_each(|a| { + a.kind = match std::mem::take(&mut a.kind) { + AtomicKind::Image(image) => AtomicKind::Image(image.tint(visuals.text_color())), + other => other, + } + }); + } + wl = wl.fallback_text_color(visuals.text_color()); wl.frame = if has_frame { From 1d36767ab1d7d10b7d4c988df766cf9f1034bbe8 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 11:18:29 +0200 Subject: [PATCH 21/77] Rename to AtomicLayout --- .../{widget_layout.rs => atomic_layout.rs} | 67 +------------------ crates/egui/src/lib.rs | 4 +- crates/egui/src/widgets/button.rs | 8 +-- crates/egui/src/widgets/checkbox.rs | 8 +-- examples/hello_world_simple/src/main.rs | 6 +- 5 files changed, 18 insertions(+), 75 deletions(-) rename crates/egui/src/{widget_layout.rs => atomic_layout.rs} (88%) diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/atomic_layout.rs similarity index 88% rename from crates/egui/src/widget_layout.rs rename to crates/egui/src/atomic_layout.rs index f30113b4378..c1d3239a10d 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -25,8 +25,7 @@ impl SizedAtomicKind<'_> { } } -/// AtomicLayout -pub struct WidgetLayout<'a> { +pub struct AtomicLayout<'a> { id: Option, pub atomics: Atomics<'a>, gap: Option, @@ -38,7 +37,7 @@ pub struct WidgetLayout<'a> { align2: Option, } -impl<'a> WidgetLayout<'a> { +impl<'a> AtomicLayout<'a> { pub fn new(atomics: impl IntoAtomics<'a>) -> Self { Self { id: None, @@ -287,66 +286,6 @@ pub struct AtomicLayoutResponse { pub custom_rects: HashMap, } -// pub struct WLButton<'a> { -// wl: WidgetLayout<'a>, -// } -// -// impl<'a> WLButton<'a> { -// pub fn new(text: impl Into) -> Self { -// Self { -// wl: WidgetLayout::new() -// .sense(Sense::click()) -// .add(Item::default(), WidgetLayoutItemType::Text(text.into())), -// } -// } -// -// pub fn image(image: impl Into>) -> Self { -// Self { -// wl: WidgetLayout::new().sense(Sense::click()).add( -// Item::default(), -// WidgetLayoutItemType::Image(image.into().max_size(Vec2::splat(16.0))), -// ), -// } -// } -// -// pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { -// Self { -// wl: WidgetLayout::new() -// .sense(Sense::click()) -// .add(Item::default(), WidgetLayoutItemType::Image(image.into())) -// .add(Item::default(), WidgetLayoutItemType::Text(text.into())), -// } -// } -// -// pub fn right_text(mut self, text: impl Into) -> Self { -// self.wl = self -// .wl -// .add(Item::default(), WidgetLayoutItemType::Grow) -// .add(Item::default(), WidgetLayoutItemType::Text(text.into())); -// self -// } -// } -// -// impl<'a> Widget for WLButton<'a> { -// fn ui(mut self, ui: &mut Ui) -> Response { -// let response = ui.ctx().read_response(ui.next_auto_id()); -// -// let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { -// ui.style().interact(&response) -// }); -// -// self.wl.frame = self -// .wl -// .frame -// .inner_margin(ui.style().spacing.button_padding) -// .fill(visuals.bg_fill) -// .stroke(visuals.bg_stroke) -// .corner_radius(visuals.corner_radius); -// -// self.wl.show(ui) -// } -// } - #[derive(Clone, Default)] pub enum AtomicKind<'a> { #[default] @@ -613,7 +552,7 @@ all_the_atomics!(T0, T1, T2, T3, T4, T5); // } // } -impl Widget for WidgetLayout<'_> { +impl Widget for AtomicLayout<'_> { fn ui(self, ui: &mut Ui) -> Response { self.show(ui).response } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 512caac990a..31c54c01857 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -441,10 +441,10 @@ mod widget_rect; pub mod widget_text; pub mod widgets; +mod atomic_layout; #[cfg(feature = "callstack")] #[cfg(debug_assertions)] mod callstack; -mod widget_layout; #[cfg(feature = "accesskit")] pub use accesskit; @@ -480,6 +480,7 @@ pub mod text { } pub use self::{ + atomic_layout::*, containers::*, context::{Context, RepaintCause, RequestRepaintInfo}, data::{ @@ -508,7 +509,6 @@ pub use self::{ ui_builder::UiBuilder, ui_stack::*, viewport::*, - widget_layout::*, widget_rect::{WidgetRect, WidgetRects}, widget_text::{RichText, WidgetText}, widgets::*, diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index f1a74379df1..d37a2bb5caf 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ - Atomic, AtomicKind, AtomicLayoutResponse, Atomics, Color32, CornerRadius, Frame, Image, - IntoAtomics, NumExt, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, - WidgetLayout, WidgetText, WidgetType, + Atomic, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, CornerRadius, Frame, + Image, IntoAtomics, NumExt, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, + WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -224,7 +224,7 @@ impl<'a> Button<'a> { image_tint_follows_text_color, } = self; - let mut wl = WidgetLayout::new(atomics) + let mut wl = AtomicLayout::new(atomics) .wrap_mode(wrap_mode.unwrap_or(ui.wrap_mode())) .sense(sense); diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index b6fb557640e..2579387b82f 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,7 +1,7 @@ use crate::AtomicKind::Custom; use crate::{ - epaint, pos2, vec2, Atomics, Id, IntoAtomics, NumExt, Response, Sense, Shape, TextStyle, Ui, - Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType, + epaint, pos2, vec2, AtomicLayout, Atomics, Id, IntoAtomics, NumExt, Response, Sense, Shape, + TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; // TODO(emilk): allow checkbox without a text label @@ -59,12 +59,12 @@ impl Widget for Checkbox<'_> { let spacing = &ui.spacing(); let icon_width = spacing.icon_width; - let rect_id = Id::new("checkbox"); + let rect_id = Id::new("egui::checkbox"); atomics.add_front(Custom(rect_id, Vec2::splat(icon_width))); let text = atomics.text(); - let mut response = WidgetLayout::new(atomics).sense(Sense::click()).show(ui); + let mut response = AtomicLayout::new(atomics).sense(Sense::click()).show(ui); if response.response.clicked() { *checked = !*checked; diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index b0d62a8481c..1b57e5b5ebe 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -41,7 +41,11 @@ fn main() -> eframe::Result { let source = include_image!("../../../crates/eframe/data/icon.png"); let response = Button::image_and_text(source.clone(), "Hello World").ui(ui); - Button::new((Image::new(source).tint(egui::Color32::RED), "Tuple Button")).ui(ui); + Button::new((Image::new(source).tint(egui::Color32::RED), "Tuple Button")) + .selected(true) + .ui(ui); + + ui.selectable_label(true, "Selectable Label"); Popup::menu(&response).show(|ui| { Button::new("Print") From 61a513a7b311f95e675d5b1092aeec6dbdf6857f Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 13:43:06 +0200 Subject: [PATCH 22/77] Split WidgetRect in allocate and paint. Make checkbox pixel perfect. Properly handle grow. --- crates/egui/src/atomic_layout.rs | 92 ++++++++++++++++++++-------- crates/egui/src/widgets/button.rs | 94 +++++++++++++++-------------- crates/egui/src/widgets/checkbox.rs | 90 +++++++++++++++------------ 3 files changed, 171 insertions(+), 105 deletions(-) diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs index c1d3239a10d..b9e6698f974 100644 --- a/crates/egui/src/atomic_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -1,17 +1,19 @@ use crate::{ FontSelection, Frame, Id, Image, Response, Sense, Style, TextStyle, Ui, Widget, WidgetText, }; -use ahash::HashMap; +use ahash::{HashMap, HashMapExt}; use emath::{Align2, NumExt, Rect, Vec2}; use epaint::text::TextWrapMode; use epaint::{Color32, Fonts, Galley}; use std::sync::Arc; +#[derive(Clone, Default)] pub enum SizedAtomicKind<'a> { + #[default] + Empty, Text(Arc), Image(Image<'a>, Vec2), Custom(Id, Vec2), - Empty, } impl SizedAtomicKind<'_> { @@ -99,6 +101,10 @@ impl<'a> AtomicLayout<'a> { } pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { + self.allocate(ui).paint(ui) + } + + pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomicLayout<'a> { let Self { id, mut atomics, @@ -129,9 +135,8 @@ impl<'a> AtomicLayout<'a> { let id = id.unwrap_or_else(|| ui.next_auto_id()); - let fallback_text_color = self - .fallback_text_color - .unwrap_or_else(|| ui.style().visuals.text_color()); + let fallback_text_color = + fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color()); let gap = gap.unwrap_or(ui.spacing().icon_spacing); // The size available for the content @@ -220,8 +225,8 @@ impl<'a> AtomicLayout<'a> { } let margin = frame.total_margin(); - let content_size = Vec2::new(desired_width, height); - let frame_size = (content_size + margin.sum()).at_least(min_size); + let desired_size = Vec2::new(desired_width, height); + let frame_size = (desired_size + margin.sum()).at_least(min_size); let (_, rect) = ui.allocate_space(frame_size); let mut response = ui.interact(rect, id, sense); @@ -229,35 +234,72 @@ impl<'a> AtomicLayout<'a> { response.intrinsic_size = Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size)); - let mut response = AtomicLayoutResponse { + AllocatedAtomicLayout { + sized_atomics: sized_items, + frame, + fallback_text_color, response, - custom_rects: HashMap::default(), - }; + grow_count, + desired_size, + align2, + gap, + } + } +} + +pub struct AllocatedAtomicLayout<'a> { + pub sized_atomics: Vec>, + pub frame: Frame, + pub fallback_text_color: Color32, + pub response: Response, + grow_count: usize, + // The size of the inner content, before any growing. + desired_size: Vec2, + align2: Align2, + gap: f32, +} + +impl<'a> AllocatedAtomicLayout<'a> { + pub fn paint(self, ui: &mut Ui) -> AtomicLayoutResponse { + let Self { + sized_atomics: sized_items, + frame, + fallback_text_color, + response, + grow_count, + desired_size, + align2, + gap, + } = self; + + let inner_rect = response.rect - self.frame.total_margin(); - let inner_rect = rect - margin; ui.painter().add(frame.paint(inner_rect)); let width_to_fill = inner_rect.width(); - let extra_space = f32::max(width_to_fill - desired_width, 0.0); + let extra_space = f32::max(width_to_fill - desired_size.x, 0.0); let grow_width = f32::max(extra_space / grow_count as f32, 0.0); let aligned_rect = if grow_count > 0 { - align2.align_size_within_rect(Vec2::new(width_to_fill, content_size.y), inner_rect) + align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect) } else { - align2.align_size_within_rect(content_size, inner_rect) + align2.align_size_within_rect(desired_size, inner_rect) }; let mut cursor = aligned_rect.left(); + let mut response = AtomicLayoutResponse { + response, + custom_rects: HashMap::new(), + }; + for sized in sized_items { let size = sized.size; - let width = match sized.kind { - // TODO: check for atomic.grow here - SizedAtomicKind::Empty => grow_width, - _ => size.x, - }; + let growth = if sized.grow { grow_width } else { 0.0 }; - let frame = aligned_rect.with_min_x(cursor).with_max_x(cursor + width); + let frame = aligned_rect + .with_min_x(cursor) + .with_max_x(cursor + size.x + growth); cursor = frame.right() + gap; let align = Align2::CENTER_CENTER; @@ -343,10 +385,11 @@ pub struct Atomic<'a> { pub kind: AtomicKind<'a>, } -struct SizedAtomic<'a> { - size: Vec2, - preferred_size: Vec2, - kind: SizedAtomicKind<'a>, +pub struct SizedAtomic<'a> { + pub grow: bool, + pub size: Vec2, + pub preferred_size: Vec2, + pub kind: SizedAtomicKind<'a>, } impl<'a> Atomic<'a> { @@ -386,6 +429,7 @@ impl<'a> Atomic<'a> { SizedAtomic { size: self.size.unwrap_or_else(|| kind.size()), preferred_size: preferred, + grow: self.grow, kind, } } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index d37a2bb5caf..0873a21a7f6 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ Atomic, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, CornerRadius, Frame, - Image, IntoAtomics, NumExt, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, - WidgetInfo, WidgetText, WidgetType, + Image, IntoAtomics, NumExt, Response, Sense, SizedAtomicKind, Stroke, TextWrapMode, Ui, Vec2, + Widget, WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -224,9 +224,16 @@ impl<'a> Button<'a> { image_tint_follows_text_color, } = self; - let mut wl = AtomicLayout::new(atomics) + if !small { + min_size.y = min_size.y.at_least(ui.spacing().interact_size.y); + } + + let text = atomics.text(); + + let mut layout = AtomicLayout::new(atomics) .wrap_mode(wrap_mode.unwrap_or(ui.wrap_mode())) - .sense(sense); + .sense(sense) + .min_size(min_size); let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); @@ -237,56 +244,55 @@ impl<'a> Button<'a> { }; if small { button_padding.y = 0.0; - wl.atomics.iter_mut().for_each(|a| { + layout.atomics.iter_mut().for_each(|a| { if let AtomicKind::Text(text) = &mut a.kind { *text = std::mem::take(text).small(); } }); } - let id = ui.next_auto_id().with("egui::button"); - wl = wl.id(id); - let response = ui.ctx().read_response(id); - - let visuals = response.map_or(ui.style().visuals.widgets.inactive, |response| { - ui.style().interact_selectable(&response, selected) - }); - - if image_tint_follows_text_color { - wl.atomics.iter_mut().for_each(|a| { - a.kind = match std::mem::take(&mut a.kind) { - AtomicKind::Image(image) => AtomicKind::Image(image.tint(visuals.text_color())), - other => other, - } - }); - } + let mut prepared = layout + .frame(Frame::new().inner_margin(button_padding)) + .allocate(ui); + + let response = if ui.is_rect_visible(prepared.response.rect) { + let visuals = ui.style().interact_selectable(&prepared.response, selected); + + if image_tint_follows_text_color { + prepared.sized_atomics.iter_mut().for_each(|a| { + a.kind = match std::mem::take(&mut a.kind) { + SizedAtomicKind::Image(image, size) => { + SizedAtomicKind::Image(image.tint(visuals.text_color()), size) + } + other => other, + } + }); + } - wl = wl.fallback_text_color(visuals.text_color()); - - wl.frame = if has_frame { - let stroke = stroke.unwrap_or(visuals.bg_stroke); - let fill = fill.unwrap_or(visuals.weak_bg_fill); - wl.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 = visuals.text_color(); + + if has_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.paint(ui) } else { - Frame::new() + AtomicLayoutResponse { + response: prepared.response, + custom_rects: Default::default(), + } }; - if !small { - min_size.y = min_size.y.at_least(ui.spacing().interact_size.y); - } - wl = wl.min_size(min_size); - - let text = wl.atomics.text(); - - let response = wl.show(ui); - response.response.widget_info(|| { if let Some(text) = &text { WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text) diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 2579387b82f..fd3f2cf32d7 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,8 +1,9 @@ use crate::AtomicKind::Custom; use crate::{ - epaint, pos2, vec2, AtomicLayout, Atomics, Id, IntoAtomics, NumExt, Response, Sense, Shape, - TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + epaint, pos2, AtomicLayout, Atomics, Id, IntoAtomics, Response, Sense, Shape, Ui, Vec2, Widget, + WidgetInfo, WidgetType, }; +use emath::NumExt; // TODO(emilk): allow checkbox without a text label /// Boolean on/off control with text label. @@ -59,18 +60,27 @@ impl Widget for Checkbox<'_> { let spacing = &ui.spacing(); let icon_width = spacing.icon_width; + let mut min_size = Vec2::splat(spacing.interact_size.y); + min_size.y = min_size.y.at_least(icon_width); + + // 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); + icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::checkbox"); - atomics.add_front(Custom(rect_id, Vec2::splat(icon_width))); + atomics.add_front(Custom(rect_id, icon_size)); let text = atomics.text(); - let mut response = AtomicLayout::new(atomics).sense(Sense::click()).show(ui); + let mut prepared = AtomicLayout::new(atomics) + .sense(Sense::click()) + .min_size(min_size) + .allocate(ui); - if response.response.clicked() { + if prepared.response.clicked() { *checked = !*checked; - response.response.mark_changed(); + prepared.response.mark_changed(); } - response.response.widget_info(|| { + prepared.response.widget_info(|| { if indeterminate { WidgetInfo::labeled( WidgetType::Checkbox, @@ -87,40 +97,46 @@ impl Widget for Checkbox<'_> { } }); - if ui.is_rect_visible(response.response.rect) { + let response = if ui.is_rect_visible(prepared.response.rect) { // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful - let visuals = ui.style().interact(&response.response); - let rect = response.custom_rects.get(&rect_id).unwrap().clone(); - - let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); - ui.painter().add(epaint::RectShape::new( - big_icon_rect.expand(visuals.expansion), - visuals.corner_radius, - visuals.bg_fill, - visuals.bg_stroke, - epaint::StrokeKind::Inside, - )); + let visuals = *ui.style().interact(&prepared.response); + prepared.fallback_text_color = visuals.text_color(); + let response = prepared.paint(ui); - if indeterminate { - // Horizontal line: - ui.painter().add(Shape::hline( - small_icon_rect.x_range(), - small_icon_rect.center().y, - visuals.fg_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()), - ], - visuals.fg_stroke, + if let Some(rect) = response.custom_rects.get(&rect_id).copied() { + let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); + ui.painter().add(epaint::RectShape::new( + big_icon_rect.expand(visuals.expansion), + visuals.corner_radius, + visuals.bg_fill, + visuals.bg_stroke, + epaint::StrokeKind::Inside, )); + + if indeterminate { + // Horizontal line: + ui.painter().add(Shape::hline( + small_icon_rect.x_range(), + small_icon_rect.center().y, + visuals.fg_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()), + ], + visuals.fg_stroke, + )); + } } - } + response.response + } else { + prepared.response + }; - response.response + response } } From 8907d902bd229990f62af3240764b29ca5235506 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 14:20:23 +0200 Subject: [PATCH 23/77] Update radio button based on checkbox --- crates/egui/src/widgets/checkbox.rs | 6 +- crates/egui/src/widgets/radio_button.rs | 101 ++++++++++++------------ 2 files changed, 52 insertions(+), 55 deletions(-) diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index fd3f2cf32d7..3e7d7f28531 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -97,7 +97,7 @@ impl Widget for Checkbox<'_> { } }); - let response = if ui.is_rect_visible(prepared.response.rect) { + 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(); @@ -135,8 +135,6 @@ impl Widget for Checkbox<'_> { response.response } else { prepared.response - }; - - response + } } } diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index fabf565b57f..59a7ae4fa2a 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -1,7 +1,8 @@ use crate::{ - epaint, pos2, vec2, NumExt, Response, Sense, TextStyle, Ui, Vec2, Widget, WidgetInfo, - WidgetText, WidgetType, + epaint, AtomicKind, AtomicLayout, Atomics, Id, IntoAtomics, Response, Sense, Ui, Widget, + WidgetInfo, WidgetType, }; +use emath::{NumExt, Vec2}; /// One out of several alternatives, either selected or not. /// @@ -23,89 +24,87 @@ use crate::{ /// # }); /// ``` #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] -pub struct RadioButton { +pub struct RadioButton<'a> { checked: bool, - text: WidgetText, + atomics: Atomics<'a>, } -impl RadioButton { - pub fn new(checked: bool, text: impl Into) -> Self { +impl<'a> RadioButton<'a> { + pub fn new(checked: bool, text: impl IntoAtomics<'a>) -> Self { Self { checked, - text: text.into(), + atomics: text.into_atomics(), } } } -impl Widget for RadioButton { +impl<'a> Widget for RadioButton<'a> { fn ui(self, ui: &mut Ui) -> Response { - let Self { checked, text } = self; + let Self { + checked, + mut atomics, + } = self; let spacing = &ui.spacing(); let icon_width = spacing.icon_width; - let icon_spacing = spacing.icon_spacing; - - let (galley, mut desired_size) = if text.is_empty() { - (None, vec2(icon_width, 0.0)) - } else { - let total_extra = vec2(icon_width + icon_spacing, 0.0); - let wrap_width = ui.available_width() - total_extra.x; - let text = text.into_galley(ui, None, wrap_width, TextStyle::Button); + let mut min_size = Vec2::splat(spacing.interact_size.y); + min_size.y = min_size.y.at_least(icon_width); - let mut desired_size = total_extra + text.size(); - desired_size = desired_size.at_least(spacing.interact_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); + icon_size.y = icon_size.y.at_least(min_size.y); + let rect_id = Id::new("egui::radio_button"); + atomics.add_front(AtomicKind::Custom(rect_id, icon_size)); - (Some(text), desired_size) - }; + let text = atomics.text(); - desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); - desired_size.y = desired_size.y.max(icon_width); - let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); + let mut prepared = AtomicLayout::new(atomics) + .sense(Sense::click()) + .min_size(min_size) + .allocate(ui); - response.widget_info(|| { + prepared.response.widget_info(|| { WidgetInfo::selected( WidgetType::RadioButton, ui.is_enabled(), checked, - galley.as_ref().map_or("", |x| x.text()), + text.as_deref().unwrap_or(""), ) }); - if ui.is_rect_visible(rect) { + if ui.is_rect_visible(prepared.response.rect) { // let visuals = ui.style().interact_selectable(&response, checked); // too colorful - let visuals = ui.style().interact(&response); + let visuals = *ui.style().interact(&prepared.response); - let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); + prepared.fallback_text_color = visuals.text_color(); + let response = prepared.paint(ui); - let painter = ui.painter(); + if let Some(rect) = response.custom_rects.get(&rect_id).copied() { + let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); - painter.add(epaint::CircleShape { - center: big_icon_rect.center(), - radius: big_icon_rect.width() / 2.0 + visuals.expansion, - fill: visuals.bg_fill, - stroke: visuals.bg_stroke, - }); + let painter = ui.painter(); - if checked { painter.add(epaint::CircleShape { - center: small_icon_rect.center(), - radius: small_icon_rect.width() / 3.0, - fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill - // fill: ui.visuals().selection.stroke.color, // too much color - stroke: Default::default(), + center: big_icon_rect.center(), + radius: big_icon_rect.width() / 2.0 + visuals.expansion, + fill: visuals.bg_fill, + stroke: visuals.bg_stroke, }); - } - if let Some(galley) = galley { - let text_pos = pos2( - rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * galley.size().y, - ); - ui.painter().galley(text_pos, galley, visuals.text_color()); + if checked { + painter.add(epaint::CircleShape { + center: small_icon_rect.center(), + radius: small_icon_rect.width() / 3.0, + fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill + // fill: ui.visuals().selection.stroke.color, // too much color + stroke: Default::default(), + }); + } } + response.response + } else { + prepared.response } - - response } } From 84b14e29f42b1bdd018a98f666909c318cdd7400 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 14:27:12 +0200 Subject: [PATCH 24/77] Clippy fixes --- crates/egui/src/atomic_layout.rs | 33 ++++++++++++++----------- crates/egui/src/containers/menu.rs | 1 - crates/egui/src/widgets/button.rs | 14 +++++------ crates/egui/src/widgets/checkbox.rs | 6 ++--- crates/egui/src/widgets/radio_button.rs | 2 +- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs index b9e6698f974..4708660de5d 100644 --- a/crates/egui/src/atomic_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -20,8 +20,7 @@ impl SizedAtomicKind<'_> { pub fn size(&self) -> Vec2 { match self { SizedAtomicKind::Text(galley) => galley.size(), - SizedAtomicKind::Image(_, size) => *size, - SizedAtomicKind::Custom(_, size) => *size, + SizedAtomicKind::Image(_, size) | SizedAtomicKind::Custom(_, size) => *size, SizedAtomicKind::Empty => Vec2::ZERO, } } @@ -54,8 +53,13 @@ impl<'a> AtomicLayout<'a> { } } - pub fn add(mut self, atomic: impl Into>) -> Self { - self.atomics.add(atomic.into()); + pub fn push(mut self, atomic: impl Into>) -> Self { + self.atomics.push(atomic.into()); + self + } + + pub fn push_front(mut self, atomic: impl Into>) -> Self { + self.atomics.push_front(atomic.into()); self } @@ -180,7 +184,7 @@ impl<'a> AtomicLayout<'a> { }) .unwrap_or_else(default_font_height); - for ((idx, item)) in atomics.0.into_iter().enumerate() { + for (idx, item) in atomics.0.into_iter().enumerate() { if item.shrink { debug_assert!( shrink_item.is_none(), @@ -260,7 +264,7 @@ pub struct AllocatedAtomicLayout<'a> { } impl<'a> AllocatedAtomicLayout<'a> { - pub fn paint(self, ui: &mut Ui) -> AtomicLayoutResponse { + pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { let Self { sized_atomics: sized_items, frame, @@ -312,7 +316,7 @@ impl<'a> AllocatedAtomicLayout<'a> { SizedAtomicKind::Image(image, _) => { image.paint_at(ui, rect); } - SizedAtomicKind::Custom(id, size) => { + SizedAtomicKind::Custom(id, _) => { response.custom_rects.insert(id, rect); } SizedAtomicKind::Empty => {} @@ -408,10 +412,9 @@ impl<'a> Atomic<'a> { match &self.kind { AtomicKind::Text(text) => Some(text.font_height(fonts, style)), AtomicKind::Custom(_, size) => Some(size.y), - AtomicKind::Empty => None, // Since this method is used to calculate the best height for an image, we always return // None for images. - AtomicKind::Image(_) => None, + AtomicKind::Empty | AtomicKind::Image(_) => None, } }) } @@ -503,11 +506,11 @@ where pub struct Atomics<'a>(Vec>); impl<'a> Atomics<'a> { - pub fn add(&mut self, atomic: impl Into>) { + pub fn push(&mut self, atomic: impl Into>) { self.0.push(atomic.into()); } - pub fn add_front(&mut self, atomic: impl Into>) { + pub fn push_front(&mut self, atomic: impl Into>) { self.0.insert(0, atomic.into()); } @@ -540,7 +543,7 @@ where T: Into>, { fn collect(self, atomics: &mut Atomics<'a>) { - atomics.add(self); + atomics.push(self); } } @@ -558,7 +561,7 @@ pub trait IntoAtomics<'a> { } impl<'a> IntoAtomics<'a> for Atomics<'a> { - fn collect(self, atomics: &mut Atomics<'a>) { + fn collect(self, atomics: &mut Self) { atomics.0.extend(self.0); } } @@ -569,10 +572,10 @@ macro_rules! all_the_atomics { where $($T: IntoAtomics<'a>),* { - fn collect(self, atomics: &mut Atomics<'a>) { + fn collect(self, _atomics: &mut Atomics<'a>) { #[allow(non_snake_case)] let ($($T),*) = self; - $($T.collect(atomics);)* + $($T.collect(_atomics);)* } } }; diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 36c25b667c4..427dc8179a9 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -2,7 +2,6 @@ use crate::style::StyleModifier; use crate::{ Button, Color32, Context, Frame, Id, InnerResponse, IntoAtomics, Layout, Popup, PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, - WidgetText, }; use emath::{vec2, Align, RectAlign, Vec2}; use epaint::Stroke; diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 0873a21a7f6..b4421b850c8 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -70,10 +70,10 @@ impl<'a> Button<'a> { pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { - button.atomics.add(image); + button.atomics.push(image); } if let Some(text) = text { - button.atomics.add(text); + button.atomics.push(text); } button } @@ -189,16 +189,16 @@ impl<'a> Button<'a> { AtomicKind::Text(text) => AtomicKind::Text(text.weak()), other => other, }; - self.atomics.add(Atomic::grow()); - self.atomics.add(atomic); + self.atomics.push(Atomic::grow()); + self.atomics.push(atomic); self } /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl Into>) -> Self { - self.atomics.add(Atomic::grow()); - self.atomics.add(right_text.into()); + self.atomics.push(Atomic::grow()); + self.atomics.push(right_text.into()); self } @@ -306,7 +306,7 @@ impl<'a> Button<'a> { } impl Widget for Button<'_> { - fn ui(mut self, ui: &mut Ui) -> Response { + fn ui(self, ui: &mut Ui) -> Response { self.atomic_ui(ui).response // diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 3e7d7f28531..1e8aac16bac 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -67,7 +67,7 @@ impl Widget for Checkbox<'_> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::checkbox"); - atomics.add_front(Custom(rect_id, icon_size)); + atomics.push_front(Custom(rect_id, icon_size)); let text = atomics.text(); @@ -85,14 +85,14 @@ impl Widget for Checkbox<'_> { WidgetInfo::labeled( WidgetType::Checkbox, ui.is_enabled(), - text.clone().unwrap_or("".to_owned()), + text.as_deref().unwrap_or(""), ) } else { WidgetInfo::selected( WidgetType::Checkbox, ui.is_enabled(), *checked, - text.clone().unwrap_or("".to_owned()), + text.as_deref().unwrap_or(""), ) } }); diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index 59a7ae4fa2a..c18c6452087 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -55,7 +55,7 @@ impl<'a> Widget for RadioButton<'a> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::radio_button"); - atomics.add_front(AtomicKind::Custom(rect_id, icon_size)); + atomics.push_front(AtomicKind::Custom(rect_id, icon_size)); let text = atomics.text(); From 6b8ee5c22badc23b9369f9156562d25e0007ceb3 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 14:28:15 +0200 Subject: [PATCH 25/77] Revert hello world simple changes --- Cargo.lock | 2 -- examples/hello_world_simple/Cargo.toml | 2 -- examples/hello_world_simple/src/main.rs | 39 ------------------------- 3 files changed, 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d0497e912d..4411be0c6ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,9 +2065,7 @@ name = "hello_world_simple" version = "0.1.0" dependencies = [ "eframe", - "egui_extras", "env_logger", - "image", ] [[package]] diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index e8d08456bbc..db1d7906de5 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -20,5 +20,3 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -egui_extras = {workspace = true, features = ["image", "all_loaders"]} -image = {workspace = true, features = ["png"]} diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 1b57e5b5ebe..4fe49a89d68 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -2,10 +2,6 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; -use eframe::egui::{ - include_image, Button, Image, Key, KeyboardShortcut, ModifierNames, Modifiers, Popup, RichText, - Widget, -}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -21,7 +17,6 @@ fn main() -> eframe::Result { eframe::run_simple_native("My egui App", options, move |ctx, _frame| { egui::CentralPanel::default().show(ctx, |ui| { - egui_extras::install_image_loaders(ctx); ui.heading("My egui Application"); ui.horizontal(|ui| { let name_label = ui.label("Your name: "); @@ -33,40 +28,6 @@ fn main() -> eframe::Result { age += 1; } ui.label(format!("Hello '{name}', age {age}")); - - if Button::new("WL Button").ui(ui).clicked() { - age += 1; - }; - - let source = include_image!("../../../crates/eframe/data/icon.png"); - let response = Button::image_and_text(source.clone(), "Hello World").ui(ui); - - Button::new((Image::new(source).tint(egui::Color32::RED), "Tuple Button")) - .selected(true) - .ui(ui); - - ui.selectable_label(true, "Selectable Label"); - - Popup::menu(&response).show(|ui| { - Button::new("Print") - .right_text( - RichText::new( - KeyboardShortcut::new(Modifiers::COMMAND, Key::P) - .format(&ModifierNames::SYMBOLS, true), - ) - .weak(), - ) - .ui(ui); - Button::new("A very long button") - .right_text( - RichText::new( - KeyboardShortcut::new(Modifiers::COMMAND, Key::O) - .format(&ModifierNames::SYMBOLS, true), - ) - .weak(), - ) - .ui(ui); - }); }); }) } From a9c466beadfb10364bb5a4e9c6c31e81cb953713 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 16:40:09 +0200 Subject: [PATCH 26/77] Add some tests for AtomicLayout --- .../layout/atomics_1 grow 2 grow 3.png | 3 +++ .../snapshots/layout/atomics_Hello World!.png | 3 +++ .../snapshots/layout/atomics_With Image.png | 3 +++ tests/egui_tests/tests/test_widgets.rs | 19 ++++++++++++++++--- 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/layout/atomics_1 grow 2 grow 3.png create mode 100644 tests/egui_tests/tests/snapshots/layout/atomics_Hello World!.png create mode 100644 tests/egui_tests/tests/snapshots/layout/atomics_With Image.png diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_1 grow 2 grow 3.png b/tests/egui_tests/tests/snapshots/layout/atomics_1 grow 2 grow 3.png new file mode 100644 index 00000000000..9686559c2f1 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/atomics_1 grow 2 grow 3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5b89974517f6164adda8519495dd017f3eb51531e3d5ba51706509c297ecf6e +size 445154 diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_Hello World!.png b/tests/egui_tests/tests/snapshots/layout/atomics_Hello World!.png new file mode 100644 index 00000000000..1eb0a83480f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/atomics_Hello World!.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a09e926d25e2b6f63dc6df00ab5e5b76745aae1f288231f1a602421b2bbb53b +size 384721 diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_With Image.png b/tests/egui_tests/tests/snapshots/layout/atomics_With Image.png new file mode 100644 index 00000000000..882cfec100c --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/atomics_With Image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43b7e82dbd38dfa2b48c5bae80466e2bbf8d2a60d84c6c85f0f2b95fc64458e +size 395339 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 264b0052f65..6b24a26a1fa 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,8 +1,8 @@ use egui::load::SizedTexture; use egui::{ - include_image, Align, Button, Color32, ColorImage, Direction, DragValue, Event, Grid, Layout, - PointerButton, Pos2, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, - TextureOptions, Ui, UiBuilder, Vec2, Widget, + include_image, Align, AtomicExt, AtomicLayout, Button, Color32, ColorImage, Direction, + DragValue, Event, Grid, Image, IntoAtomics, Layout, PointerButton, Pos2, Response, Slider, + Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget, }; use egui_kittest::kittest::{by, Node, Queryable}; use egui_kittest::{Harness, SnapshotResult, SnapshotResults}; @@ -92,6 +92,19 @@ fn widget_tests() { }, &mut results, ); + + let source = include_image!("../../../crates/eframe/data/icon.png"); + let interesting_atomics = vec![ + ("Hello World!").into_atomics(), + (Image::new(source.clone()), "With Image").into_atomics(), + ("1", "grow".a_grow(true), "2", "grow".a_grow(true), "3").into_atomics(), + ]; + + for atomics in interesting_atomics { + test_widget_layout(&format!("atomics_{}", atomics.text().unwrap()), |ui| { + AtomicLayout::new(atomics.clone()).ui(ui) + }); + } } fn test_widget(name: &str, mut w: impl FnMut(&mut Ui) -> Response, results: &mut SnapshotResults) { From f6430456114f3b0d47f958e4d8e5730ed0c9bec4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Apr 2025 18:02:31 +0200 Subject: [PATCH 27/77] Update snapshots --- crates/egui/src/atomic_layout.rs | 16 ++++++++++++++++ crates/egui/src/widgets/button.rs | 7 +------ .../tests/snapshots/imageviewer.png | 4 ++-- .../tests/snapshots/demos/Scrolling.png | 2 +- .../tests/snapshots/demos/Sliders.png | 4 ++-- .../tests/snapshots/demos/Table.png | 4 ++-- crates/egui_kittest/tests/menu.rs | 12 +++++++----- .../tests/snapshots/layout/button_image.png | 4 ++-- .../tests/snapshots/layout/checkbox_checked.png | 4 ++-- .../visuals/button_image_shortcut_selected.png | 4 ++-- 10 files changed, 37 insertions(+), 24 deletions(-) diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs index 4708660de5d..8694bcccde4 100644 --- a/crates/egui/src/atomic_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -5,6 +5,7 @@ use ahash::{HashMap, HashMapExt}; use emath::{Align2, NumExt, Rect, Vec2}; use epaint::text::TextWrapMode; use epaint::{Color32, Fonts, Galley}; +use std::fmt::Formatter; use std::sync::Arc; #[derive(Clone, Default)] @@ -341,6 +342,17 @@ pub enum AtomicKind<'a> { Custom(Id, Vec2), } +impl std::fmt::Debug for AtomicKind<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AtomicKind::Empty => write!(f, "AtomicKind::Empty"), + AtomicKind::Text(text) => write!(f, "AtomicKind::Text({})", text.text()), + AtomicKind::Image(image) => write!(f, "AtomicKind::Image({image:?})"), + AtomicKind::Custom(id, size) => write!(f, "AtomicKind::Custom({id:?}, {size:?})"), + } + } +} + impl<'a> AtomicKind<'a> { pub fn text(text: impl Into) -> Self { AtomicKind::Text(text.into()) @@ -382,6 +394,7 @@ impl<'a> AtomicKind<'a> { } } +#[derive(Clone, Debug)] pub struct Atomic<'a> { pub size: Option, pub grow: bool, @@ -503,6 +516,7 @@ where } } +#[derive(Clone, Debug, Default)] pub struct Atomics<'a>(Vec>); impl<'a> Atomics<'a> { @@ -522,6 +536,8 @@ impl<'a> Atomics<'a> { self.0.iter_mut() } + // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. + // in a submenu button there is a right text '⏵' which is now passed to the screen reader. pub fn text(&self) -> Option { let mut string: Option = None; for atomic in &self.0 { diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index b4421b850c8..e6cd525933d 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -230,7 +230,7 @@ impl<'a> Button<'a> { let text = atomics.text(); - let mut layout = AtomicLayout::new(atomics) + let layout = AtomicLayout::new(atomics) .wrap_mode(wrap_mode.unwrap_or(ui.wrap_mode())) .sense(sense) .min_size(min_size); @@ -244,11 +244,6 @@ impl<'a> Button<'a> { }; if small { button_padding.y = 0.0; - layout.atomics.iter_mut().for_each(|a| { - if let AtomicKind::Text(text) = &mut a.kind { - *text = std::mem::take(text).small(); - } - }); } let mut prepared = layout diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 57c88b50a04..d5bde1f987e 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7572ec2dad9038c24beb9949e4c05155cd0f5479153de6647c38911ec5c67a0 -size 100779 +oid sha256:e2fae780123389ca0affa762a5c031b84abcdd31c7a830d485c907c8c370b006 +size 100780 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 17e76840e3e..49b223e7d24 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b236fe02f6cd52041359cf4b1a00e9812b95560353ce5df4fa6cb20fdbb45307 +oid sha256:4a347875ef98ebbd606774e03baffdb317cb0246882db116fee1aa7685efbb88 size 179653 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 09f8eaa6c6b..92e94b78f93 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1579351658875af48ad9aafeb08d928d83f1bda42bf092fdcceecd0aa6730e26 -size 115313 +oid sha256:f0e3eeca8abb4fba632cef4621d478fb66af1a0f13e099dda9a79420cc2b6301 +size 115320 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index c11788f403b..8a269fd4ed6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9446da28768cae0b489e0f6243410a8b3acf0ca2a0b70690d65d2a6221bc25b9 -size 30517 +oid sha256:9d27ed8292a2612b337f663bff73cd009a82f806c61f0863bf70a53fd4c281ff +size 75074 diff --git a/crates/egui_kittest/tests/menu.rs b/crates/egui_kittest/tests/menu.rs index 4c6d6fd5c91..28a749603d2 100644 --- a/crates/egui_kittest/tests/menu.rs +++ b/crates/egui_kittest/tests/menu.rs @@ -99,7 +99,7 @@ fn menu_close_on_click_outside() { harness.run(); harness - .get_by_label("Submenu C (CloseOnClickOutside)") + .get_by_label_contains("Submenu C (CloseOnClickOutside)") .hover(); harness.run(); @@ -133,7 +133,7 @@ fn menu_close_on_click() { harness.get_by_label("Menu A").simulate_click(); harness.run(); - harness.get_by_label("Submenu B with icon").hover(); + harness.get_by_label_contains("Submenu B with icon").hover(); harness.run(); // Clicking the button should close the menu (even if ui.close() is not called by the button) @@ -154,7 +154,9 @@ fn clicking_submenu_button_should_never_close_menu() { harness.run(); // Clicking the submenu button should not close the menu - harness.get_by_label("Submenu B with icon").simulate_click(); + harness + .get_by_label_contains("Submenu B with icon") + .simulate_click(); harness.run(); harness.get_by_label("Button in Submenu B").simulate_click(); @@ -177,12 +179,12 @@ fn menu_snapshots() { results.add(harness.try_snapshot("menu/opened")); harness - .get_by_label("Submenu C (CloseOnClickOutside)") + .get_by_label_contains("Submenu C (CloseOnClickOutside)") .hover(); harness.run(); results.add(harness.try_snapshot("menu/submenu")); - harness.get_by_label("Submenu D").hover(); + harness.get_by_label_contains("Submenu D").hover(); harness.run(); results.add(harness.try_snapshot("menu/subsubmenu")); } diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png index 737f0670cd7..fb6ff3b34ff 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:975c279d6da2a2cb000df72bf5d9f3bdd200bb20adc00e29e8fd9ed4d2c6f6b1 -size 340923 +oid sha256:01309596ac9eb90b2dfc00074cfd39d26e3f6d1f83299f227cb4bbea9ccd3b66 +size 339917 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png index 66ae8115fc6..9c6fb4c076b 100644 --- a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png +++ b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aaf9b032037d0708894e568cc8e256b32be9cfb586eaffdc6167143b85562b37 -size 415016 +oid sha256:1d842f88b6a94f19aa59bdae9dbbf42f4662aaead1b8f73ac0194f183112e1b8 +size 415066 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png index 3ff34c6be99..114baa35d40 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09c5904877c8895d3ad41b7082019ef87db40c6a91ad47401bb9b8ac79a62bdc -size 12914 +oid sha256:f9151f1c9d8a769ac2143a684cabf5d9ed1e453141fff555da245092003f1df1 +size 13563 From f0176d4a5ccfb8ead1e5194789ebad73e9de2637 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 10:37:09 +0200 Subject: [PATCH 28/77] All the docs! --- crates/egui/src/atomic_layout.rs | 180 +++++++++++++++++++++++------- crates/egui/src/widgets/button.rs | 1 + 2 files changed, 143 insertions(+), 38 deletions(-) diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs index 8694bcccde4..4eac9541f4c 100644 --- a/crates/egui/src/atomic_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -8,25 +8,25 @@ use epaint::{Color32, Fonts, Galley}; use std::fmt::Formatter; use std::sync::Arc; -#[derive(Clone, Default)] -pub enum SizedAtomicKind<'a> { - #[default] - Empty, - Text(Arc), - Image(Image<'a>, Vec2), - Custom(Id, Vec2), -} - -impl SizedAtomicKind<'_> { - pub fn size(&self) -> Vec2 { - match self { - SizedAtomicKind::Text(galley) => galley.size(), - SizedAtomicKind::Image(_, size) | SizedAtomicKind::Custom(_, size) => *size, - SizedAtomicKind::Empty => Vec2::ZERO, - } - } -} - +/// Intra-widget layout utility. +/// +/// Used to lay out and paint [`Atomic`]s. +/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`]. +/// You can use it to make your own widgets. +/// +/// Painting the atomics can be split in two phases: +/// - [`AtomicLayout::allocate`] +/// - calculates sizes +/// - converts texts to [`Galley`]s +/// - allocates a [`Response`] +/// - returns a [`AllocatedAtomicLayout`] +/// - [`AllocatedAtomicLayout::paint`] +/// - paints the [`Frame`] +/// - calculates individual [`Atomic`] positions +/// - paints each single atomic +/// +/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the +/// [`AllocatedAtomicLayout`] for interaction styling. pub struct AtomicLayout<'a> { id: Option, pub atomics: Atomics<'a>, @@ -39,6 +39,12 @@ pub struct AtomicLayout<'a> { align2: Option, } +impl Default for AtomicLayout<'_> { + fn default() -> Self { + Self::new(()) + } +} + impl<'a> AtomicLayout<'a> { pub fn new(atomics: impl IntoAtomics<'a>) -> Self { Self { @@ -54,61 +60,86 @@ impl<'a> AtomicLayout<'a> { } } + /// Insert a new [`Atomic`] at the end of the list (left side). pub fn push(mut self, atomic: impl Into>) -> Self { self.atomics.push(atomic.into()); self } + /// Insert a new [`Atomic`] at the beginning of the list (right side). pub fn push_front(mut self, atomic: impl Into>) -> Self { self.atomics.push_front(atomic.into()); self } + /// Set the gap between atomics. + /// /// Default: `Spacing::icon_spacing` pub fn gap(mut self, gap: f32) -> Self { self.gap = Some(gap); self } + /// Set the [`Frame`]. pub fn frame(mut self, frame: Frame) -> Self { self.frame = frame; self } + /// Set the [`Sense`] used when allocating the [`Response`]. pub fn sense(mut self, sense: Sense) -> Self { self.sense = sense; self } + /// Set the fallback (default) text color. + /// + /// Default: [`crate::Visuals::text_color`] pub fn fallback_text_color(mut self, color: Color32) -> Self { self.fallback_text_color = Some(color); self } + /// Set the minimum size of the Widget. pub fn min_size(mut self, size: Vec2) -> Self { self.min_size = size; self } + /// Set the [`Id`] used to allocate a [`Response`]. pub fn id(mut self, id: Id) -> Self { self.id = Some(id); self } + /// Set the [`TextWrapMode`] for the [`Atomic`] marked as `shrink`. + /// + /// Only a single [`Atomic`] may shrink. If this (or `ui.wrap_mode()`) is not + /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (right-most) + /// [`AtomicKind::Text`] will be set to shrink. pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { self.wrap_mode = Some(wrap_mode); self } + /// Set the [`Align2`]. + /// + /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See + /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) + /// for info on how the [`Layout`] affects the alignment. pub fn align2(mut self, align2: Align2) -> Self { self.align2 = Some(align2); self } + /// [`AtomicLayout::allocate`] and [`AllocatedAtomicLayout::paint`] in one go. pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { self.allocate(ui).paint(ui) } + /// Calculate sizes, create [`Galley`]s and allocate a [`Response`]. + /// + /// Use the returned [`AllocatedAtomicLayout`] for painting. pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomicLayout<'a> { let Self { id, @@ -252,6 +283,8 @@ impl<'a> AtomicLayout<'a> { } } +/// Instructions for painting an [`AtomicLayout`]. +#[derive(Clone, Debug)] pub struct AllocatedAtomicLayout<'a> { pub sized_atomics: Vec>, pub frame: Frame, @@ -265,6 +298,7 @@ pub struct AllocatedAtomicLayout<'a> { } impl<'a> AllocatedAtomicLayout<'a> { + /// Paint the [`Frame`] and individual [`Atomic`]s. pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { let Self { sized_atomics: sized_items, @@ -328,17 +362,45 @@ impl<'a> AllocatedAtomicLayout<'a> { } } +/// Response from a [`AtomicLayout::show`] or [`AllocatedAtomicLayout::paint`]. +/// +/// Use the `custom_rects` together with [`AtomicKind::Custom`] to add child widgets to a widget. +/// +/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible. +#[derive(Clone, Debug)] pub struct AtomicLayoutResponse { pub response: Response, pub custom_rects: HashMap, } +/// The different kinds of [`Atomic`]s. #[derive(Clone, Default)] pub enum AtomicKind<'a> { + /// Empty, that can be used with [`Atomic::a_grow`] to reserve space. #[default] Empty, Text(WidgetText), Image(Image<'a>), + + /// For custom rendering. + /// + /// You can get the [`Rect`] with the [`Id`] from [`AtomicLayoutResponse`] and use a + /// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content. + /// + /// Example: + /// ``` + /// # use egui::{AtomicKind, Button, Id, __run_test_ui}; + /// # use emath::Vec2; + /// # __run_test_ui(|ui| { + /// let id = Id::new("my_button"); + /// let response = Button::new(("Hi!", AtomicKind::Custom(id, Vec2::splat(18.0)))).atomic_ui(ui); + /// + /// let rect = response.custom_rects.get(&id); + /// if let Some(rect) = rect { + /// ui.put(*rect, Button::new("⏵")); + /// } + /// # }); + /// ``` Custom(Id, Vec2), } @@ -366,7 +428,10 @@ impl<'a> AtomicKind<'a> { AtomicKind::Custom(id, size) } - /// First returned argument is the preferred size. + /// Turn this [`AtomicKind`] into a [`SizedAtomicKind`]. + /// + /// This converts [`WidgetText`] into [`Galley`] and tries to load and size [`Image`]. + /// The first returned argument is the preferred size. pub fn into_sized( self, ui: &Ui, @@ -394,6 +459,37 @@ impl<'a> AtomicKind<'a> { } } +/// A sized [`AtomicKind`]. +#[derive(Clone, Default, Debug)] +pub enum SizedAtomicKind<'a> { + #[default] + Empty, + Text(Arc), + Image(Image<'a>, Vec2), + Custom(Id, Vec2), +} + +impl SizedAtomicKind<'_> { + /// Get the calculated size. + pub fn size(&self) -> Vec2 { + match self { + SizedAtomicKind::Text(galley) => galley.size(), + SizedAtomicKind::Image(_, size) | SizedAtomicKind::Custom(_, size) => *size, + SizedAtomicKind::Empty => Vec2::ZERO, + } + } +} + +/// A low-level ui building block. +/// +/// Implements [`From`] for [`String`], [`str`], [`Image`] and much more for convenience. +/// You can directly call the `a_*` methods on anything that implements `Into`. +/// ``` +/// # use egui::{Image, emath::Vec2}; +/// use egui::AtomicExt; +/// let string_atomic = "Hello".a_grow(true); +/// let image_atomic = Image::new("some_image_url").a_size(Vec2::splat(20.0)); +/// ``` #[derive(Clone, Debug)] pub struct Atomic<'a> { pub size: Option, @@ -402,6 +498,8 @@ pub struct Atomic<'a> { pub kind: AtomicKind<'a>, } +/// A [`Atomic`] which has been sized. +#[derive(Clone, Debug)] pub struct SizedAtomic<'a> { pub grow: bool, pub size: Vec2, @@ -420,6 +518,8 @@ impl<'a> Atomic<'a> { } } + /// Heuristic to find the best height for an image. + /// Basically returns the height if this is not an [`Image`]. fn get_min_height_for_image(&self, fonts: &Fonts, style: &Style) -> Option { self.size.map(|s| s.y).or_else(|| { match &self.kind { @@ -432,7 +532,8 @@ impl<'a> Atomic<'a> { }) } - fn into_sized( + /// Turn this into a [`SizedAtomic`]. + pub fn into_sized( self, ui: &Ui, available_size: Vec2, @@ -451,9 +552,17 @@ impl<'a> Atomic<'a> { } } +/// A trait for conveniently building [`Atomic`]s. pub trait AtomicExt<'a> { + /// Set the atomic to a fixed size. fn a_size(self, size: Vec2) -> Atomic<'a>; + + /// Grow this atomic to the available space. fn a_grow(self, grow: bool) -> Atomic<'a>; + + /// Shrink this atomic if there isn't enough space. + /// + /// NOTE: Only a single [`Atomic`] may shrink for each widget. fn a_shrink(self, shrink: bool) -> Atomic<'a>; } @@ -501,12 +610,6 @@ impl<'a> From> for AtomicKind<'a> { } } -// impl<'a> From<&str> for AtomicKind<'a> { -// fn from(value: &str) -> Self { -// AtomicKind::Text(value.into()) -// } -// } - impl<'a, T> From for AtomicKind<'a> where T: Into, @@ -516,6 +619,7 @@ where } } +/// A list of [`Atomic`]s. #[derive(Clone, Debug, Default)] pub struct Atomics<'a>(Vec>); @@ -536,6 +640,7 @@ impl<'a> Atomics<'a> { self.0.iter_mut() } + /// Concatenate and return the text contents. // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. // in a submenu button there is a right text '⏵' which is now passed to the screen reader. pub fn text(&self) -> Option { @@ -554,6 +659,16 @@ impl<'a> Atomics<'a> { } } +/// Helper trait to convert a tuple of atomics into [`Atomics`]. +/// +/// ``` +/// use egui::{Atomics, Image, IntoAtomics, RichText}; +/// let atomics: Atomics = ( +/// "Some text", +/// RichText::new("Some RichText"), +/// Image::new("some_image_url"), +/// ).into_atomics(); +/// ``` impl<'a, T> IntoAtomics<'a> for T where T: Into>, @@ -604,17 +719,6 @@ all_the_atomics!(T0, T1, T2, T3); all_the_atomics!(T0, T1, T2, T3, T4); all_the_atomics!(T0, T1, T2, T3, T4, T5); -// trait AtomicWidget { -// fn show(&self, ui: &mut Ui) -> WidgetLayout; -// } - -// TODO: This conflicts with the FnOnce Widget impl, is there some way around that? -// impl Widget for T where T: AtomicWidget { -// fn ui(self, ui: &mut Ui) -> Response { -// ui.add(self) -// } -// } - impl Widget for AtomicLayout<'_> { fn ui(self, ui: &mut Ui) -> Response { self.show(ui).response diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index e6cd525933d..92ca6c33186 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -209,6 +209,7 @@ impl<'a> Button<'a> { self } + /// Show the button and return a [`AtomicLayoutResponse`] for painting custom contents. pub fn atomic_ui(self, ui: &mut Ui) -> AtomicLayoutResponse { let Button { atomics, From 5b84d90cf9793045987aa8b8af42fc4f3e8ba865 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 10:42:55 +0200 Subject: [PATCH 29/77] Fixes --- crates/egui/src/atomic_layout.rs | 12 +++++++++++- crates/egui/src/widgets/button.rs | 1 - tests/egui_tests/tests/test_widgets.rs | 7 ++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs index 4eac9541f4c..4047133a791 100644 --- a/crates/egui/src/atomic_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -61,12 +61,14 @@ impl<'a> AtomicLayout<'a> { } /// Insert a new [`Atomic`] at the end of the list (left side). + #[inline] pub fn push(mut self, atomic: impl Into>) -> Self { self.atomics.push(atomic.into()); self } /// Insert a new [`Atomic`] at the beginning of the list (right side). + #[inline] pub fn push_front(mut self, atomic: impl Into>) -> Self { self.atomics.push_front(atomic.into()); self @@ -75,18 +77,21 @@ impl<'a> AtomicLayout<'a> { /// Set the gap between atomics. /// /// Default: `Spacing::icon_spacing` + #[inline] pub fn gap(mut self, gap: f32) -> Self { self.gap = Some(gap); self } /// Set the [`Frame`]. + #[inline] pub fn frame(mut self, frame: Frame) -> Self { self.frame = frame; self } /// Set the [`Sense`] used when allocating the [`Response`]. + #[inline] pub fn sense(mut self, sense: Sense) -> Self { self.sense = sense; self @@ -95,18 +100,21 @@ impl<'a> AtomicLayout<'a> { /// Set the fallback (default) text color. /// /// Default: [`crate::Visuals::text_color`] + #[inline] pub fn fallback_text_color(mut self, color: Color32) -> Self { self.fallback_text_color = Some(color); self } /// Set the minimum size of the Widget. + #[inline] pub fn min_size(mut self, size: Vec2) -> Self { self.min_size = size; self } /// Set the [`Id`] used to allocate a [`Response`]. + #[inline] pub fn id(mut self, id: Id) -> Self { self.id = Some(id); self @@ -117,6 +125,7 @@ impl<'a> AtomicLayout<'a> { /// Only a single [`Atomic`] may shrink. If this (or `ui.wrap_mode()`) is not /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (right-most) /// [`AtomicKind::Text`] will be set to shrink. + #[inline] pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { self.wrap_mode = Some(wrap_mode); self @@ -127,6 +136,7 @@ impl<'a> AtomicLayout<'a> { /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) /// for info on how the [`Layout`] affects the alignment. + #[inline] pub fn align2(mut self, align2: Align2) -> Self { self.align2 = Some(align2); self @@ -443,7 +453,7 @@ impl<'a> AtomicKind<'a> { AtomicKind::Text(text) => { let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); ( - galley.size(), // TODO + galley.size(), // TODO(lucasmerlin): calculate the preferred size SizedAtomicKind::Text(galley), ) } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 92ca6c33186..11a4d0efd24 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -26,7 +26,6 @@ use crate::{ pub struct Button<'a> { atomics: Atomics<'a>, wrap_mode: Option, - /// None means default for interact fill: Option, stroke: Option, sense: Sense, diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 6b24a26a1fa..e6dfd2486d7 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -101,9 +101,10 @@ fn widget_tests() { ]; for atomics in interesting_atomics { - test_widget_layout(&format!("atomics_{}", atomics.text().unwrap()), |ui| { - AtomicLayout::new(atomics.clone()).ui(ui) - }); + results.add(test_widget_layout( + &format!("atomics_{}", atomics.text().unwrap()), + |ui| AtomicLayout::new(atomics.clone()).ui(ui), + )); } } From d02fc1952fd7146b1dd3540412d3f2743247536c Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 10:46:10 +0200 Subject: [PATCH 30/77] Remove commented-out code --- crates/egui/src/widgets/button.rs | 167 ------------------------------ 1 file changed, 167 deletions(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 11a4d0efd24..7939942f81e 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -303,172 +303,5 @@ impl<'a> Button<'a> { impl Widget for Button<'_> { fn ui(self, ui: &mut Ui) -> Response { self.atomic_ui(ui).response - - // - // let space_available_for_image = if let Some(text) = &text { - // let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style())); - // Vec2::splat(font_height) // Reasonable? - // } else { - // ui.available_size() - 2.0 * button_padding - // }; - // - // let image_size = if let Some(image) = &image { - // image - // .load_and_calc_size(ui, space_available_for_image) - // .unwrap_or(space_available_for_image) - // } else { - // Vec2::ZERO - // }; - // - // let gap_before_right_text = ui.spacing().item_spacing.x; - // - // let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x; - // if image.is_some() { - // text_wrap_width -= image_size.x + ui.spacing().icon_spacing; - // } - // - // // Note: we don't wrap the right text - // let right_galley = (!right_text.is_empty()).then(|| { - // right_text.into_galley( - // ui, - // Some(TextWrapMode::Extend), - // f32::INFINITY, - // TextStyle::Button, - // ) - // }); - // - // if let Some(right_galley) = &right_galley { - // // Leave space for the right text: - // text_wrap_width -= gap_before_right_text + right_galley.size().x; - // } - // - // let galley = - // text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button)); - // - // let mut desired_size = Vec2::ZERO; - // if image.is_some() { - // desired_size.x += image_size.x; - // desired_size.y = desired_size.y.max(image_size.y); - // } - // if image.is_some() && galley.is_some() { - // desired_size.x += ui.spacing().icon_spacing; - // } - // if let Some(galley) = &galley { - // desired_size.x += galley.size().x; - // desired_size.y = desired_size.y.max(galley.size().y); - // } - // if let Some(right_galley) = &right_galley { - // desired_size.x += gap_before_right_text + right_galley.size().x; - // desired_size.y = desired_size.y.max(right_galley.size().y); - // } - // desired_size += 2.0 * button_padding; - // if !small { - // desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); - // } - // desired_size = desired_size.at_least(min_size); - // - // let (rect, mut response) = ui.allocate_at_least(desired_size, sense); - // - // if ui.is_rect_visible(rect) { - // let visuals = ui.style().interact(&response); - // - // let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected { - // let selection = ui.visuals().selection; - // ( - // Vec2::ZERO, - // CornerRadius::ZERO, - // selection.bg_fill, - // selection.stroke, - // ) - // } else if frame { - // let expansion = Vec2::splat(visuals.expansion); - // ( - // expansion, - // visuals.corner_radius, - // visuals.weak_bg_fill, - // visuals.bg_stroke, - // ) - // } else { - // Default::default() - // }; - // let frame_cr = corner_radius.unwrap_or(frame_cr); - // let frame_fill = fill.unwrap_or(frame_fill); - // let frame_stroke = stroke.unwrap_or(frame_stroke); - // ui.painter().rect( - // rect.expand2(frame_expansion), - // frame_cr, - // frame_fill, - // frame_stroke, - // epaint::StrokeKind::Inside, - // ); - // - // let mut cursor_x = rect.min.x + button_padding.x; - // - // if let Some(image) = &image { - // let mut image_pos = ui - // .layout() - // .align_size_within_rect(image_size, rect.shrink2(button_padding)) - // .min; - // if galley.is_some() || right_galley.is_some() { - // image_pos.x = cursor_x; - // } - // let image_rect = Rect::from_min_size(image_pos, image_size); - // cursor_x += image_size.x; - // let tlr = image.load_for_size(ui.ctx(), image_size); - // let mut image_options = image.image_options().clone(); - // if image_tint_follows_text_color { - // image_options.tint = image_options.tint * visuals.text_color(); - // } - // widgets::image::paint_texture_load_result( - // ui, - // &tlr, - // image_rect, - // image.show_loading_spinner, - // &image_options, - // None, - // ); - // response = widgets::image::texture_load_result_response( - // &image.source(ui.ctx()), - // &tlr, - // response, - // ); - // } - // - // if image.is_some() && galley.is_some() { - // cursor_x += ui.spacing().icon_spacing; - // } - // - // if let Some(galley) = galley { - // let mut text_pos = ui - // .layout() - // .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) - // .min; - // if image.is_some() || right_galley.is_some() { - // text_pos.x = cursor_x; - // } - // ui.painter().galley(text_pos, galley, visuals.text_color()); - // } - // - // if let Some(right_galley) = right_galley { - // // Always align to the right - // let layout = if ui.layout().is_horizontal() { - // ui.layout().with_main_align(Align::Max) - // } else { - // ui.layout().with_cross_align(Align::Max) - // }; - // let right_text_pos = layout - // .align_size_within_rect(right_galley.size(), rect.shrink2(button_padding)) - // .min; - // - // ui.painter() - // .galley(right_text_pos, right_galley, visuals.text_color()); - // } - // } - - // if let Some(cursor) = ui.visuals().interact_cursor { - // if response.hovered() { - // ui.ctx().set_cursor_icon(cursor); - // } - // } } } From 285c0654f1f97af56a0502508b001c33435aad93 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 11:12:55 +0200 Subject: [PATCH 31/77] Fix grow count being off --- crates/egui/src/atomic_layout.rs | 4 ++++ .../snapshots/layout/atomics_1 grow 2 grow 3.png | 3 --- ...{atomics_With Image.png => atomics_image.png} | 0 ...mics_Hello World!.png => atomics_minimal.png} | 0 .../snapshots/layout/atomics_multi_grow.png | 3 +++ tests/egui_tests/tests/test_widgets.rs | 16 +++++++++++----- 6 files changed, 18 insertions(+), 8 deletions(-) delete mode 100644 tests/egui_tests/tests/snapshots/layout/atomics_1 grow 2 grow 3.png rename tests/egui_tests/tests/snapshots/layout/{atomics_With Image.png => atomics_image.png} (100%) rename tests/egui_tests/tests/snapshots/layout/{atomics_Hello World!.png => atomics_minimal.png} (100%) create mode 100644 tests/egui_tests/tests/snapshots/layout/atomics_multi_grow.png diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs index 4047133a791..26bbc9ef83f 100644 --- a/crates/egui/src/atomic_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -258,6 +258,10 @@ impl<'a> AtomicLayout<'a> { available_inner_size.x - desired_width, available_inner_size.y, ); + if item.grow { + grow_count += 1; + } + let sized = item.into_sized(ui, shrunk_size, max_font_size, Some(wrap_mode)); let size = sized.size; diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_1 grow 2 grow 3.png b/tests/egui_tests/tests/snapshots/layout/atomics_1 grow 2 grow 3.png deleted file mode 100644 index 9686559c2f1..00000000000 --- a/tests/egui_tests/tests/snapshots/layout/atomics_1 grow 2 grow 3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b5b89974517f6164adda8519495dd017f3eb51531e3d5ba51706509c297ecf6e -size 445154 diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_With Image.png b/tests/egui_tests/tests/snapshots/layout/atomics_image.png similarity index 100% rename from tests/egui_tests/tests/snapshots/layout/atomics_With Image.png rename to tests/egui_tests/tests/snapshots/layout/atomics_image.png diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_Hello World!.png b/tests/egui_tests/tests/snapshots/layout/atomics_minimal.png similarity index 100% rename from tests/egui_tests/tests/snapshots/layout/atomics_Hello World!.png rename to tests/egui_tests/tests/snapshots/layout/atomics_minimal.png diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_multi_grow.png b/tests/egui_tests/tests/snapshots/layout/atomics_multi_grow.png new file mode 100644 index 00000000000..87211765f1c --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/atomics_multi_grow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14a1dc826aeced98cab1413f915dcbbe904b5b1eadfc4d811232bc8ccbe7f550 +size 299556 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index e6dfd2486d7..9eb53bcd08f 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -95,15 +95,21 @@ fn widget_tests() { let source = include_image!("../../../crates/eframe/data/icon.png"); let interesting_atomics = vec![ - ("Hello World!").into_atomics(), - (Image::new(source.clone()), "With Image").into_atomics(), - ("1", "grow".a_grow(true), "2", "grow".a_grow(true), "3").into_atomics(), + ("minimal", ("Hello World!").into_atomics()), + ( + "image", + (Image::new(source.clone()), "With Image").into_atomics(), + ), + ( + "multi_grow", + ("g".a_grow(true), "2", "g".a_grow(true), "4").into_atomics(), + ), ]; for atomics in interesting_atomics { results.add(test_widget_layout( - &format!("atomics_{}", atomics.text().unwrap()), - |ui| AtomicLayout::new(atomics.clone()).ui(ui), + &format!("atomics_{}", atomics.0), + |ui| AtomicLayout::new(atomics.1.clone()).ui(ui), )); } } From 31b29748461464e3717bd9b14e1b3c86d6a532bb Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 11:30:16 +0200 Subject: [PATCH 32/77] Fix doc links --- crates/egui/src/atomic_layout.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs index 26bbc9ef83f..fba22010ec7 100644 --- a/crates/egui/src/atomic_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -135,7 +135,7 @@ impl<'a> AtomicLayout<'a> { /// /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) - /// for info on how the [`Layout`] affects the alignment. + /// for info on how the [`crate::Layout`] affects the alignment. #[inline] pub fn align2(mut self, align2: Align2) -> Self { self.align2 = Some(align2); @@ -390,7 +390,7 @@ pub struct AtomicLayoutResponse { /// The different kinds of [`Atomic`]s. #[derive(Clone, Default)] pub enum AtomicKind<'a> { - /// Empty, that can be used with [`Atomic::a_grow`] to reserve space. + /// Empty, that can be used with [`AtomicExt::a_grow`] to reserve space. #[default] Empty, Text(WidgetText), From 55bbf2b2e25375818be13d640025543177073464 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 24 Apr 2025 11:38:51 +0200 Subject: [PATCH 33/77] Revert style change --- crates/egui/src/style.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 194de500569..38d403749f5 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1514,7 +1514,7 @@ impl Widgets { inactive: WidgetVisuals { weak_bg_fill: Color32::from_gray(230), // button background bg_fill: Color32::from_gray(230), // checkbox background - bg_stroke: Stroke::new(1.0, Color32::default()), + bg_stroke: Default::default(), fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text corner_radius: CornerRadius::same(2), expansion: 0.0, From 3303b276484b8564281ddad259c43d39dee1f517 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 11:49:36 +0200 Subject: [PATCH 34/77] Sprinkle some more atomics --- crates/egui/src/ui.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 9094c00ed4e..f15838e710a 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2073,7 +2073,7 @@ impl Ui { /// /// See also [`Self::toggle_value`]. #[inline] - pub fn checkbox(&mut self, checked: &mut bool, text: impl Into) -> Response { + pub fn checkbox<'a>(&mut self, checked: &'a mut bool, text: impl IntoAtomics<'a>) -> Response { Checkbox::new(checked, text).ui(self) } @@ -2095,7 +2095,7 @@ impl Ui { /// Often you want to use [`Self::radio_value`] instead. #[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "] #[inline] - pub fn radio(&mut self, selected: bool, text: impl Into) -> Response { + pub fn radio<'a>(&mut self, selected: bool, text: impl IntoAtomics<'a>) -> Response { RadioButton::new(selected, text).ui(self) } @@ -2118,11 +2118,11 @@ impl Ui { /// } /// # }); /// ``` - pub fn radio_value( + pub fn radio_value<'a, Value: PartialEq>( &mut self, current_value: &mut Value, alternative: Value, - text: impl Into, + text: impl IntoAtomics<'a>, ) -> Response { let mut response = self.radio(*current_value == alternative, text); if response.clicked() && *current_value != alternative { @@ -3041,15 +3041,15 @@ impl Ui { /// ``` /// /// See also: [`Self::close`] and [`Response::context_menu`]. - pub fn menu_button( + pub fn menu_button<'a, R>( &mut self, - title: impl Into, + content: impl IntoAtomics<'a>, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse> { let (response, inner) = if menu::is_in_menu(self) { - menu::SubMenuButton::new(title).ui(self, add_contents) + menu::SubMenuButton::new(content).ui(self, add_contents) } else { - menu::MenuButton::new(title).ui(self, add_contents) + menu::MenuButton::new(content).ui(self, add_contents) }; InnerResponse::new(inner.map(|i| i.inner), response) } From 2524f44ceeac8de9d7082672e2eda7f4a2fffa41 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 17:01:32 +0200 Subject: [PATCH 35/77] Split atomics into several modules --- crates/egui/src/atomic_layout.rs | 739 ------------------- crates/egui/src/atomics/atomic.rs | 119 +++ crates/egui/src/atomics/atomic_kind.rs | 105 +++ crates/egui/src/atomics/atomic_layout.rs | 392 ++++++++++ crates/egui/src/atomics/atomics.rs | 118 +++ crates/egui/src/atomics/mod.rs | 14 + crates/egui/src/atomics/sized_atomic.rs | 11 + crates/egui/src/atomics/sized_atomic_kind.rs | 25 + crates/egui/src/lib.rs | 3 +- 9 files changed, 786 insertions(+), 740 deletions(-) create mode 100644 crates/egui/src/atomics/atomic.rs create mode 100644 crates/egui/src/atomics/atomic_kind.rs create mode 100644 crates/egui/src/atomics/atomic_layout.rs create mode 100644 crates/egui/src/atomics/atomics.rs create mode 100644 crates/egui/src/atomics/mod.rs create mode 100644 crates/egui/src/atomics/sized_atomic.rs create mode 100644 crates/egui/src/atomics/sized_atomic_kind.rs diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs index fba22010ec7..8b137891791 100644 --- a/crates/egui/src/atomic_layout.rs +++ b/crates/egui/src/atomic_layout.rs @@ -1,740 +1 @@ -use crate::{ - FontSelection, Frame, Id, Image, Response, Sense, Style, TextStyle, Ui, Widget, WidgetText, -}; -use ahash::{HashMap, HashMapExt}; -use emath::{Align2, NumExt, Rect, Vec2}; -use epaint::text::TextWrapMode; -use epaint::{Color32, Fonts, Galley}; -use std::fmt::Formatter; -use std::sync::Arc; -/// Intra-widget layout utility. -/// -/// Used to lay out and paint [`Atomic`]s. -/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`]. -/// You can use it to make your own widgets. -/// -/// Painting the atomics can be split in two phases: -/// - [`AtomicLayout::allocate`] -/// - calculates sizes -/// - converts texts to [`Galley`]s -/// - allocates a [`Response`] -/// - returns a [`AllocatedAtomicLayout`] -/// - [`AllocatedAtomicLayout::paint`] -/// - paints the [`Frame`] -/// - calculates individual [`Atomic`] positions -/// - paints each single atomic -/// -/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the -/// [`AllocatedAtomicLayout`] for interaction styling. -pub struct AtomicLayout<'a> { - id: Option, - pub atomics: Atomics<'a>, - gap: Option, - pub(crate) frame: Frame, - pub(crate) sense: Sense, - fallback_text_color: Option, - min_size: Vec2, - wrap_mode: Option, - align2: Option, -} - -impl Default for AtomicLayout<'_> { - fn default() -> Self { - Self::new(()) - } -} - -impl<'a> AtomicLayout<'a> { - pub fn new(atomics: impl IntoAtomics<'a>) -> Self { - Self { - id: None, - atomics: atomics.into_atomics(), - gap: None, - frame: Frame::default(), - sense: Sense::hover(), - fallback_text_color: None, - min_size: Vec2::ZERO, - wrap_mode: None, - align2: None, - } - } - - /// Insert a new [`Atomic`] at the end of the list (left side). - #[inline] - pub fn push(mut self, atomic: impl Into>) -> Self { - self.atomics.push(atomic.into()); - self - } - - /// Insert a new [`Atomic`] at the beginning of the list (right side). - #[inline] - pub fn push_front(mut self, atomic: impl Into>) -> Self { - self.atomics.push_front(atomic.into()); - self - } - - /// Set the gap between atomics. - /// - /// Default: `Spacing::icon_spacing` - #[inline] - pub fn gap(mut self, gap: f32) -> Self { - self.gap = Some(gap); - self - } - - /// Set the [`Frame`]. - #[inline] - pub fn frame(mut self, frame: Frame) -> Self { - self.frame = frame; - self - } - - /// Set the [`Sense`] used when allocating the [`Response`]. - #[inline] - pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; - self - } - - /// Set the fallback (default) text color. - /// - /// Default: [`crate::Visuals::text_color`] - #[inline] - pub fn fallback_text_color(mut self, color: Color32) -> Self { - self.fallback_text_color = Some(color); - self - } - - /// Set the minimum size of the Widget. - #[inline] - pub fn min_size(mut self, size: Vec2) -> Self { - self.min_size = size; - self - } - - /// Set the [`Id`] used to allocate a [`Response`]. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - /// Set the [`TextWrapMode`] for the [`Atomic`] marked as `shrink`. - /// - /// Only a single [`Atomic`] may shrink. If this (or `ui.wrap_mode()`) is not - /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (right-most) - /// [`AtomicKind::Text`] will be set to shrink. - #[inline] - pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { - self.wrap_mode = Some(wrap_mode); - self - } - - /// Set the [`Align2`]. - /// - /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See - /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) - /// for info on how the [`crate::Layout`] affects the alignment. - #[inline] - pub fn align2(mut self, align2: Align2) -> Self { - self.align2 = Some(align2); - self - } - - /// [`AtomicLayout::allocate`] and [`AllocatedAtomicLayout::paint`] in one go. - pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { - self.allocate(ui).paint(ui) - } - - /// Calculate sizes, create [`Galley`]s and allocate a [`Response`]. - /// - /// Use the returned [`AllocatedAtomicLayout`] for painting. - pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomicLayout<'a> { - let Self { - id, - mut atomics, - gap, - frame, - sense, - fallback_text_color, - min_size, - wrap_mode, - align2, - } = self; - - let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); - - // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. - // If none is found, mark the first text item as `shrink`. - if !matches!(wrap_mode, TextWrapMode::Extend) { - let any_shrink = atomics.iter().any(|a| a.shrink); - if !any_shrink { - let first_text = atomics - .iter_mut() - .find(|a| matches!(a.kind, AtomicKind::Text(..))); - if let Some(atomic) = first_text { - atomic.shrink = true; - } - } - } - - let id = id.unwrap_or_else(|| ui.next_auto_id()); - - let fallback_text_color = - fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color()); - let gap = gap.unwrap_or(ui.spacing().icon_spacing); - - // The size available for the content - let available_inner_size = ui.available_size() - frame.total_margin().sum(); - - let mut desired_width = 0.0; - let mut preferred_width = 0.0; - let mut preferred_height = 0.0; - - let mut height: f32 = 0.0; - - let mut sized_items = Vec::new(); - - let mut grow_count = 0; - - let mut shrink_item = None; - - let align2 = align2.unwrap_or_else(|| { - Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()]) - }); - - if atomics.0.len() > 1 { - let gap_space = gap * (atomics.0.len() as f32 - 1.0); - desired_width += gap_space; - preferred_width += gap_space; - } - - let default_font_height = || { - let font_selection = FontSelection::default(); - let font_id = font_selection.resolve(ui.style()); - ui.fonts(|f| f.row_height(&font_id)) - }; - - let max_font_size = ui - .fonts(|fonts| { - atomics - .0 - .iter() - .filter_map(|a| a.get_min_height_for_image(fonts, ui.style())) - .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) - }) - .unwrap_or_else(default_font_height); - - for (idx, item) in atomics.0.into_iter().enumerate() { - if item.shrink { - debug_assert!( - shrink_item.is_none(), - "Only one atomic may be marked as shrink" - ); - if shrink_item.is_none() { - shrink_item = Some((idx, item)); - continue; - } - } - if item.grow { - grow_count += 1; - } - let sized = item.into_sized(ui, available_inner_size, max_font_size, Some(wrap_mode)); - let size = sized.size; - - desired_width += size.x; - preferred_width += sized.preferred_size.x; - - height = height.at_least(size.y); - preferred_height = preferred_height.at_least(sized.preferred_size.y); - - sized_items.push(sized); - } - - if let Some((index, item)) = shrink_item { - // The `shrink` item gets the remaining space - let shrunk_size = Vec2::new( - available_inner_size.x - desired_width, - available_inner_size.y, - ); - if item.grow { - grow_count += 1; - } - - let sized = item.into_sized(ui, shrunk_size, max_font_size, Some(wrap_mode)); - let size = sized.size; - - desired_width += size.x; - preferred_width += sized.preferred_size.x; - - height = height.at_least(size.y); - preferred_height = preferred_height.at_least(sized.preferred_size.y); - - sized_items.insert(index, sized); - } - - let margin = frame.total_margin(); - let desired_size = Vec2::new(desired_width, height); - let frame_size = (desired_size + margin.sum()).at_least(min_size); - - let (_, rect) = ui.allocate_space(frame_size); - let mut response = ui.interact(rect, id, sense); - - response.intrinsic_size = - Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size)); - - AllocatedAtomicLayout { - sized_atomics: sized_items, - frame, - fallback_text_color, - response, - grow_count, - desired_size, - align2, - gap, - } - } -} - -/// Instructions for painting an [`AtomicLayout`]. -#[derive(Clone, Debug)] -pub struct AllocatedAtomicLayout<'a> { - pub sized_atomics: Vec>, - pub frame: Frame, - pub fallback_text_color: Color32, - pub response: Response, - grow_count: usize, - // The size of the inner content, before any growing. - desired_size: Vec2, - align2: Align2, - gap: f32, -} - -impl<'a> AllocatedAtomicLayout<'a> { - /// Paint the [`Frame`] and individual [`Atomic`]s. - pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { - let Self { - sized_atomics: sized_items, - frame, - fallback_text_color, - response, - grow_count, - desired_size, - align2, - gap, - } = self; - - let inner_rect = response.rect - self.frame.total_margin(); - - ui.painter().add(frame.paint(inner_rect)); - - let width_to_fill = inner_rect.width(); - let extra_space = f32::max(width_to_fill - desired_size.x, 0.0); - let grow_width = f32::max(extra_space / grow_count as f32, 0.0); - - let aligned_rect = if grow_count > 0 { - align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect) - } else { - align2.align_size_within_rect(desired_size, inner_rect) - }; - - let mut cursor = aligned_rect.left(); - - let mut response = AtomicLayoutResponse { - response, - custom_rects: HashMap::new(), - }; - - for sized in sized_items { - let size = sized.size; - let growth = if sized.grow { grow_width } else { 0.0 }; - - let frame = aligned_rect - .with_min_x(cursor) - .with_max_x(cursor + size.x + growth); - cursor = frame.right() + gap; - - let align = Align2::CENTER_CENTER; - let rect = align.align_size_within_rect(size, frame); - - match sized.kind { - SizedAtomicKind::Text(galley) => { - ui.painter().galley(rect.min, galley, fallback_text_color); - } - SizedAtomicKind::Image(image, _) => { - image.paint_at(ui, rect); - } - SizedAtomicKind::Custom(id, _) => { - response.custom_rects.insert(id, rect); - } - SizedAtomicKind::Empty => {} - } - } - - response - } -} - -/// Response from a [`AtomicLayout::show`] or [`AllocatedAtomicLayout::paint`]. -/// -/// Use the `custom_rects` together with [`AtomicKind::Custom`] to add child widgets to a widget. -/// -/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible. -#[derive(Clone, Debug)] -pub struct AtomicLayoutResponse { - pub response: Response, - pub custom_rects: HashMap, -} - -/// The different kinds of [`Atomic`]s. -#[derive(Clone, Default)] -pub enum AtomicKind<'a> { - /// Empty, that can be used with [`AtomicExt::a_grow`] to reserve space. - #[default] - Empty, - Text(WidgetText), - Image(Image<'a>), - - /// For custom rendering. - /// - /// You can get the [`Rect`] with the [`Id`] from [`AtomicLayoutResponse`] and use a - /// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content. - /// - /// Example: - /// ``` - /// # use egui::{AtomicKind, Button, Id, __run_test_ui}; - /// # use emath::Vec2; - /// # __run_test_ui(|ui| { - /// let id = Id::new("my_button"); - /// let response = Button::new(("Hi!", AtomicKind::Custom(id, Vec2::splat(18.0)))).atomic_ui(ui); - /// - /// let rect = response.custom_rects.get(&id); - /// if let Some(rect) = rect { - /// ui.put(*rect, Button::new("⏵")); - /// } - /// # }); - /// ``` - Custom(Id, Vec2), -} - -impl std::fmt::Debug for AtomicKind<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - AtomicKind::Empty => write!(f, "AtomicKind::Empty"), - AtomicKind::Text(text) => write!(f, "AtomicKind::Text({})", text.text()), - AtomicKind::Image(image) => write!(f, "AtomicKind::Image({image:?})"), - AtomicKind::Custom(id, size) => write!(f, "AtomicKind::Custom({id:?}, {size:?})"), - } - } -} - -impl<'a> AtomicKind<'a> { - pub fn text(text: impl Into) -> Self { - AtomicKind::Text(text.into()) - } - - pub fn image(image: impl Into>) -> Self { - AtomicKind::Image(image.into()) - } - - pub fn custom(id: Id, size: Vec2) -> Self { - AtomicKind::Custom(id, size) - } - - /// Turn this [`AtomicKind`] into a [`SizedAtomicKind`]. - /// - /// This converts [`WidgetText`] into [`Galley`] and tries to load and size [`Image`]. - /// The first returned argument is the preferred size. - pub fn into_sized( - self, - ui: &Ui, - available_size: Vec2, - font_size: f32, - wrap_mode: Option, - ) -> (Vec2, SizedAtomicKind<'a>) { - match self { - AtomicKind::Text(text) => { - let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); - ( - galley.size(), // TODO(lucasmerlin): calculate the preferred size - SizedAtomicKind::Text(galley), - ) - } - AtomicKind::Image(image) => { - let max_size = Vec2::splat(font_size); - let size = image.load_and_calc_size(ui, Vec2::min(available_size, max_size)); - let size = size.unwrap_or(max_size); - (size, SizedAtomicKind::Image(image, size)) - } - AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), - AtomicKind::Empty => (Vec2::ZERO, SizedAtomicKind::Empty), - } - } -} - -/// A sized [`AtomicKind`]. -#[derive(Clone, Default, Debug)] -pub enum SizedAtomicKind<'a> { - #[default] - Empty, - Text(Arc), - Image(Image<'a>, Vec2), - Custom(Id, Vec2), -} - -impl SizedAtomicKind<'_> { - /// Get the calculated size. - pub fn size(&self) -> Vec2 { - match self { - SizedAtomicKind::Text(galley) => galley.size(), - SizedAtomicKind::Image(_, size) | SizedAtomicKind::Custom(_, size) => *size, - SizedAtomicKind::Empty => Vec2::ZERO, - } - } -} - -/// A low-level ui building block. -/// -/// Implements [`From`] for [`String`], [`str`], [`Image`] and much more for convenience. -/// You can directly call the `a_*` methods on anything that implements `Into`. -/// ``` -/// # use egui::{Image, emath::Vec2}; -/// use egui::AtomicExt; -/// let string_atomic = "Hello".a_grow(true); -/// let image_atomic = Image::new("some_image_url").a_size(Vec2::splat(20.0)); -/// ``` -#[derive(Clone, Debug)] -pub struct Atomic<'a> { - pub size: Option, - pub grow: bool, - pub shrink: bool, - pub kind: AtomicKind<'a>, -} - -/// A [`Atomic`] which has been sized. -#[derive(Clone, Debug)] -pub struct SizedAtomic<'a> { - pub grow: bool, - pub size: Vec2, - pub preferred_size: Vec2, - pub kind: SizedAtomicKind<'a>, -} - -impl<'a> Atomic<'a> { - /// Create an empty [`Atomic`] marked as `grow`. - pub fn grow() -> Self { - Atomic { - size: None, - grow: true, - shrink: false, - kind: AtomicKind::Empty, - } - } - - /// Heuristic to find the best height for an image. - /// Basically returns the height if this is not an [`Image`]. - fn get_min_height_for_image(&self, fonts: &Fonts, style: &Style) -> Option { - self.size.map(|s| s.y).or_else(|| { - match &self.kind { - AtomicKind::Text(text) => Some(text.font_height(fonts, style)), - AtomicKind::Custom(_, size) => Some(size.y), - // Since this method is used to calculate the best height for an image, we always return - // None for images. - AtomicKind::Empty | AtomicKind::Image(_) => None, - } - }) - } - - /// Turn this into a [`SizedAtomic`]. - pub fn into_sized( - self, - ui: &Ui, - available_size: Vec2, - font_size: f32, - wrap_mode: Option, - ) -> SizedAtomic<'a> { - let (preferred, kind) = self - .kind - .into_sized(ui, available_size, font_size, wrap_mode); - SizedAtomic { - size: self.size.unwrap_or_else(|| kind.size()), - preferred_size: preferred, - grow: self.grow, - kind, - } - } -} - -/// A trait for conveniently building [`Atomic`]s. -pub trait AtomicExt<'a> { - /// Set the atomic to a fixed size. - fn a_size(self, size: Vec2) -> Atomic<'a>; - - /// Grow this atomic to the available space. - fn a_grow(self, grow: bool) -> Atomic<'a>; - - /// Shrink this atomic if there isn't enough space. - /// - /// NOTE: Only a single [`Atomic`] may shrink for each widget. - fn a_shrink(self, shrink: bool) -> Atomic<'a>; -} - -impl<'a, T> AtomicExt<'a> for T -where - T: Into> + Sized, -{ - fn a_size(self, size: Vec2) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.size = Some(size); - atomic - } - - fn a_grow(self, grow: bool) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.grow = grow; - atomic - } - - /// NOTE: Only a single atomic may be marked as shrink - fn a_shrink(self, shrink: bool) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.shrink = shrink; - atomic - } -} - -impl<'a, T> From for Atomic<'a> -where - T: Into>, -{ - fn from(value: T) -> Self { - Atomic { - size: None, - grow: false, - shrink: false, - kind: value.into(), - } - } -} - -impl<'a> From> for AtomicKind<'a> { - fn from(value: Image<'a>) -> Self { - AtomicKind::Image(value) - } -} - -impl<'a, T> From for AtomicKind<'a> -where - T: Into, -{ - fn from(value: T) -> Self { - AtomicKind::Text(value.into()) - } -} - -/// A list of [`Atomic`]s. -#[derive(Clone, Debug, Default)] -pub struct Atomics<'a>(Vec>); - -impl<'a> Atomics<'a> { - pub fn push(&mut self, atomic: impl Into>) { - self.0.push(atomic.into()); - } - - pub fn push_front(&mut self, atomic: impl Into>) { - self.0.insert(0, atomic.into()); - } - - pub fn iter(&self) -> impl Iterator> { - self.0.iter() - } - - pub fn iter_mut(&mut self) -> impl Iterator> { - self.0.iter_mut() - } - - /// Concatenate and return the text contents. - // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. - // in a submenu button there is a right text '⏵' which is now passed to the screen reader. - pub fn text(&self) -> Option { - let mut string: Option = None; - for atomic in &self.0 { - if let AtomicKind::Text(text) = &atomic.kind { - if let Some(string) = &mut string { - string.push(' '); - string.push_str(text.text()); - } else { - string = Some(text.text().to_owned()); - } - } - } - string - } -} - -/// Helper trait to convert a tuple of atomics into [`Atomics`]. -/// -/// ``` -/// use egui::{Atomics, Image, IntoAtomics, RichText}; -/// let atomics: Atomics = ( -/// "Some text", -/// RichText::new("Some RichText"), -/// Image::new("some_image_url"), -/// ).into_atomics(); -/// ``` -impl<'a, T> IntoAtomics<'a> for T -where - T: Into>, -{ - fn collect(self, atomics: &mut Atomics<'a>) { - atomics.push(self); - } -} - -pub trait IntoAtomics<'a> { - fn collect(self, atomics: &mut Atomics<'a>); - - fn into_atomics(self) -> Atomics<'a> - where - Self: Sized, - { - let mut atomics = Atomics(Vec::new()); - self.collect(&mut atomics); - atomics - } -} - -impl<'a> IntoAtomics<'a> for Atomics<'a> { - fn collect(self, atomics: &mut Self) { - atomics.0.extend(self.0); - } -} - -macro_rules! all_the_atomics { - ($($T:ident),*) => { - impl<'a, $($T),*> IntoAtomics<'a> for ($($T),*) - where - $($T: IntoAtomics<'a>),* - { - fn collect(self, _atomics: &mut Atomics<'a>) { - #[allow(non_snake_case)] - let ($($T),*) = self; - $($T.collect(_atomics);)* - } - } - }; -} - -all_the_atomics!(); -all_the_atomics!(T0, T1); -all_the_atomics!(T0, T1, T2); -all_the_atomics!(T0, T1, T2, T3); -all_the_atomics!(T0, T1, T2, T3, T4); -all_the_atomics!(T0, T1, T2, T3, T4, T5); - -impl Widget for AtomicLayout<'_> { - fn ui(self, ui: &mut Ui) -> Response { - self.show(ui).response - } -} diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs new file mode 100644 index 00000000000..23f1f8cfe6a --- /dev/null +++ b/crates/egui/src/atomics/atomic.rs @@ -0,0 +1,119 @@ +use crate::{AtomicKind, SizedAtomic, Style, Ui}; +use emath::Vec2; +use epaint::text::TextWrapMode; +use epaint::Fonts; + +/// A low-level ui building block. +/// +/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience. +/// You can directly call the `a_*` methods on anything that implements `Into`. +/// ``` +/// # use egui::{Image, emath::Vec2}; +/// use egui::AtomicExt; +/// let string_atomic = "Hello".a_grow(true); +/// let image_atomic = Image::new("some_image_url").a_size(Vec2::splat(20.0)); +/// ``` +#[derive(Clone, Debug)] +pub struct Atomic<'a> { + pub size: Option, + pub grow: bool, + pub shrink: bool, + pub kind: AtomicKind<'a>, +} + +impl<'a> Atomic<'a> { + /// Create an empty [`Atomic`] marked as `grow`. + pub fn grow() -> Self { + Atomic { + size: None, + grow: true, + shrink: false, + kind: AtomicKind::Empty, + } + } + + /// Heuristic to find the best height for an image. + /// Basically returns the height if this is not an [`Image`]. + pub(crate) fn get_min_height_for_image(&self, fonts: &Fonts, style: &Style) -> Option { + self.size.map(|s| s.y).or_else(|| { + match &self.kind { + AtomicKind::Text(text) => Some(text.font_height(fonts, style)), + AtomicKind::Custom(_, size) => Some(size.y), + // Since this method is used to calculate the best height for an image, we always return + // None for images. + AtomicKind::Empty | AtomicKind::Image(_) => None, + } + }) + } + + /// Turn this into a [`SizedAtomic`]. + pub fn into_sized( + self, + ui: &Ui, + available_size: Vec2, + font_size: f32, + wrap_mode: Option, + ) -> SizedAtomic<'a> { + let (preferred, kind) = self + .kind + .into_sized(ui, available_size, font_size, wrap_mode); + SizedAtomic { + size: self.size.unwrap_or_else(|| kind.size()), + preferred_size: preferred, + grow: self.grow, + kind, + } + } +} + +/// A trait for conveniently building [`Atomic`]s. +pub trait AtomicExt<'a> { + /// Set the atomic to a fixed size. + fn a_size(self, size: Vec2) -> Atomic<'a>; + + /// Grow this atomic to the available space. + fn a_grow(self, grow: bool) -> Atomic<'a>; + + /// Shrink this atomic if there isn't enough space. + /// + /// NOTE: Only a single [`Atomic`] may shrink for each widget. + fn a_shrink(self, shrink: bool) -> Atomic<'a>; +} + +impl<'a, T> AtomicExt<'a> for T +where + T: Into> + Sized, +{ + fn a_size(self, size: Vec2) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.size = Some(size); + atomic + } + + fn a_grow(self, grow: bool) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.grow = grow; + atomic + } + + /// NOTE: Only a single atomic may be marked as shrink + fn a_shrink(self, shrink: bool) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.shrink = shrink; + atomic + } +} + +impl<'a, T> From for Atomic<'a> +where + T: Into>, +{ + fn from(value: T) -> Self { + Atomic { + size: None, + grow: false, + shrink: false, + kind: value.into(), + } + } +} diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs new file mode 100644 index 00000000000..d233db5e3f8 --- /dev/null +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -0,0 +1,105 @@ +use crate::{Id, Image, SizedAtomicKind, TextStyle, Ui, WidgetText}; +use emath::Vec2; +use epaint::text::TextWrapMode; +use std::fmt::Formatter; + +/// The different kinds of [`Atomic`]s. +#[derive(Clone, Default)] +pub enum AtomicKind<'a> { + /// Empty, that can be used with [`AtomicExt::a_grow`] to reserve space. + #[default] + Empty, + Text(WidgetText), + Image(Image<'a>), + + /// For custom rendering. + /// + /// You can get the [`Rect`] with the [`Id`] from [`AtomicLayoutResponse`] and use a + /// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content. + /// + /// Example: + /// ``` + /// # use egui::{AtomicKind, Button, Id, __run_test_ui}; + /// # use emath::Vec2; + /// # __run_test_ui(|ui| { + /// let id = Id::new("my_button"); + /// let response = Button::new(("Hi!", AtomicKind::Custom(id, Vec2::splat(18.0)))).atomic_ui(ui); + /// + /// let rect = response.custom_rects.get(&id); + /// if let Some(rect) = rect { + /// ui.put(*rect, Button::new("⏵")); + /// } + /// # }); + /// ``` + Custom(Id, Vec2), +} + +impl std::fmt::Debug for AtomicKind<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AtomicKind::Empty => write!(f, "AtomicKind::Empty"), + AtomicKind::Text(text) => write!(f, "AtomicKind::Text({})", text.text()), + AtomicKind::Image(image) => write!(f, "AtomicKind::Image({image:?})"), + AtomicKind::Custom(id, size) => write!(f, "AtomicKind::Custom({id:?}, {size:?})"), + } + } +} + +impl<'a> AtomicKind<'a> { + pub fn text(text: impl Into) -> Self { + AtomicKind::Text(text.into()) + } + + pub fn image(image: impl Into>) -> Self { + AtomicKind::Image(image.into()) + } + + pub fn custom(id: Id, size: Vec2) -> Self { + AtomicKind::Custom(id, size) + } + + /// Turn this [`AtomicKind`] into a [`SizedAtomicKind`]. + /// + /// This converts [`WidgetText`] into [`Galley`] and tries to load and size [`Image`]. + /// The first returned argument is the preferred size. + pub fn into_sized( + self, + ui: &Ui, + available_size: Vec2, + font_size: f32, + wrap_mode: Option, + ) -> (Vec2, SizedAtomicKind<'a>) { + match self { + AtomicKind::Text(text) => { + let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); + ( + galley.size(), // TODO(lucasmerlin): calculate the preferred size + SizedAtomicKind::Text(galley), + ) + } + AtomicKind::Image(image) => { + let max_size = Vec2::splat(font_size); + let size = image.load_and_calc_size(ui, Vec2::min(available_size, max_size)); + let size = size.unwrap_or(max_size); + (size, SizedAtomicKind::Image(image, size)) + } + AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), + AtomicKind::Empty => (Vec2::ZERO, SizedAtomicKind::Empty), + } + } +} + +impl<'a> From> for AtomicKind<'a> { + fn from(value: Image<'a>) -> Self { + AtomicKind::Image(value) + } +} + +impl<'a, T> From for AtomicKind<'a> +where + T: Into, +{ + fn from(value: T) -> Self { + AtomicKind::Text(value.into()) + } +} diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs new file mode 100644 index 00000000000..8acdb3e56ca --- /dev/null +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -0,0 +1,392 @@ +use crate::{ + Atomic, AtomicKind, Atomics, FontSelection, Frame, Id, IntoAtomics, Response, Sense, + SizedAtomic, SizedAtomicKind, Ui, Widget, +}; +use ahash::{HashMap, HashMapExt}; +use emath::{Align2, NumExt, Rect, Vec2}; +use epaint::text::TextWrapMode; +use epaint::Color32; + +/// Intra-widget layout utility. +/// +/// Used to lay out and paint [`Atomic`]s. +/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`]. +/// You can use it to make your own widgets. +/// +/// Painting the atomics can be split in two phases: +/// - [`AtomicLayout::allocate`] +/// - calculates sizes +/// - converts texts to [`Galley`]s +/// - allocates a [`Response`] +/// - returns a [`AllocatedAtomicLayout`] +/// - [`AllocatedAtomicLayout::paint`] +/// - paints the [`Frame`] +/// - calculates individual [`Atomic`] positions +/// - paints each single atomic +/// +/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the +/// [`AllocatedAtomicLayout`] for interaction styling. +pub struct AtomicLayout<'a> { + id: Option, + pub atomics: Atomics<'a>, + gap: Option, + pub(crate) frame: Frame, + pub(crate) sense: Sense, + fallback_text_color: Option, + min_size: Vec2, + wrap_mode: Option, + align2: Option, +} + +impl Default for AtomicLayout<'_> { + fn default() -> Self { + Self::new(()) + } +} + +impl<'a> AtomicLayout<'a> { + pub fn new(atomics: impl IntoAtomics<'a>) -> Self { + Self { + id: None, + atomics: atomics.into_atomics(), + gap: None, + frame: Frame::default(), + sense: Sense::hover(), + fallback_text_color: None, + min_size: Vec2::ZERO, + wrap_mode: None, + align2: None, + } + } + + /// Insert a new [`Atomic`] at the end of the list (left side). + #[inline] + pub fn push(mut self, atomic: impl Into>) -> Self { + self.atomics.push(atomic.into()); + self + } + + /// Insert a new [`Atomic`] at the beginning of the list (right side). + #[inline] + pub fn push_front(mut self, atomic: impl Into>) -> Self { + self.atomics.push_front(atomic.into()); + self + } + + /// Set the gap between atomics. + /// + /// Default: `Spacing::icon_spacing` + #[inline] + pub fn gap(mut self, gap: f32) -> Self { + self.gap = Some(gap); + self + } + + /// Set the [`Frame`]. + #[inline] + pub fn frame(mut self, frame: Frame) -> Self { + self.frame = frame; + self + } + + /// Set the [`Sense`] used when allocating the [`Response`]. + #[inline] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = sense; + self + } + + /// Set the fallback (default) text color. + /// + /// Default: [`crate::Visuals::text_color`] + #[inline] + pub fn fallback_text_color(mut self, color: Color32) -> Self { + self.fallback_text_color = Some(color); + self + } + + /// Set the minimum size of the Widget. + #[inline] + pub fn min_size(mut self, size: Vec2) -> Self { + self.min_size = size; + self + } + + /// Set the [`Id`] used to allocate a [`Response`]. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + + /// Set the [`TextWrapMode`] for the [`Atomic`] marked as `shrink`. + /// + /// Only a single [`Atomic`] may shrink. If this (or `ui.wrap_mode()`) is not + /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (right-most) + /// [`AtomicKind::Text`] will be set to shrink. + #[inline] + pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { + self.wrap_mode = Some(wrap_mode); + self + } + + /// Set the [`Align2`]. + /// + /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See + /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) + /// for info on how the [`crate::Layout`] affects the alignment. + #[inline] + pub fn align2(mut self, align2: Align2) -> Self { + self.align2 = Some(align2); + self + } + + /// [`AtomicLayout::allocate`] and [`AllocatedAtomicLayout::paint`] in one go. + pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { + self.allocate(ui).paint(ui) + } + + /// Calculate sizes, create [`Galley`]s and allocate a [`Response`]. + /// + /// Use the returned [`AllocatedAtomicLayout`] for painting. + pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomicLayout<'a> { + let Self { + id, + mut atomics, + gap, + frame, + sense, + fallback_text_color, + min_size, + wrap_mode, + align2, + } = self; + + let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); + + // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. + // If none is found, mark the first text item as `shrink`. + if !matches!(wrap_mode, TextWrapMode::Extend) { + let any_shrink = atomics.iter().any(|a| a.shrink); + if !any_shrink { + let first_text = atomics + .iter_mut() + .find(|a| matches!(a.kind, AtomicKind::Text(..))); + if let Some(atomic) = first_text { + atomic.shrink = true; + } + } + } + + let id = id.unwrap_or_else(|| ui.next_auto_id()); + + let fallback_text_color = + fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color()); + let gap = gap.unwrap_or(ui.spacing().icon_spacing); + + // The size available for the content + let available_inner_size = ui.available_size() - frame.total_margin().sum(); + + let mut desired_width = 0.0; + let mut preferred_width = 0.0; + let mut preferred_height = 0.0; + + let mut height: f32 = 0.0; + + let mut sized_items = Vec::new(); + + let mut grow_count = 0; + + let mut shrink_item = None; + + let align2 = align2.unwrap_or_else(|| { + Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()]) + }); + + if atomics.len() > 1 { + let gap_space = gap * (atomics.len() as f32 - 1.0); + desired_width += gap_space; + preferred_width += gap_space; + } + + let default_font_height = || { + let font_selection = FontSelection::default(); + let font_id = font_selection.resolve(ui.style()); + ui.fonts(|f| f.row_height(&font_id)) + }; + + let max_font_size = ui + .fonts(|fonts| { + atomics + .iter() + .filter_map(|a| a.get_min_height_for_image(fonts, ui.style())) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + }) + .unwrap_or_else(default_font_height); + + for (idx, item) in atomics.into_iter().enumerate() { + if item.shrink { + debug_assert!( + shrink_item.is_none(), + "Only one atomic may be marked as shrink" + ); + if shrink_item.is_none() { + shrink_item = Some((idx, item)); + continue; + } + } + if item.grow { + grow_count += 1; + } + let sized = item.into_sized(ui, available_inner_size, max_font_size, Some(wrap_mode)); + let size = sized.size; + + desired_width += size.x; + preferred_width += sized.preferred_size.x; + + height = height.at_least(size.y); + preferred_height = preferred_height.at_least(sized.preferred_size.y); + + sized_items.push(sized); + } + + if let Some((index, item)) = shrink_item { + // The `shrink` item gets the remaining space + let shrunk_size = Vec2::new( + available_inner_size.x - desired_width, + available_inner_size.y, + ); + if item.grow { + grow_count += 1; + } + + let sized = item.into_sized(ui, shrunk_size, max_font_size, Some(wrap_mode)); + let size = sized.size; + + desired_width += size.x; + preferred_width += sized.preferred_size.x; + + height = height.at_least(size.y); + preferred_height = preferred_height.at_least(sized.preferred_size.y); + + sized_items.insert(index, sized); + } + + let margin = frame.total_margin(); + let desired_size = Vec2::new(desired_width, height); + let frame_size = (desired_size + margin.sum()).at_least(min_size); + + let (_, rect) = ui.allocate_space(frame_size); + let mut response = ui.interact(rect, id, sense); + + response.intrinsic_size = + Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size)); + + AllocatedAtomicLayout { + sized_atomics: sized_items, + frame, + fallback_text_color, + response, + grow_count, + desired_size, + align2, + gap, + } + } +} + +/// Instructions for painting an [`AtomicLayout`]. +#[derive(Clone, Debug)] +pub struct AllocatedAtomicLayout<'a> { + pub sized_atomics: Vec>, + pub frame: Frame, + pub fallback_text_color: Color32, + pub response: Response, + grow_count: usize, + // The size of the inner content, before any growing. + desired_size: Vec2, + align2: Align2, + gap: f32, +} + +impl<'a> AllocatedAtomicLayout<'a> { + /// Paint the [`Frame`] and individual [`Atomic`]s. + pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { + let Self { + sized_atomics: sized_items, + frame, + fallback_text_color, + response, + grow_count, + desired_size, + align2, + gap, + } = self; + + let inner_rect = response.rect - self.frame.total_margin(); + + ui.painter().add(frame.paint(inner_rect)); + + let width_to_fill = inner_rect.width(); + let extra_space = f32::max(width_to_fill - desired_size.x, 0.0); + let grow_width = f32::max(extra_space / grow_count as f32, 0.0); + + let aligned_rect = if grow_count > 0 { + align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect) + } else { + align2.align_size_within_rect(desired_size, inner_rect) + }; + + let mut cursor = aligned_rect.left(); + + let mut response = AtomicLayoutResponse { + response, + custom_rects: HashMap::new(), + }; + + for sized in sized_items { + let size = sized.size; + let growth = if sized.grow { grow_width } else { 0.0 }; + + let frame = aligned_rect + .with_min_x(cursor) + .with_max_x(cursor + size.x + growth); + cursor = frame.right() + gap; + + let align = Align2::CENTER_CENTER; + let rect = align.align_size_within_rect(size, frame); + + match sized.kind { + SizedAtomicKind::Text(galley) => { + ui.painter().galley(rect.min, galley, fallback_text_color); + } + SizedAtomicKind::Image(image, _) => { + image.paint_at(ui, rect); + } + SizedAtomicKind::Custom(id, _) => { + response.custom_rects.insert(id, rect); + } + SizedAtomicKind::Empty => {} + } + } + + response + } +} + +/// Response from a [`AtomicLayout::show`] or [`AllocatedAtomicLayout::paint`]. +/// +/// Use the `custom_rects` together with [`AtomicKind::Custom`] to add child widgets to a widget. +/// +/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible. +#[derive(Clone, Debug)] +pub struct AtomicLayoutResponse { + pub response: Response, + pub custom_rects: HashMap, +} + +impl Widget for AtomicLayout<'_> { + fn ui(self, ui: &mut Ui) -> Response { + self.show(ui).response + } +} diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs new file mode 100644 index 00000000000..9cf1e87b908 --- /dev/null +++ b/crates/egui/src/atomics/atomics.rs @@ -0,0 +1,118 @@ +use crate::{Atomic, AtomicKind}; + +/// A list of [`Atomic`]s. +#[derive(Clone, Debug, Default)] +pub struct Atomics<'a>(Vec>); + +impl<'a> Atomics<'a> { + pub fn push(&mut self, atomic: impl Into>) { + self.0.push(atomic.into()); + } + + pub fn push_front(&mut self, atomic: impl Into>) { + self.0.insert(0, atomic.into()); + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> impl Iterator> { + self.0.iter() + } + + pub fn iter_mut(&mut self) -> impl Iterator> { + self.0.iter_mut() + } + + /// Concatenate and return the text contents. + // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. + // in a submenu button there is a right text '⏵' which is now passed to the screen reader. + pub fn text(&self) -> Option { + let mut string: Option = None; + for atomic in &self.0 { + if let AtomicKind::Text(text) = &atomic.kind { + if let Some(string) = &mut string { + string.push(' '); + string.push_str(text.text()); + } else { + string = Some(text.text().to_owned()); + } + } + } + string + } +} + +impl<'a> IntoIterator for Atomics<'a> { + type Item = Atomic<'a>; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// Helper trait to convert a tuple of atomics into [`Atomics`]. +/// +/// ``` +/// use egui::{Atomics, Image, IntoAtomics, RichText}; +/// let atomics: Atomics = ( +/// "Some text", +/// RichText::new("Some RichText"), +/// Image::new("some_image_url"), +/// ).into_atomics(); +/// ``` +impl<'a, T> IntoAtomics<'a> for T +where + T: Into>, +{ + fn collect(self, atomics: &mut Atomics<'a>) { + atomics.push(self); + } +} + +pub trait IntoAtomics<'a> { + fn collect(self, atomics: &mut Atomics<'a>); + + fn into_atomics(self) -> Atomics<'a> + where + Self: Sized, + { + let mut atomics = Atomics(Vec::new()); + self.collect(&mut atomics); + atomics + } +} + +impl<'a> IntoAtomics<'a> for Atomics<'a> { + fn collect(self, atomics: &mut Self) { + atomics.0.extend(self.0); + } +} + +macro_rules! all_the_atomics { + ($($T:ident),*) => { + impl<'a, $($T),*> IntoAtomics<'a> for ($($T),*) + where + $($T: IntoAtomics<'a>),* + { + fn collect(self, _atomics: &mut Atomics<'a>) { + #[allow(non_snake_case)] + let ($($T),*) = self; + $($T.collect(_atomics);)* + } + } + }; +} + +all_the_atomics!(); +all_the_atomics!(T0, T1); +all_the_atomics!(T0, T1, T2); +all_the_atomics!(T0, T1, T2, T3); +all_the_atomics!(T0, T1, T2, T3, T4); +all_the_atomics!(T0, T1, T2, T3, T4, T5); diff --git a/crates/egui/src/atomics/mod.rs b/crates/egui/src/atomics/mod.rs new file mode 100644 index 00000000000..56c16d2d389 --- /dev/null +++ b/crates/egui/src/atomics/mod.rs @@ -0,0 +1,14 @@ +mod atomic; +mod atomic_kind; +mod atomic_layout; +#[allow(clippy::module_inception)] +mod atomics; +mod sized_atomic; +mod sized_atomic_kind; + +pub use atomic::*; +pub use atomic_kind::*; +pub use atomic_layout::*; +pub use atomics::*; +pub use sized_atomic::*; +pub use sized_atomic_kind::*; diff --git a/crates/egui/src/atomics/sized_atomic.rs b/crates/egui/src/atomics/sized_atomic.rs new file mode 100644 index 00000000000..37bd64dc704 --- /dev/null +++ b/crates/egui/src/atomics/sized_atomic.rs @@ -0,0 +1,11 @@ +use crate::SizedAtomicKind; +use emath::Vec2; + +/// A [`Atomic`] which has been sized. +#[derive(Clone, Debug)] +pub struct SizedAtomic<'a> { + pub grow: bool, + pub size: Vec2, + pub preferred_size: Vec2, + pub kind: SizedAtomicKind<'a>, +} diff --git a/crates/egui/src/atomics/sized_atomic_kind.rs b/crates/egui/src/atomics/sized_atomic_kind.rs new file mode 100644 index 00000000000..58bf7307629 --- /dev/null +++ b/crates/egui/src/atomics/sized_atomic_kind.rs @@ -0,0 +1,25 @@ +use crate::{Id, Image}; +use emath::Vec2; +use epaint::Galley; +use std::sync::Arc; + +/// A sized [`AtomicKind`]. +#[derive(Clone, Default, Debug)] +pub enum SizedAtomicKind<'a> { + #[default] + Empty, + Text(Arc), + Image(Image<'a>, Vec2), + Custom(Id, Vec2), +} + +impl SizedAtomicKind<'_> { + /// Get the calculated size. + pub fn size(&self) -> Vec2 { + match self { + SizedAtomicKind::Text(galley) => galley.size(), + SizedAtomicKind::Image(_, size) | SizedAtomicKind::Custom(_, size) => *size, + SizedAtomicKind::Empty => Vec2::ZERO, + } + } +} diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 31c54c01857..f65ced96c7d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -442,6 +442,7 @@ pub mod widget_text; pub mod widgets; mod atomic_layout; +mod atomics; #[cfg(feature = "callstack")] #[cfg(debug_assertions)] mod callstack; @@ -480,7 +481,7 @@ pub mod text { } pub use self::{ - atomic_layout::*, + atomics::*, containers::*, context::{Context, RepaintCause, RequestRepaintInfo}, data::{ From 2279b937b739ed8b1bd8be2f2c3d45b9f767d2ad Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 17:06:41 +0200 Subject: [PATCH 36/77] Fix left <-> right mixup --- crates/egui/src/atomics/atomic_layout.rs | 4 ++-- crates/egui/src/atomics/atomics.rs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index 8acdb3e56ca..a7b25897044 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -59,14 +59,14 @@ impl<'a> AtomicLayout<'a> { } } - /// Insert a new [`Atomic`] at the end of the list (left side). + /// Insert a new [`Atomic`] at the end of the list (right side). #[inline] pub fn push(mut self, atomic: impl Into>) -> Self { self.atomics.push(atomic.into()); self } - /// Insert a new [`Atomic`] at the beginning of the list (right side). + /// Insert a new [`Atomic`] at the beginning of the list (left side). #[inline] pub fn push_front(mut self, atomic: impl Into>) -> Self { self.atomics.push_front(atomic.into()); diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 9cf1e87b908..5d17407d45f 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -5,10 +5,12 @@ use crate::{Atomic, AtomicKind}; pub struct Atomics<'a>(Vec>); impl<'a> Atomics<'a> { + /// Insert a new [`Atomic`] at the end of the list (right side). pub fn push(&mut self, atomic: impl Into>) { self.0.push(atomic.into()); } + /// Insert a new [`Atomic`] at the beginning of the list (left side). pub fn push_front(&mut self, atomic: impl Into>) { self.0.insert(0, atomic.into()); } From 285d737d60a5a0c265a1f45fa957080ec9f813b0 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 17:07:39 +0200 Subject: [PATCH 37/77] replace matches --- crates/egui/src/atomics/atomic_layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index a7b25897044..0a59f898726 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -166,7 +166,7 @@ impl<'a> AtomicLayout<'a> { // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. // If none is found, mark the first text item as `shrink`. - if !matches!(wrap_mode, TextWrapMode::Extend) { + if wrap_mode != TextWrapMode::Extend { let any_shrink = atomics.iter().any(|a| a.shrink); if !any_shrink { let first_text = atomics From 59a3361490239c0b140f25132b1eb1fdc3ef8029 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Apr 2025 17:39:30 +0200 Subject: [PATCH 38/77] Small vec optimization --- Cargo.lock | 1 + Cargo.toml | 1 + crates/egui/Cargo.toml | 1 + crates/egui/src/atomics/atomic_layout.rs | 40 ++++++++++++++++++------ crates/egui/src/atomics/atomics.rs | 15 +++++++-- crates/egui/src/widgets/button.rs | 5 +-- crates/egui/src/widgets/checkbox.rs | 2 +- crates/egui/src/widgets/radio_button.rs | 2 +- 8 files changed, 49 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4411be0c6ac..336efb0489d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1285,6 +1285,7 @@ dependencies = [ "profiling", "ron", "serde", + "smallvec", "unicode-segmentation", ] diff --git a/Cargo.toml b/Cargo.toml index d272498f706..a132c3c0ab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ raw-window-handle = "0.6.0" ron = "0.8" serde = { version = "1", features = ["derive"] } similar-asserts = "1.4.2" +smallvec = "1" thiserror = "1.0.37" type-map = "0.5.0" unicode-segmentation = "1.12.0" diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index e7641ef315b..238a8d8ead9 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -87,6 +87,7 @@ ahash.workspace = true bitflags.workspace = true nohash-hasher.workspace = true profiling.workspace = true +smallvec.workspace = true unicode-segmentation.workspace = true #! ### Optional dependencies diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index 0a59f898726..cfb31da4a41 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -1,11 +1,12 @@ +use crate::atomics::ATOMICS_SMALL_VEC_SIZE; use crate::{ Atomic, AtomicKind, Atomics, FontSelection, Frame, Id, IntoAtomics, Response, Sense, SizedAtomic, SizedAtomicKind, Ui, Widget, }; -use ahash::{HashMap, HashMapExt}; use emath::{Align2, NumExt, Rect, Vec2}; use epaint::text::TextWrapMode; use epaint::Color32; +use smallvec::SmallVec; /// Intra-widget layout utility. /// @@ -193,7 +194,7 @@ impl<'a> AtomicLayout<'a> { let mut height: f32 = 0.0; - let mut sized_items = Vec::new(); + let mut sized_items = SmallVec::new(); let mut grow_count = 0; @@ -298,7 +299,7 @@ impl<'a> AtomicLayout<'a> { /// Instructions for painting an [`AtomicLayout`]. #[derive(Clone, Debug)] pub struct AllocatedAtomicLayout<'a> { - pub sized_atomics: Vec>, + pub sized_atomics: SmallVec<[SizedAtomic<'a>; ATOMICS_SMALL_VEC_SIZE]>, pub frame: Frame, pub fallback_text_color: Color32, pub response: Response, @@ -339,10 +340,7 @@ impl<'a> AllocatedAtomicLayout<'a> { let mut cursor = aligned_rect.left(); - let mut response = AtomicLayoutResponse { - response, - custom_rects: HashMap::new(), - }; + let mut response = AtomicLayoutResponse::empty(response); for sized in sized_items { let size = sized.size; @@ -364,7 +362,11 @@ impl<'a> AllocatedAtomicLayout<'a> { image.paint_at(ui, rect); } SizedAtomicKind::Custom(id, _) => { - response.custom_rects.insert(id, rect); + debug_assert!( + !response.custom_rects.iter().any(|(i, _)| *i == id), + "Duplicate custom id" + ); + response.custom_rects.push((id, rect)); } SizedAtomicKind::Empty => {} } @@ -382,7 +384,27 @@ impl<'a> AllocatedAtomicLayout<'a> { #[derive(Clone, Debug)] pub struct AtomicLayoutResponse { pub response: Response, - pub custom_rects: HashMap, + // There should rarely be more than one custom rect. + custom_rects: SmallVec<[(Id, Rect); 1]>, +} + +impl AtomicLayoutResponse { + pub fn empty(response: Response) -> Self { + Self { + response, + custom_rects: SmallVec::new(), + } + } + + pub fn custom_rects(&self) -> impl Iterator + '_ { + self.custom_rects.iter().copied() + } + + pub fn get_rect(&self, id: Id) -> Option { + self.custom_rects + .iter() + .find_map(|(i, r)| if *i == id { Some(*r) } else { None }) + } } impl Widget for AtomicLayout<'_> { diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 5d17407d45f..8dba1ec9245 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -1,10 +1,19 @@ use crate::{Atomic, AtomicKind}; +use smallvec::SmallVec; + +// Rarely there should be more than 2 atomics in one Widget. +// I guess it could happen in a menu button with Image and right text... +pub(crate) const ATOMICS_SMALL_VEC_SIZE: usize = 2; /// A list of [`Atomic`]s. #[derive(Clone, Debug, Default)] -pub struct Atomics<'a>(Vec>); +pub struct Atomics<'a>(SmallVec<[Atomic<'a>; ATOMICS_SMALL_VEC_SIZE]>); impl<'a> Atomics<'a> { + pub fn new(content: impl IntoAtomics<'a>) -> Self { + content.into_atomics() + } + /// Insert a new [`Atomic`] at the end of the list (right side). pub fn push(&mut self, atomic: impl Into>) { self.0.push(atomic.into()); @@ -52,7 +61,7 @@ impl<'a> Atomics<'a> { impl<'a> IntoIterator for Atomics<'a> { type Item = Atomic<'a>; - type IntoIter = std::vec::IntoIter; + type IntoIter = smallvec::IntoIter<[Atomic<'a>; ATOMICS_SMALL_VEC_SIZE]>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() @@ -85,7 +94,7 @@ pub trait IntoAtomics<'a> { where Self: Sized, { - let mut atomics = Atomics(Vec::new()); + let mut atomics = Atomics::default(); self.collect(&mut atomics); atomics } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 7939942f81e..e7225559b55 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -282,10 +282,7 @@ impl<'a> Button<'a> { prepared.paint(ui) } else { - AtomicLayoutResponse { - response: prepared.response, - custom_rects: Default::default(), - } + AtomicLayoutResponse::empty(prepared.response) }; response.response.widget_info(|| { diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 1e8aac16bac..1b0938d5c30 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -103,7 +103,7 @@ impl Widget for Checkbox<'_> { prepared.fallback_text_color = visuals.text_color(); let response = prepared.paint(ui); - if let Some(rect) = response.custom_rects.get(&rect_id).copied() { + if let Some(rect) = response.get_rect(rect_id) { let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); ui.painter().add(epaint::RectShape::new( big_icon_rect.expand(visuals.expansion), diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index c18c6452087..743be706265 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -80,7 +80,7 @@ impl<'a> Widget for RadioButton<'a> { prepared.fallback_text_color = visuals.text_color(); let response = prepared.paint(ui); - if let Some(rect) = response.custom_rects.get(&rect_id).copied() { + if let Some(rect) = response.get_rect(rect_id) { let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); let painter = ui.painter(); From 8485ec430f7c4223522026ce04fd61a26e0c4772 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Fri, 25 Apr 2025 10:07:51 +0200 Subject: [PATCH 39/77] Some fixes after review --- crates/egui/src/atomic_layout.rs | 1 - crates/egui/src/atomics/atomic.rs | 2 +- crates/egui/src/lib.rs | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 crates/egui/src/atomic_layout.rs diff --git a/crates/egui/src/atomic_layout.rs b/crates/egui/src/atomic_layout.rs deleted file mode 100644 index 8b137891791..00000000000 --- a/crates/egui/src/atomic_layout.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index 23f1f8cfe6a..e205199ba87 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -9,7 +9,7 @@ use epaint::Fonts; /// You can directly call the `a_*` methods on anything that implements `Into`. /// ``` /// # use egui::{Image, emath::Vec2}; -/// use egui::AtomicExt; +/// use egui::AtomicExt as _; /// let string_atomic = "Hello".a_grow(true); /// let image_atomic = Image::new("some_image_url").a_size(Vec2::splat(20.0)); /// ``` diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 4572b5675e2..11afb321ff8 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -441,7 +441,6 @@ mod widget_rect; pub mod widget_text; pub mod widgets; -mod atomic_layout; mod atomics; #[cfg(feature = "callstack")] #[cfg(debug_assertions)] From ef19ccd27a9ac89fa58ce834563d68e604f01dde Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Fri, 25 Apr 2025 12:29:21 +0200 Subject: [PATCH 40/77] Rename a_* to atom_* --- crates/egui/src/atomics/atomic.rs | 17 ++++++++--------- tests/egui_tests/tests/test_widgets.rs | 5 +++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index e205199ba87..5fce3c6b0c6 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -10,8 +10,8 @@ use epaint::Fonts; /// ``` /// # use egui::{Image, emath::Vec2}; /// use egui::AtomicExt as _; -/// let string_atomic = "Hello".a_grow(true); -/// let image_atomic = Image::new("some_image_url").a_size(Vec2::splat(20.0)); +/// let string_atomic = "Hello".atom_grow(true); +/// let image_atomic = Image::new("some_image_url").atom_size(Vec2::splat(20.0)); /// ``` #[derive(Clone, Debug)] pub struct Atomic<'a> { @@ -69,35 +69,34 @@ impl<'a> Atomic<'a> { /// A trait for conveniently building [`Atomic`]s. pub trait AtomicExt<'a> { /// Set the atomic to a fixed size. - fn a_size(self, size: Vec2) -> Atomic<'a>; + fn atom_size(self, size: Vec2) -> Atomic<'a>; /// Grow this atomic to the available space. - fn a_grow(self, grow: bool) -> Atomic<'a>; + fn atom_grow(self, grow: bool) -> Atomic<'a>; /// Shrink this atomic if there isn't enough space. /// /// NOTE: Only a single [`Atomic`] may shrink for each widget. - fn a_shrink(self, shrink: bool) -> Atomic<'a>; + fn atom_shrink(self, shrink: bool) -> Atomic<'a>; } impl<'a, T> AtomicExt<'a> for T where T: Into> + Sized, { - fn a_size(self, size: Vec2) -> Atomic<'a> { + fn atom_size(self, size: Vec2) -> Atomic<'a> { let mut atomic = self.into(); atomic.size = Some(size); atomic } - fn a_grow(self, grow: bool) -> Atomic<'a> { + fn atom_grow(self, grow: bool) -> Atomic<'a> { let mut atomic = self.into(); atomic.grow = grow; atomic } - /// NOTE: Only a single atomic may be marked as shrink - fn a_shrink(self, shrink: bool) -> Atomic<'a> { + fn atom_shrink(self, shrink: bool) -> Atomic<'a> { let mut atomic = self.into(); atomic.shrink = shrink; atomic diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index c56ec8839ca..b3c2b253bbf 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -2,7 +2,8 @@ use egui::load::SizedTexture; use egui::{ include_image, Align, AtomicExt, AtomicLayout, Button, Color32, ColorImage, Direction, DragValue, Event, Grid, Image, IntoAtomics, Layout, PointerButton, Pos2, Response, Slider, - Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, + Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, + Widget as _, }; use egui_kittest::kittest::{by, Node, Queryable as _}; use egui_kittest::{Harness, SnapshotResult, SnapshotResults}; @@ -102,7 +103,7 @@ fn widget_tests() { ), ( "multi_grow", - ("g".a_grow(true), "2", "g".a_grow(true), "4").into_atomics(), + ("g".atom_grow(true), "2", "g".atom_grow(true), "4").into_atomics(), ), ]; From eb9ce42ee9bb98de5e4226020104400ae79f0b4a Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Fri, 25 Apr 2025 12:44:54 +0200 Subject: [PATCH 41/77] Rename front/back to left/right and impl deref instead of duplicating iters --- crates/egui/src/atomics/atomic_layout.rs | 48 +++++++++++++++--------- crates/egui/src/atomics/atomics.rs | 37 +++++++++--------- crates/egui/src/widgets/button.rs | 16 ++++---- crates/egui/src/widgets/checkbox.rs | 6 +-- crates/egui/src/widgets/radio_button.rs | 2 +- 5 files changed, 61 insertions(+), 48 deletions(-) diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index cfb31da4a41..b0f729267f3 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -1,16 +1,17 @@ use crate::atomics::ATOMICS_SMALL_VEC_SIZE; use crate::{ - Atomic, AtomicKind, Atomics, FontSelection, Frame, Id, IntoAtomics, Response, Sense, - SizedAtomic, SizedAtomicKind, Ui, Widget, + AtomicKind, Atomics, FontSelection, Frame, Id, IntoAtomics, Response, Sense, SizedAtomic, + SizedAtomicKind, Ui, Widget, }; use emath::{Align2, NumExt, Rect, Vec2}; use epaint::text::TextWrapMode; use epaint::Color32; use smallvec::SmallVec; +use std::ops::{Deref, DerefMut}; /// Intra-widget layout utility. /// -/// Used to lay out and paint [`Atomic`]s. +/// Used to lay out and paint [`crate::Atomic`]s. /// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`]. /// You can use it to make your own widgets. /// @@ -60,20 +61,6 @@ impl<'a> AtomicLayout<'a> { } } - /// Insert a new [`Atomic`] at the end of the list (right side). - #[inline] - pub fn push(mut self, atomic: impl Into>) -> Self { - self.atomics.push(atomic.into()); - self - } - - /// Insert a new [`Atomic`] at the beginning of the list (left side). - #[inline] - pub fn push_front(mut self, atomic: impl Into>) -> Self { - self.atomics.push_front(atomic.into()); - self - } - /// Set the gap between atomics. /// /// Default: `Spacing::icon_spacing` @@ -412,3 +399,30 @@ impl Widget for AtomicLayout<'_> { self.show(ui).response } } + +impl<'a> Deref for AtomicLayout<'a> { + type Target = Atomics<'a>; + fn deref(&self) -> &Self::Target { + &self.atomics + } +} + +impl<'a> DerefMut for AtomicLayout<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.atomics + } +} + +impl<'a> Deref for AllocatedAtomicLayout<'a> { + type Target = [SizedAtomic<'a>]; + + fn deref(&self) -> &Self::Target { + &self.sized_atomics + } +} + +impl<'a> DerefMut for AllocatedAtomicLayout<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sized_atomics + } +} diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 8dba1ec9245..7adbf597782 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -1,5 +1,6 @@ use crate::{Atomic, AtomicKind}; use smallvec::SmallVec; +use std::ops::{Deref, DerefMut}; // Rarely there should be more than 2 atomics in one Widget. // I guess it could happen in a menu button with Image and right text... @@ -15,31 +16,15 @@ impl<'a> Atomics<'a> { } /// Insert a new [`Atomic`] at the end of the list (right side). - pub fn push(&mut self, atomic: impl Into>) { + pub fn push_left(&mut self, atomic: impl Into>) { self.0.push(atomic.into()); } /// Insert a new [`Atomic`] at the beginning of the list (left side). - pub fn push_front(&mut self, atomic: impl Into>) { + pub fn push_right(&mut self, atomic: impl Into>) { self.0.insert(0, atomic.into()); } - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn iter(&self) -> impl Iterator> { - self.0.iter() - } - - pub fn iter_mut(&mut self) -> impl Iterator> { - self.0.iter_mut() - } - /// Concatenate and return the text contents. // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. // in a submenu button there is a right text '⏵' which is now passed to the screen reader. @@ -83,7 +68,7 @@ where T: Into>, { fn collect(self, atomics: &mut Atomics<'a>) { - atomics.push(self); + atomics.push_left(self); } } @@ -127,3 +112,17 @@ all_the_atomics!(T0, T1, T2); all_the_atomics!(T0, T1, T2, T3); all_the_atomics!(T0, T1, T2, T3, T4); all_the_atomics!(T0, T1, T2, T3, T4, T5); + +impl<'a> Deref for Atomics<'a> { + type Target = [Atomic<'a>]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> DerefMut for Atomics<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 1843f53bec4..3f79c51fe90 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ Atomic, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, CornerRadius, Frame, - Image, IntoAtomics, NumExt as _, Response, Sense, SizedAtomicKind, Stroke, TextWrapMode, Ui, Vec2, - Widget, WidgetInfo, WidgetText, WidgetType, + Image, IntoAtomics, NumExt as _, Response, Sense, SizedAtomicKind, Stroke, TextWrapMode, Ui, + Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -67,10 +67,10 @@ impl<'a> Button<'a> { pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { - button.atomics.push(image); + button.atomics.push_left(image); } if let Some(text) = text { - button.atomics.push(text); + button.atomics.push_left(text); } button } @@ -186,16 +186,16 @@ impl<'a> Button<'a> { AtomicKind::Text(text) => AtomicKind::Text(text.weak()), other => other, }; - self.atomics.push(Atomic::grow()); - self.atomics.push(atomic); + self.atomics.push_left(Atomic::grow()); + self.atomics.push_left(atomic); self } /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl Into>) -> Self { - self.atomics.push(Atomic::grow()); - self.atomics.push(right_text.into()); + self.atomics.push_left(Atomic::grow()); + self.atomics.push_left(right_text.into()); self } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 43244757386..3e361192dd5 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,7 +1,7 @@ use crate::AtomicKind::Custom; use crate::{ - epaint, pos2, AtomicLayout, Atomics, Id, IntoAtomics, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget, - WidgetInfo, WidgetType, + epaint, pos2, AtomicLayout, Atomics, Id, IntoAtomics, NumExt as _, Response, Sense, Shape, Ui, + Vec2, Widget, WidgetInfo, WidgetType, }; // TODO(emilk): allow checkbox without a text label @@ -66,7 +66,7 @@ impl Widget for Checkbox<'_> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::checkbox"); - atomics.push_front(Custom(rect_id, icon_size)); + atomics.push_right(Custom(rect_id, icon_size)); let text = atomics.text(); diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index 2b1c947a243..443a1b9cf8b 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -54,7 +54,7 @@ impl<'a> Widget for RadioButton<'a> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::radio_button"); - atomics.push_front(AtomicKind::Custom(rect_id, icon_size)); + atomics.push_right(AtomicKind::Custom(rect_id, icon_size)); let text = atomics.text(); From 349c66b524a48ce88f67f227701e82f553e75dce Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 11:51:43 +0200 Subject: [PATCH 42/77] Clippy fixes --- crates/egui/src/atomics/atomic_kind.rs | 4 ++-- crates/egui/src/atomics/atomic_layout.rs | 8 ++++---- crates/egui/src/atomics/atomics.rs | 4 ++-- crates/egui/src/atomics/mod.rs | 2 +- crates/egui/src/widgets/radio_button.rs | 2 +- tests/egui_tests/tests/test_widgets.rs | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index d233db5e3f8..0c458d077e5 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -25,7 +25,7 @@ pub enum AtomicKind<'a> { /// let id = Id::new("my_button"); /// let response = Button::new(("Hi!", AtomicKind::Custom(id, Vec2::splat(18.0)))).atomic_ui(ui); /// - /// let rect = response.custom_rects.get(&id); + /// let rect = response.get_rect(id); /// if let Some(rect) = rect { /// ui.put(*rect, Button::new("⏵")); /// } @@ -95,7 +95,7 @@ impl<'a> From> for AtomicKind<'a> { } } -impl<'a, T> From for AtomicKind<'a> +impl From for AtomicKind<'_> where T: Into, { diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index b0f729267f3..53d050d8838 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -3,7 +3,7 @@ use crate::{ AtomicKind, Atomics, FontSelection, Frame, Id, IntoAtomics, Response, Sense, SizedAtomic, SizedAtomicKind, Ui, Widget, }; -use emath::{Align2, NumExt, Rect, Vec2}; +use emath::{Align2, NumExt as _, Rect, Vec2}; use epaint::text::TextWrapMode; use epaint::Color32; use smallvec::SmallVec; @@ -297,7 +297,7 @@ pub struct AllocatedAtomicLayout<'a> { gap: f32, } -impl<'a> AllocatedAtomicLayout<'a> { +impl AllocatedAtomicLayout<'_> { /// Paint the [`Frame`] and individual [`Atomic`]s. pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { let Self { @@ -407,7 +407,7 @@ impl<'a> Deref for AtomicLayout<'a> { } } -impl<'a> DerefMut for AtomicLayout<'a> { +impl DerefMut for AtomicLayout<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.atomics } @@ -421,7 +421,7 @@ impl<'a> Deref for AllocatedAtomicLayout<'a> { } } -impl<'a> DerefMut for AllocatedAtomicLayout<'a> { +impl DerefMut for AllocatedAtomicLayout<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.sized_atomics } diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 7adbf597782..019194b2f7a 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -98,7 +98,7 @@ macro_rules! all_the_atomics { $($T: IntoAtomics<'a>),* { fn collect(self, _atomics: &mut Atomics<'a>) { - #[allow(non_snake_case)] + #[expect(non_snake_case)] let ($($T),*) = self; $($T.collect(_atomics);)* } @@ -121,7 +121,7 @@ impl<'a> Deref for Atomics<'a> { } } -impl<'a> DerefMut for Atomics<'a> { +impl DerefMut for Atomics<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } diff --git a/crates/egui/src/atomics/mod.rs b/crates/egui/src/atomics/mod.rs index 56c16d2d389..8862029b787 100644 --- a/crates/egui/src/atomics/mod.rs +++ b/crates/egui/src/atomics/mod.rs @@ -1,7 +1,7 @@ mod atomic; mod atomic_kind; mod atomic_layout; -#[allow(clippy::module_inception)] +#[expect(clippy::module_inception)] mod atomics; mod sized_atomic; mod sized_atomic_kind; diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index 443a1b9cf8b..cab8f834cf8 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -37,7 +37,7 @@ impl<'a> RadioButton<'a> { } } -impl<'a> Widget for RadioButton<'a> { +impl Widget for RadioButton<'_> { fn ui(self, ui: &mut Ui) -> Response { let Self { checked, diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index b3c2b253bbf..37f18defd3c 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,7 +1,7 @@ use egui::load::SizedTexture; use egui::{ - include_image, Align, AtomicExt, AtomicLayout, Button, Color32, ColorImage, Direction, - DragValue, Event, Grid, Image, IntoAtomics, Layout, PointerButton, Pos2, Response, Slider, + include_image, Align, AtomicExt as _, AtomicLayout, Button, Color32, ColorImage, Direction, + DragValue, Event, Grid, Image, IntoAtomics as _, Layout, PointerButton, Pos2, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, }; From f1e66e16ea524b304f6d96f4b39ed97d00615f86 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 12:13:04 +0200 Subject: [PATCH 43/77] Clippy --- crates/egui/src/atomics/atomics.rs | 3 ++- crates/egui_extras/src/syntax_highlighting.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 019194b2f7a..1bf382de6c2 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -98,7 +98,8 @@ macro_rules! all_the_atomics { $($T: IntoAtomics<'a>),* { fn collect(self, _atomics: &mut Atomics<'a>) { - #[expect(non_snake_case)] + #[allow(clippy::allow_attributes)] + #[allow(non_snake_case)] let ($($T),*) = self; $($T.collect(_atomics);)* } diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 6bb51184f12..8d688ae8ede 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -511,7 +511,7 @@ struct Highlighter {} #[cfg(not(feature = "syntect"))] impl Highlighter { - #[expect(clippy::unused_self, clippy::unnecessary_wraps)] + #[expect(clippy::unused_self)] fn highlight_impl( &self, theme: &CodeTheme, From 44729b0470347992f19ad2aa3327a09d5ec9108b Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 12:16:51 +0200 Subject: [PATCH 44/77] Lint --- crates/egui/src/atomics/atomic_layout.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index 53d050d8838..3cd4bbe50ac 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -402,6 +402,7 @@ impl Widget for AtomicLayout<'_> { impl<'a> Deref for AtomicLayout<'a> { type Target = Atomics<'a>; + fn deref(&self) -> &Self::Target { &self.atomics } From f3cc4d29aa0eaa0b93049d2e9c6d38a31855a1f7 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 12:22:20 +0200 Subject: [PATCH 45/77] Move AtomicExt --- crates/egui/src/atomics/atomic.rs | 37 ------------------------- crates/egui/src/atomics/atomic_ext.rs | 39 +++++++++++++++++++++++++++ crates/egui/src/atomics/mod.rs | 2 ++ 3 files changed, 41 insertions(+), 37 deletions(-) create mode 100644 crates/egui/src/atomics/atomic_ext.rs diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index 5fce3c6b0c6..5e109e3023a 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -66,43 +66,6 @@ impl<'a> Atomic<'a> { } } -/// A trait for conveniently building [`Atomic`]s. -pub trait AtomicExt<'a> { - /// Set the atomic to a fixed size. - fn atom_size(self, size: Vec2) -> Atomic<'a>; - - /// Grow this atomic to the available space. - fn atom_grow(self, grow: bool) -> Atomic<'a>; - - /// Shrink this atomic if there isn't enough space. - /// - /// NOTE: Only a single [`Atomic`] may shrink for each widget. - fn atom_shrink(self, shrink: bool) -> Atomic<'a>; -} - -impl<'a, T> AtomicExt<'a> for T -where - T: Into> + Sized, -{ - fn atom_size(self, size: Vec2) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.size = Some(size); - atomic - } - - fn atom_grow(self, grow: bool) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.grow = grow; - atomic - } - - fn atom_shrink(self, shrink: bool) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.shrink = shrink; - atomic - } -} - impl<'a, T> From for Atomic<'a> where T: Into>, diff --git a/crates/egui/src/atomics/atomic_ext.rs b/crates/egui/src/atomics/atomic_ext.rs new file mode 100644 index 00000000000..fad8fdcf037 --- /dev/null +++ b/crates/egui/src/atomics/atomic_ext.rs @@ -0,0 +1,39 @@ +use crate::Atomic; +use emath::Vec2; + +/// A trait for conveniently building [`Atomic`]s. +pub trait AtomicExt<'a> { + /// Set the atomic to a fixed size. + fn atom_size(self, size: Vec2) -> Atomic<'a>; + + /// Grow this atomic to the available space. + fn atom_grow(self, grow: bool) -> Atomic<'a>; + + /// Shrink this atomic if there isn't enough space. + /// + /// NOTE: Only a single [`Atomic`] may shrink for each widget. + fn atom_shrink(self, shrink: bool) -> Atomic<'a>; +} + +impl<'a, T> AtomicExt<'a> for T +where + T: Into> + Sized, +{ + fn atom_size(self, size: Vec2) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.size = Some(size); + atomic + } + + fn atom_grow(self, grow: bool) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.grow = grow; + atomic + } + + fn atom_shrink(self, shrink: bool) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.shrink = shrink; + atomic + } +} diff --git a/crates/egui/src/atomics/mod.rs b/crates/egui/src/atomics/mod.rs index 8862029b787..f44223a60d8 100644 --- a/crates/egui/src/atomics/mod.rs +++ b/crates/egui/src/atomics/mod.rs @@ -1,4 +1,5 @@ mod atomic; +mod atomic_ext; mod atomic_kind; mod atomic_layout; #[expect(clippy::module_inception)] @@ -7,6 +8,7 @@ mod sized_atomic; mod sized_atomic_kind; pub use atomic::*; +pub use atomic_ext::*; pub use atomic_kind::*; pub use atomic_layout::*; pub use atomics::*; From 545f533de19240dd1faeb03d6ce4faa74d498bc8 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 12:27:07 +0200 Subject: [PATCH 46/77] Add comment explaining how atomic size relates to grow / shrink --- crates/egui/src/atomics/atomic_ext.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/egui/src/atomics/atomic_ext.rs b/crates/egui/src/atomics/atomic_ext.rs index fad8fdcf037..e7995932546 100644 --- a/crates/egui/src/atomics/atomic_ext.rs +++ b/crates/egui/src/atomics/atomic_ext.rs @@ -4,6 +4,10 @@ use emath::Vec2; /// A trait for conveniently building [`Atomic`]s. pub trait AtomicExt<'a> { /// Set the atomic to a fixed size. + /// + /// If [`Atomic::grow`] is `true`, this will be the minimum width. + /// If [`Atomic::shrink`] is `true`, this will be the maximum width. + /// If both are true, the width will have no effect. fn atom_size(self, size: Vec2) -> Atomic<'a>; /// Grow this atomic to the available space. From b90f01b83ffcc6dc03aa98db075d5b8dacbb7cd4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 13:40:47 +0200 Subject: [PATCH 47/77] Explain how truncation works --- crates/egui/src/atomics/atomic.rs | 5 ++++- crates/egui/src/atomics/atomic_kind.rs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index 5e109e3023a..512c115ef53 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -52,8 +52,11 @@ impl<'a> Atomic<'a> { ui: &Ui, available_size: Vec2, font_size: f32, - wrap_mode: Option, + mut wrap_mode: Option, ) -> SizedAtomic<'a> { + if !self.shrink { + wrap_mode = Some(TextWrapMode::Extend); + } let (preferred, kind) = self .kind .into_sized(ui, available_size, font_size, wrap_mode); diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index 0c458d077e5..be03d5e917e 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -9,7 +9,23 @@ pub enum AtomicKind<'a> { /// Empty, that can be used with [`AtomicExt::a_grow`] to reserve space. #[default] Empty, + + /// Text atomic. + /// + /// Truncation within [`crate::AtomicLayout`] works like this: + /// - + /// - if `wrap_mode` is not Extend + /// - if no atomic is `shrink` + /// - the first text atomic is selected and will be marked as `shrink` + /// - the atomic marked as `shrink` will shrink / wrap based on the selected wrap mode + /// - any other text atomics will have `wrap_mode` extend + /// - if `wrap_mode` is extend, Text will extend as expected. + /// + /// Generally, `wrap_mode` should only be set via [`crate::Style`] or + /// [`crate::AtomicLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atomic + /// that is not `shrink` will have unexpected results. Text(WidgetText), + Image(Image<'a>), /// For custom rendering. From ca5027ddb6a9a08c70ad33708ec948acf8783626 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 14:37:56 +0200 Subject: [PATCH 48/77] Add iterator utilities --- crates/egui/src/atomics/atomic_layout.rs | 79 ++++++++++++++++++++-- crates/egui/src/atomics/atomics.rs | 85 +++++++++++++++++++++++- crates/egui/src/widgets/button.rs | 9 +-- 3 files changed, 160 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index 3cd4bbe50ac..a33a55d0564 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -1,13 +1,14 @@ use crate::atomics::ATOMICS_SMALL_VEC_SIZE; use crate::{ - AtomicKind, Atomics, FontSelection, Frame, Id, IntoAtomics, Response, Sense, SizedAtomic, - SizedAtomicKind, Ui, Widget, + AtomicKind, Atomics, FontSelection, Frame, Id, Image, IntoAtomics, Response, Sense, + SizedAtomic, SizedAtomicKind, Ui, Widget, }; use emath::{Align2, NumExt as _, Rect, Vec2}; use epaint::text::TextWrapMode; -use epaint::Color32; +use epaint::{Color32, Galley}; use smallvec::SmallVec; use std::ops::{Deref, DerefMut}; +use std::sync::Arc; /// Intra-widget layout utility. /// @@ -297,7 +298,77 @@ pub struct AllocatedAtomicLayout<'a> { gap: f32, } -impl AllocatedAtomicLayout<'_> { +impl<'atomic> AllocatedAtomicLayout<'atomic> { + pub fn iter_kinds(&self) -> impl Iterator> { + self.sized_atomics.iter().map(|atomic| &atomic.kind) + } + + pub fn iter_kinds_mut(&mut self) -> impl Iterator> { + self.sized_atomics.iter_mut().map(|atomic| &mut atomic.kind) + } + + pub fn iter_images(&self) -> impl Iterator> { + self.iter_kinds().filter_map(|kind| { + if let SizedAtomicKind::Image(image, _) = kind { + Some(image) + } else { + None + } + }) + } + + pub fn iter_images_mut(&mut self) -> impl Iterator> { + self.iter_kinds_mut().filter_map(|kind| { + if let SizedAtomicKind::Image(image, _) = kind { + Some(image) + } else { + None + } + }) + } + + pub fn iter_texts(&self) -> impl Iterator> + use<'atomic, '_> { + self.iter_kinds().filter_map(|kind| { + if let SizedAtomicKind::Text(text) = kind { + Some(text) + } else { + None + } + }) + } + + pub fn iter_texts_mut(&mut self) -> impl Iterator> + use<'atomic, '_> { + self.iter_kinds_mut().filter_map(|kind| { + if let SizedAtomicKind::Text(text) = kind { + Some(text) + } else { + None + } + }) + } + + pub fn map_kind(&mut self, mut f: F) + where + F: FnMut(SizedAtomicKind<'atomic>) -> SizedAtomicKind<'atomic>, + { + for kind in self.iter_kinds_mut() { + *kind = f(std::mem::take(kind)); + } + } + + pub fn map_images(&mut self, mut f: F) + where + F: FnMut(Image<'atomic>) -> Image<'atomic>, + { + self.map_kind(|kind| { + if let SizedAtomicKind::Image(image, size) = kind { + SizedAtomicKind::Image(f(image), size) + } else { + kind + } + }); + } + /// Paint the [`Frame`] and individual [`Atomic`]s. pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { let Self { diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 1bf382de6c2..0acab7b77d3 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -1,4 +1,4 @@ -use crate::{Atomic, AtomicKind}; +use crate::{Atomic, AtomicKind, Image, WidgetText}; use smallvec::SmallVec; use std::ops::{Deref, DerefMut}; @@ -42,6 +42,89 @@ impl<'a> Atomics<'a> { } string } + + pub fn iter_kinds(&'a self) -> impl Iterator> { + self.0.iter().map(|atomic| &atomic.kind) + } + + pub fn iter_kinds_mut(&'a mut self) -> impl Iterator> { + self.0.iter_mut().map(|atomic| &mut atomic.kind) + } + + pub fn iter_images(&'a self) -> impl Iterator> { + self.iter_kinds().filter_map(|kind| { + if let AtomicKind::Image(image) = kind { + Some(image) + } else { + None + } + }) + } + + pub fn iter_images_mut(&'a mut self) -> impl Iterator> { + self.iter_kinds_mut().filter_map(|kind| { + if let AtomicKind::Image(image) = kind { + Some(image) + } else { + None + } + }) + } + + pub fn iter_texts(&'a self) -> impl Iterator { + self.iter_kinds().filter_map(|kind| { + if let AtomicKind::Text(text) = kind { + Some(text) + } else { + None + } + }) + } + + pub fn iter_texts_mut(&'a mut self) -> impl Iterator { + self.iter_kinds_mut().filter_map(|kind| { + if let AtomicKind::Text(text) = kind { + Some(text) + } else { + None + } + }) + } + + pub fn map_kind(&'a mut self, mut f: F) + where + F: FnMut(AtomicKind<'a>) -> AtomicKind<'a>, + { + for kind in self.iter_kinds_mut() { + *kind = f(std::mem::take(kind)); + } + } + + pub fn map_images(&'a mut self, mut f: F) + where + F: FnMut(Image<'a>) -> Image<'a>, + { + self.map_kind(|kind| { + if let AtomicKind::Image(image) = kind { + AtomicKind::Image(f(image)) + } else { + kind + } + }); + } + + pub fn map_texts(&'a mut self, mut f: F) + where + F: FnMut(WidgetText) -> WidgetText, + { + self.map_kind(|kind| { + if let AtomicKind::Text(text) = kind { + AtomicKind::Text(f(text)) + } else { + kind + } + }); + } } impl<'a> IntoIterator for Atomics<'a> { diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 3f79c51fe90..1b709762d08 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -252,14 +252,7 @@ impl<'a> Button<'a> { let visuals = ui.style().interact_selectable(&prepared.response, selected); if image_tint_follows_text_color { - prepared.sized_atomics.iter_mut().for_each(|a| { - a.kind = match std::mem::take(&mut a.kind) { - SizedAtomicKind::Image(image, size) => { - SizedAtomicKind::Image(image.tint(visuals.text_color()), size) - } - other => other, - } - }); + prepared.map_images(|image| image.tint(visuals.text_color())); } prepared.fallback_text_color = visuals.text_color(); From 2eb39d879b59267659c7c1d5d9819b638ab2cb1c Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 14:54:46 +0200 Subject: [PATCH 49/77] Remove default font size image height behavior from AtomicLayout and implement it as fallback for old Button::image methods instead --- crates/egui/src/atomics/atomic.rs | 42 +++++++++++------------- crates/egui/src/atomics/atomic_ext.rs | 42 +++++++++++++++++++++++- crates/egui/src/atomics/atomic_kind.rs | 13 +++++--- crates/egui/src/atomics/atomic_layout.rs | 23 +++---------- crates/egui/src/atomics/atomics.rs | 4 +++ crates/egui/src/widgets/button.rs | 34 ++++++++++++++++--- examples/hello_world/src/main.rs | 9 +++-- 7 files changed, 112 insertions(+), 55 deletions(-) diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index 512c115ef53..2fe8167ce02 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -1,7 +1,6 @@ -use crate::{AtomicKind, SizedAtomic, Style, Ui}; +use crate::{AtomicKind, SizedAtomic, Ui}; use emath::Vec2; use epaint::text::TextWrapMode; -use epaint::Fonts; /// A low-level ui building block. /// @@ -16,34 +15,34 @@ use epaint::Fonts; #[derive(Clone, Debug)] pub struct Atomic<'a> { pub size: Option, + pub max_size: Vec2, pub grow: bool, pub shrink: bool, pub kind: AtomicKind<'a>, } -impl<'a> Atomic<'a> { - /// Create an empty [`Atomic`] marked as `grow`. - pub fn grow() -> Self { +impl Default for Atomic<'_> { + fn default() -> Self { Atomic { size: None, - grow: true, + max_size: Vec2::INFINITY, + grow: false, shrink: false, kind: AtomicKind::Empty, } } +} - /// Heuristic to find the best height for an image. - /// Basically returns the height if this is not an [`Image`]. - pub(crate) fn get_min_height_for_image(&self, fonts: &Fonts, style: &Style) -> Option { - self.size.map(|s| s.y).or_else(|| { - match &self.kind { - AtomicKind::Text(text) => Some(text.font_height(fonts, style)), - AtomicKind::Custom(_, size) => Some(size.y), - // Since this method is used to calculate the best height for an image, we always return - // None for images. - AtomicKind::Empty | AtomicKind::Image(_) => None, - } - }) +impl<'a> Atomic<'a> { + /// Create an empty [`Atomic`] marked as `grow`. + /// + /// This will expand in size, allowing all proceeding atomics to be left-aligned, + /// and all following atomics to be right-aligned + pub fn grow() -> Self { + Atomic { + grow: true, + ..Default::default() + } } /// Turn this into a [`SizedAtomic`]. @@ -51,7 +50,6 @@ impl<'a> Atomic<'a> { self, ui: &Ui, available_size: Vec2, - font_size: f32, mut wrap_mode: Option, ) -> SizedAtomic<'a> { if !self.shrink { @@ -59,7 +57,7 @@ impl<'a> Atomic<'a> { } let (preferred, kind) = self .kind - .into_sized(ui, available_size, font_size, wrap_mode); + .into_sized(ui, available_size, self.max_size, wrap_mode); SizedAtomic { size: self.size.unwrap_or_else(|| kind.size()), preferred_size: preferred, @@ -75,10 +73,8 @@ where { fn from(value: T) -> Self { Atomic { - size: None, - grow: false, - shrink: false, kind: value.into(), + ..Default::default() } } } diff --git a/crates/egui/src/atomics/atomic_ext.rs b/crates/egui/src/atomics/atomic_ext.rs index e7995932546..54f131f5f17 100644 --- a/crates/egui/src/atomics/atomic_ext.rs +++ b/crates/egui/src/atomics/atomic_ext.rs @@ -1,4 +1,4 @@ -use crate::Atomic; +use crate::{Atomic, FontSelection, Ui}; use emath::Vec2; /// A trait for conveniently building [`Atomic`]s. @@ -17,6 +17,28 @@ pub trait AtomicExt<'a> { /// /// NOTE: Only a single [`Atomic`] may shrink for each widget. fn atom_shrink(self, shrink: bool) -> Atomic<'a>; + + /// Set the maximum size of this atomic. + fn atom_max_size(self, max_size: Vec2) -> Atomic<'a>; + + /// Set the maximum width of this atomic. + fn atom_max_width(self, max_width: f32) -> Atomic<'a>; + + /// Set the maximum height of this atomic. + fn atom_max_height(self, max_height: f32) -> Atomic<'a>; + + /// Set the max height of this atomic to match the font size. + /// + /// This is useful for e.g. limiting the height of icons in buttons. + fn atom_max_height_font_size(self, ui: &Ui) -> Atomic<'a> + where + Self: Sized, + { + let font_selection = FontSelection::default(); + let font_id = font_selection.resolve(ui.style()); + let height = ui.fonts(|f| f.row_height(&font_id)); + self.atom_max_height(height) + } } impl<'a, T> AtomicExt<'a> for T @@ -40,4 +62,22 @@ where atomic.shrink = shrink; atomic } + + fn atom_max_size(self, max_size: Vec2) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.max_size = max_size; + atomic + } + + fn atom_max_width(self, max_width: f32) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.max_size.x = max_width; + atomic + } + + fn atom_max_height(self, max_height: f32) -> Atomic<'a> { + let mut atomic = self.into(); + atomic.max_size.y = max_height; + atomic + } } diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index be03d5e917e..1516b43970d 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -1,4 +1,4 @@ -use crate::{Id, Image, SizedAtomicKind, TextStyle, Ui, WidgetText}; +use crate::{Id, Image, ImageSource, SizedAtomicKind, TextStyle, Ui, WidgetText}; use emath::Vec2; use epaint::text::TextWrapMode; use std::fmt::Formatter; @@ -82,7 +82,7 @@ impl<'a> AtomicKind<'a> { self, ui: &Ui, available_size: Vec2, - font_size: f32, + max_size: Vec2, wrap_mode: Option, ) -> (Vec2, SizedAtomicKind<'a>) { match self { @@ -94,9 +94,8 @@ impl<'a> AtomicKind<'a> { ) } AtomicKind::Image(image) => { - let max_size = Vec2::splat(font_size); let size = image.load_and_calc_size(ui, Vec2::min(available_size, max_size)); - let size = size.unwrap_or(max_size); + let size = size.unwrap_or(Vec2::ZERO); (size, SizedAtomicKind::Image(image, size)) } AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), @@ -105,6 +104,12 @@ impl<'a> AtomicKind<'a> { } } +impl<'a> From> for AtomicKind<'a> { + fn from(value: ImageSource<'a>) -> Self { + AtomicKind::Image(value.into()) + } +} + impl<'a> From> for AtomicKind<'a> { fn from(value: Image<'a>) -> Self { AtomicKind::Image(value) diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index a33a55d0564..ae156e66598 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -1,7 +1,7 @@ use crate::atomics::ATOMICS_SMALL_VEC_SIZE; use crate::{ - AtomicKind, Atomics, FontSelection, Frame, Id, Image, IntoAtomics, Response, Sense, - SizedAtomic, SizedAtomicKind, Ui, Widget, + AtomicKind, Atomics, Frame, Id, Image, IntoAtomics, Response, Sense, SizedAtomic, + SizedAtomicKind, Ui, Widget, }; use emath::{Align2, NumExt as _, Rect, Vec2}; use epaint::text::TextWrapMode; @@ -198,21 +198,6 @@ impl<'a> AtomicLayout<'a> { preferred_width += gap_space; } - let default_font_height = || { - let font_selection = FontSelection::default(); - let font_id = font_selection.resolve(ui.style()); - ui.fonts(|f| f.row_height(&font_id)) - }; - - let max_font_size = ui - .fonts(|fonts| { - atomics - .iter() - .filter_map(|a| a.get_min_height_for_image(fonts, ui.style())) - .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) - }) - .unwrap_or_else(default_font_height); - for (idx, item) in atomics.into_iter().enumerate() { if item.shrink { debug_assert!( @@ -227,7 +212,7 @@ impl<'a> AtomicLayout<'a> { if item.grow { grow_count += 1; } - let sized = item.into_sized(ui, available_inner_size, max_font_size, Some(wrap_mode)); + let sized = item.into_sized(ui, available_inner_size, Some(wrap_mode)); let size = sized.size; desired_width += size.x; @@ -249,7 +234,7 @@ impl<'a> AtomicLayout<'a> { grow_count += 1; } - let sized = item.into_sized(ui, shrunk_size, max_font_size, Some(wrap_mode)); + let sized = item.into_sized(ui, shrunk_size, Some(wrap_mode)); let size = sized.size; desired_width += size.x; diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 0acab7b77d3..c27586aee8c 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -91,6 +91,10 @@ impl<'a> Atomics<'a> { }) } + pub fn map(self, f: impl FnMut(Atomic<'a>) -> Atomic<'a>) -> Atomics<'a> { + Atomics(self.0.into_iter().map(f).collect()) + } + pub fn map_kind(&'a mut self, mut f: F) where F: FnMut(AtomicKind<'a>) -> AtomicKind<'a>, diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 1b709762d08..0cc331ab350 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ - Atomic, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, CornerRadius, Frame, - Image, IntoAtomics, NumExt as _, Response, Sense, SizedAtomicKind, Stroke, TextWrapMode, Ui, - Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + Atomic, AtomicExt, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, + CornerRadius, Frame, Image, IntoAtomics, NumExt as _, Response, Sense, Stroke, TextWrapMode, + Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -35,6 +35,7 @@ pub struct Button<'a> { corner_radius: Option, selected: bool, image_tint_follows_text_color: bool, + limit_image_size: bool, } impl<'a> Button<'a> { @@ -51,19 +52,30 @@ impl<'a> Button<'a> { corner_radius: None, selected: false, image_tint_follows_text_color: false, + limit_image_size: false, } } /// Creates a button with an image. The size of the image as displayed is defined by the provided size. + /// + /// Note: In contrast to [`Button::new`], this limits the image size to the default font height + /// (using [`Atomic::atom_max_height_font_size`]). pub fn image(image: impl Into>) -> Self { Self::opt_image_and_text(Some(image.into()), None) } - /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size. + /// Creates a button with an image to the left of the text. + /// + /// Note: In contrast to [`Button::new`], this limits the image size to the default font height + /// (using [`Atomic::atom_max_height_font_size`]). pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { Self::opt_image_and_text(Some(image.into()), Some(text.into())) } + /// Create a button with an optional image and optional text. + /// + /// Note: In contrast to [`Button::new`], this limits the image size to the default font height + /// (using [`Atomic::atom_max_height_font_size`]). pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { @@ -72,6 +84,7 @@ impl<'a> Button<'a> { if let Some(text) = text { button.atomics.push_left(text); } + button.limit_image_size = true; button } @@ -209,7 +222,7 @@ impl<'a> Button<'a> { /// Show the button and return a [`AtomicLayoutResponse`] for painting custom contents. pub fn atomic_ui(self, ui: &mut Ui) -> AtomicLayoutResponse { let Button { - atomics, + mut atomics, wrap_mode, fill, stroke, @@ -220,12 +233,23 @@ impl<'a> Button<'a> { corner_radius, selected, image_tint_follows_text_color, + limit_image_size, } = self; if !small { min_size.y = min_size.y.at_least(ui.spacing().interact_size.y); } + if limit_image_size { + atomics = atomics.map(|atomic| { + if matches!(&atomic.kind, AtomicKind::Image(_)) { + atomic.atom_max_height_font_size(&ui) + } else { + atomic + } + }); + } + let text = atomics.text(); let layout = AtomicLayout::new(atomics) diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index 6b85198a4d4..3022d452a38 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -2,6 +2,7 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; +use eframe::egui::{AtomicExt, Button}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -50,9 +51,11 @@ impl eframe::App for MyApp { } ui.label(format!("Hello '{}', age {}", self.name, self.age)); - ui.image(egui::include_image!( - "../../../crates/egui/assets/ferris.png" - )); + ui.add(Button::new(( + egui::include_image!("../../../crates/egui/assets/ferris.png") + .atom_max_height_font_size(ui), + "Hiii", + ))); }); } } From a0de60cafe18084d189ca0439b69af9ab8d2608b Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 15:14:07 +0200 Subject: [PATCH 50/77] Update snapshots --- crates/egui_demo_lib/src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/widget_gallery.rs | 5 ++++- crates/egui_demo_lib/tests/snapshots/demos/Scene.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/widget_gallery.png | 4 ++-- examples/hello_world/src/main.rs | 9 +++------ .../egui_tests/tests/snapshots/layout/atomics_image.png | 4 ++-- tests/egui_tests/tests/test_widgets.rs | 2 +- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 7b89727861d..9744771ef3b 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -383,6 +383,7 @@ mod tests { .map_or(demo.name(), |(_, name)| name); let mut harness = Harness::new(|ctx| { + egui_extras::install_image_loaders(ctx); demo.show(ctx, &mut true); }); diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index c7b6df28afb..31f5d279a55 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -322,7 +322,10 @@ mod tests { let mut harness = Harness::builder() .with_pixels_per_point(2.0) .with_size(Vec2::new(380.0, 550.0)) - .build_ui(|ui| demo.ui(ui)); + .build_ui(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + demo.ui(ui); + }); harness.fit_contents(); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index a2c2e7bac93..212a7ccd4d0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4bf35ad4ce01122de5bc0830018044fd70f116938293fbeb72a2278de0bbb22 -size 35068 +oid sha256:0fcfee082fe1dcbb7515ca6e3d5457e71fecf91a3efc4f76906a32fdb588adb4 +size 35096 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index b05ebda12d6..bcb09fe26b1 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af75f773e9e4ad2615893babce5b99e7fd127c76dd0976ac8dc95307f38a59dc -size 152854 +oid sha256:ee129f0542f21e12f5aa3c2f9746e7cadd73441a04d580f57c12c1cdd40d8b07 +size 153136 diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index 3022d452a38..6b85198a4d4 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -2,7 +2,6 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; -use eframe::egui::{AtomicExt, Button}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -51,11 +50,9 @@ impl eframe::App for MyApp { } ui.label(format!("Hello '{}', age {}", self.name, self.age)); - ui.add(Button::new(( - egui::include_image!("../../../crates/egui/assets/ferris.png") - .atom_max_height_font_size(ui), - "Hiii", - ))); + ui.image(egui::include_image!( + "../../../crates/egui/assets/ferris.png" + )); }); } } diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_image.png b/tests/egui_tests/tests/snapshots/layout/atomics_image.png index 882cfec100c..3d9efa8a1e6 100644 --- a/tests/egui_tests/tests/snapshots/layout/atomics_image.png +++ b/tests/egui_tests/tests/snapshots/layout/atomics_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43b7e82dbd38dfa2b48c5bae80466e2bbf8d2a60d84c6c85f0f2b95fc64458e -size 395339 +oid sha256:39a13fdac498d6f851a28ea3ca19d523235d5e0ab8e765ea980cf8fb2f64ba35 +size 387619 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 37f18defd3c..96bd00e2340 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -99,7 +99,7 @@ fn widget_tests() { ("minimal", ("Hello World!").into_atomics()), ( "image", - (Image::new(source.clone()), "With Image").into_atomics(), + (source.clone().atom_max_height(12.0), "With Image").into_atomics(), ), ( "multi_grow", From 1d537d520fe90ba30b0c131ffcadb38d469f94c4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 15:17:36 +0200 Subject: [PATCH 51/77] Import --- tests/egui_tests/tests/test_widgets.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 96bd00e2340..13fc212ed1c 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,7 +1,7 @@ use egui::load::SizedTexture; use egui::{ include_image, Align, AtomicExt as _, AtomicLayout, Button, Color32, ColorImage, Direction, - DragValue, Event, Grid, Image, IntoAtomics as _, Layout, PointerButton, Pos2, Response, Slider, + DragValue, Event, Grid, IntoAtomics as _, Layout, PointerButton, Pos2, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, }; From bda7816ddf95b1024266f256e71cda814eb478f4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 16:10:39 +0200 Subject: [PATCH 52/77] Implement max size for Text --- crates/egui/src/atomics/atomic.rs | 10 +++++----- crates/egui/src/atomics/atomic_kind.rs | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index 2fe8167ce02..a8962f1f179 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -1,5 +1,5 @@ use crate::{AtomicKind, SizedAtomic, Ui}; -use emath::Vec2; +use emath::{NumExt, Vec2}; use epaint::text::TextWrapMode; /// A low-level ui building block. @@ -49,15 +49,15 @@ impl<'a> Atomic<'a> { pub fn into_sized( self, ui: &Ui, - available_size: Vec2, + mut available_size: Vec2, mut wrap_mode: Option, ) -> SizedAtomic<'a> { if !self.shrink { wrap_mode = Some(TextWrapMode::Extend); } - let (preferred, kind) = self - .kind - .into_sized(ui, available_size, self.max_size, wrap_mode); + available_size = available_size.at_most(self.max_size); + + let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode); SizedAtomic { size: self.size.unwrap_or_else(|| kind.size()), preferred_size: preferred, diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index 1516b43970d..3413bb69eee 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -82,7 +82,6 @@ impl<'a> AtomicKind<'a> { self, ui: &Ui, available_size: Vec2, - max_size: Vec2, wrap_mode: Option, ) -> (Vec2, SizedAtomicKind<'a>) { match self { @@ -94,7 +93,7 @@ impl<'a> AtomicKind<'a> { ) } AtomicKind::Image(image) => { - let size = image.load_and_calc_size(ui, Vec2::min(available_size, max_size)); + let size = image.load_and_calc_size(ui, available_size); let size = size.unwrap_or(Vec2::ZERO); (size, SizedAtomicKind::Image(image, size)) } From a7b78e452c523cf391caade01fc1d46add616d18 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 16:29:38 +0200 Subject: [PATCH 53/77] More docs --- crates/egui/src/atomics/atomic_kind.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index 3413bb69eee..29e1ef8e329 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -1,4 +1,4 @@ -use crate::{Id, Image, ImageSource, SizedAtomicKind, TextStyle, Ui, WidgetText}; +use crate::{Atomic, Id, Image, ImageSource, SizedAtomicKind, TextStyle, Ui, WidgetText}; use emath::Vec2; use epaint::text::TextWrapMode; use std::fmt::Formatter; @@ -21,11 +21,22 @@ pub enum AtomicKind<'a> { /// - any other text atomics will have `wrap_mode` extend /// - if `wrap_mode` is extend, Text will extend as expected. /// - /// Generally, `wrap_mode` should only be set via [`crate::Style`] or + /// Unless [`Atomic::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or /// [`crate::AtomicLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atomic /// that is not `shrink` will have unexpected results. + /// + /// The size is determined by converting the [`WidgetText`] into a galley and using the galleys + /// size. You can use [`Atomic::atom_size`] to override this, and [`Atomic::atom_max_width`] + /// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`. + /// [`Atomic::atom_max_height`] has no effect on text. Text(WidgetText), + /// Image atomic. + /// + /// By default the size is determined via [`Image::calc_size`]. + /// You can use [`Atomic::atom_max_size`] or [`Atomic::atom_size`] to customize the size. + /// There is also a helper [`Atomic::atom_max_height_font_size`] to set the max height to the + /// default font height, which is convenient for icons. Image(Image<'a>), /// For custom rendering. From f9ac3b6db8952b9ce5419429ec1ecd55cc6512dd Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 16:33:15 +0200 Subject: [PATCH 54/77] Impl debug for WidgetText --- crates/egui/src/atomics/atomic_kind.rs | 13 +------------ crates/egui/src/widget_text.rs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index 29e1ef8e329..8f64101d183 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -4,7 +4,7 @@ use epaint::text::TextWrapMode; use std::fmt::Formatter; /// The different kinds of [`Atomic`]s. -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] pub enum AtomicKind<'a> { /// Empty, that can be used with [`AtomicExt::a_grow`] to reserve space. #[default] @@ -61,17 +61,6 @@ pub enum AtomicKind<'a> { Custom(Id, Vec2), } -impl std::fmt::Debug for AtomicKind<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - AtomicKind::Empty => write!(f, "AtomicKind::Empty"), - AtomicKind::Text(text) => write!(f, "AtomicKind::Text({})", text.text()), - AtomicKind::Image(image) => write!(f, "AtomicKind::Image({image:?})"), - AtomicKind::Custom(id, size) => write!(f, "AtomicKind::Custom({id:?}, {size:?})"), - } - } -} - impl<'a> AtomicKind<'a> { pub fn text(text: impl Into) -> Self { AtomicKind::Text(text.into()) diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index d9f98859b5c..d0edb6a26d2 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,7 +1,7 @@ -use std::{borrow::Cow, sync::Arc}; - use emath::GuiRounding as _; use epaint::text::TextFormat; +use std::fmt::Formatter; +use std::{borrow::Cow, sync::Arc}; use crate::{ text::{LayoutJob, TextWrapping}, @@ -521,6 +521,18 @@ pub enum WidgetText { Galley(Arc), } +impl std::fmt::Debug for WidgetText { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let text = self.text(); + match self { + Self::Text(_) => write!(f, "Text({text:?})"), + Self::RichText(_) => write!(f, "RichText({text:?})"), + Self::LayoutJob(_) => write!(f, "LayoutJob({text:?})"), + Self::Galley(_) => write!(f, "Galley({text:?})"), + } + } +} + impl Default for WidgetText { fn default() -> Self { Self::Text(String::new()) From a74f3dda45ecbb158b63b4f939cd8ea565ca01f5 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 16:37:15 +0200 Subject: [PATCH 55/77] Make grow private in SizedAtomic --- crates/egui/src/atomics/sized_atomic.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/sized_atomic.rs b/crates/egui/src/atomics/sized_atomic.rs index 37bd64dc704..8a62be8948b 100644 --- a/crates/egui/src/atomics/sized_atomic.rs +++ b/crates/egui/src/atomics/sized_atomic.rs @@ -4,8 +4,15 @@ use emath::Vec2; /// A [`Atomic`] which has been sized. #[derive(Clone, Debug)] pub struct SizedAtomic<'a> { - pub grow: bool, + grow: bool, pub size: Vec2, pub preferred_size: Vec2, pub kind: SizedAtomicKind<'a>, } + +impl SizedAtomic<'_> { + /// Was this [`Atomic`] marked as `grow`? + fn is_grow(&self) -> bool { + self.grow + } +} From 3a45786d882b0eaa75c7b482cc2e18e1344d6b2e Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 7 May 2025 17:01:43 +0200 Subject: [PATCH 56/77] Fixes from review --- crates/egui/src/atomics/atomic.rs | 2 +- crates/egui/src/atomics/atomic_kind.rs | 9 +++--- crates/egui/src/atomics/atomic_layout.rs | 36 +++++++++++++----------- crates/egui/src/atomics/atomics.rs | 2 +- crates/egui/src/atomics/sized_atomic.rs | 12 ++++++-- crates/egui/src/widgets/button.rs | 4 +-- 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index a8962f1f179..e9089d5430f 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -1,5 +1,5 @@ use crate::{AtomicKind, SizedAtomic, Ui}; -use emath::{NumExt, Vec2}; +use emath::{NumExt as _, Vec2}; use epaint::text::TextWrapMode; /// A low-level ui building block. diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index 8f64101d183..b184ff4993c 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -1,7 +1,6 @@ -use crate::{Atomic, Id, Image, ImageSource, SizedAtomicKind, TextStyle, Ui, WidgetText}; +use crate::{Id, Image, ImageSource, SizedAtomicKind, TextStyle, Ui, WidgetText}; use emath::Vec2; use epaint::text::TextWrapMode; -use std::fmt::Formatter; /// The different kinds of [`Atomic`]s. #[derive(Clone, Default, Debug)] @@ -21,14 +20,14 @@ pub enum AtomicKind<'a> { /// - any other text atomics will have `wrap_mode` extend /// - if `wrap_mode` is extend, Text will extend as expected. /// - /// Unless [`Atomic::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or + /// Unless [`crate::Atomic::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or /// [`crate::AtomicLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atomic /// that is not `shrink` will have unexpected results. /// /// The size is determined by converting the [`WidgetText`] into a galley and using the galleys - /// size. You can use [`Atomic::atom_size`] to override this, and [`Atomic::atom_max_width`] + /// size. You can use [`crate::Atomic::atom_size`] to override this, and [`crate::Atomic::atom_max_width`] /// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`. - /// [`Atomic::atom_max_height`] has no effect on text. + /// [`crate::Atomic::atom_max_height`] has no effect on text. Text(WidgetText), /// Image atomic. diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index ae156e66598..307a46b9cbc 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -3,7 +3,7 @@ use crate::{ AtomicKind, Atomics, Frame, Id, Image, IntoAtomics, Response, Sense, SizedAtomic, SizedAtomicKind, Ui, Widget, }; -use emath::{Align2, NumExt as _, Rect, Vec2}; +use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2}; use epaint::text::TextWrapMode; use epaint::{Color32, Galley}; use smallvec::SmallVec; @@ -111,7 +111,7 @@ impl<'a> AtomicLayout<'a> { /// Set the [`TextWrapMode`] for the [`Atomic`] marked as `shrink`. /// /// Only a single [`Atomic`] may shrink. If this (or `ui.wrap_mode()`) is not - /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (right-most) + /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most) /// [`AtomicKind::Text`] will be set to shrink. #[inline] pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { @@ -121,6 +121,8 @@ impl<'a> AtomicLayout<'a> { /// Set the [`Align2`]. /// + /// This will align the [`Atomic`]s within the [`Rect`] returned by [`Ui::allocate_space`]. + /// /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) /// for info on how the [`crate::Layout`] affects the alignment. @@ -162,7 +164,7 @@ impl<'a> AtomicLayout<'a> { .iter_mut() .find(|a| matches!(a.kind, AtomicKind::Text(..))); if let Some(atomic) = first_text { - atomic.shrink = true; + atomic.shrink = true; // Will make the text truncate or shrink depending on wrap_mode } } } @@ -177,6 +179,9 @@ impl<'a> AtomicLayout<'a> { let available_inner_size = ui.available_size() - frame.total_margin().sum(); let mut desired_width = 0.0; + + // Preferred width / height is the ideal size of the widget, e.g. the size where the + // text is not wrapped. Used to set Response::intrinsic_size. let mut preferred_width = 0.0; let mut preferred_height = 0.0; @@ -199,19 +204,19 @@ impl<'a> AtomicLayout<'a> { } for (idx, item) in atomics.into_iter().enumerate() { + if item.grow { + grow_count += 1; + } if item.shrink { debug_assert!( shrink_item.is_none(), - "Only one atomic may be marked as shrink" + "Only one atomic may be marked as shrink. {item:?}" ); if shrink_item.is_none() { shrink_item = Some((idx, item)); continue; } } - if item.grow { - grow_count += 1; - } let sized = item.into_sized(ui, available_inner_size, Some(wrap_mode)); let size = sized.size; @@ -226,15 +231,12 @@ impl<'a> AtomicLayout<'a> { if let Some((index, item)) = shrink_item { // The `shrink` item gets the remaining space - let shrunk_size = Vec2::new( + let available_size_for_shrink_item = Vec2::new( available_inner_size.x - desired_width, available_inner_size.y, ); - if item.grow { - grow_count += 1; - } - let sized = item.into_sized(ui, shrunk_size, Some(wrap_mode)); + let sized = item.into_sized(ui, available_size_for_shrink_item, Some(wrap_mode)); let size = sized.size; desired_width += size.x; @@ -357,7 +359,7 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { /// Paint the [`Frame`] and individual [`Atomic`]s. pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { let Self { - sized_atomics: sized_items, + sized_atomics, frame, fallback_text_color, response, @@ -373,7 +375,7 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { let width_to_fill = inner_rect.width(); let extra_space = f32::max(width_to_fill - desired_size.x, 0.0); - let grow_width = f32::max(extra_space / grow_count as f32, 0.0); + let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui(); let aligned_rect = if grow_count > 0 { align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect) @@ -385,9 +387,9 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { let mut response = AtomicLayoutResponse::empty(response); - for sized in sized_items { + for sized in sized_atomics { let size = sized.size; - let growth = if sized.grow { grow_width } else { 0.0 }; + let growth = if sized.is_grow() { grow_width } else { 0.0 }; let frame = aligned_rect .with_min_x(cursor) @@ -435,7 +437,7 @@ impl AtomicLayoutResponse { pub fn empty(response: Response) -> Self { Self { response, - custom_rects: SmallVec::new(), + custom_rects: Default::default(), } } diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index c27586aee8c..2a77b8b1cd3 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -91,7 +91,7 @@ impl<'a> Atomics<'a> { }) } - pub fn map(self, f: impl FnMut(Atomic<'a>) -> Atomic<'a>) -> Atomics<'a> { + pub fn map(self, f: impl FnMut(Atomic<'a>) -> Atomic<'a>) -> Self { Atomics(self.0.into_iter().map(f).collect()) } diff --git a/crates/egui/src/atomics/sized_atomic.rs b/crates/egui/src/atomics/sized_atomic.rs index 8a62be8948b..da059f2e45f 100644 --- a/crates/egui/src/atomics/sized_atomic.rs +++ b/crates/egui/src/atomics/sized_atomic.rs @@ -4,15 +4,23 @@ use emath::Vec2; /// A [`Atomic`] which has been sized. #[derive(Clone, Debug)] pub struct SizedAtomic<'a> { - grow: bool, + pub(crate) grow: bool, + + /// The size of the atomic. + /// + /// Used for placing this atomic in [`crate::AtomicLayout`], the cursor will advance by + /// size.x + gap. pub size: Vec2, + + /// Preferred size of the atomic. This is used to calculate `Response::intrinsic_size`. pub preferred_size: Vec2, + pub kind: SizedAtomicKind<'a>, } impl SizedAtomic<'_> { /// Was this [`Atomic`] marked as `grow`? - fn is_grow(&self) -> bool { + pub fn is_grow(&self) -> bool { self.grow } } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 0cc331ab350..f273874564b 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,5 +1,5 @@ use crate::{ - Atomic, AtomicExt, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, + Atomic, AtomicExt as _, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, CornerRadius, Frame, Image, IntoAtomics, NumExt as _, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; @@ -243,7 +243,7 @@ impl<'a> Button<'a> { if limit_image_size { atomics = atomics.map(|atomic| { if matches!(&atomic.kind, AtomicKind::Image(_)) { - atomic.atom_max_height_font_size(&ui) + atomic.atom_max_height_font_size(ui) } else { atomic } From e9c89bafe1b33c9fc35cf34d8a1c6165e3a3e6eb Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 8 May 2025 09:49:04 +0200 Subject: [PATCH 57/77] Improve docs --- crates/egui/src/atomics/atomic_kind.rs | 18 +++++++++--------- crates/egui/src/atomics/atomic_layout.rs | 10 +++++----- crates/egui/src/atomics/atomics.rs | 1 + crates/egui/src/atomics/sized_atomic.rs | 4 ++-- crates/egui/src/atomics/sized_atomic_kind.rs | 2 +- crates/egui/src/widgets/button.rs | 8 ++++---- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index b184ff4993c..77fb89407d4 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -2,10 +2,10 @@ use crate::{Id, Image, ImageSource, SizedAtomicKind, TextStyle, Ui, WidgetText}; use emath::Vec2; use epaint::text::TextWrapMode; -/// The different kinds of [`Atomic`]s. +/// The different kinds of [`crate::Atomic`]s. #[derive(Clone, Default, Debug)] pub enum AtomicKind<'a> { - /// Empty, that can be used with [`AtomicExt::a_grow`] to reserve space. + /// Empty, that can be used with [`crate::AtomicExt::atom_grow`] to reserve space. #[default] Empty, @@ -20,27 +20,27 @@ pub enum AtomicKind<'a> { /// - any other text atomics will have `wrap_mode` extend /// - if `wrap_mode` is extend, Text will extend as expected. /// - /// Unless [`crate::Atomic::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or + /// Unless [`crate::AtomicExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or /// [`crate::AtomicLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atomic /// that is not `shrink` will have unexpected results. /// /// The size is determined by converting the [`WidgetText`] into a galley and using the galleys - /// size. You can use [`crate::Atomic::atom_size`] to override this, and [`crate::Atomic::atom_max_width`] + /// size. You can use [`crate::AtomicExt::atom_size`] to override this, and [`crate::AtomicExt::atom_max_width`] /// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`. - /// [`crate::Atomic::atom_max_height`] has no effect on text. + /// [`crate::AtomicExt::atom_max_height`] has no effect on text. Text(WidgetText), /// Image atomic. /// /// By default the size is determined via [`Image::calc_size`]. - /// You can use [`Atomic::atom_max_size`] or [`Atomic::atom_size`] to customize the size. - /// There is also a helper [`Atomic::atom_max_height_font_size`] to set the max height to the + /// You can use [`crate::AtomicExt::atom_max_size`] or [`crate::AtomicExt::atom_size`] to customize the size. + /// There is also a helper [`crate::AtomicExt::atom_max_height_font_size`] to set the max height to the /// default font height, which is convenient for icons. Image(Image<'a>), /// For custom rendering. /// - /// You can get the [`Rect`] with the [`Id`] from [`AtomicLayoutResponse`] and use a + /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomicLayoutResponse`] and use a /// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content. /// /// Example: @@ -75,7 +75,7 @@ impl<'a> AtomicKind<'a> { /// Turn this [`AtomicKind`] into a [`SizedAtomicKind`]. /// - /// This converts [`WidgetText`] into [`Galley`] and tries to load and size [`Image`]. + /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`]. /// The first returned argument is the preferred size. pub fn into_sized( self, diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index 307a46b9cbc..b4a4e3384a2 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -24,7 +24,7 @@ use std::sync::Arc; /// - returns a [`AllocatedAtomicLayout`] /// - [`AllocatedAtomicLayout::paint`] /// - paints the [`Frame`] -/// - calculates individual [`Atomic`] positions +/// - calculates individual [`crate::Atomic`] positions /// - paints each single atomic /// /// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the @@ -108,9 +108,9 @@ impl<'a> AtomicLayout<'a> { self } - /// Set the [`TextWrapMode`] for the [`Atomic`] marked as `shrink`. + /// Set the [`TextWrapMode`] for the [`crate::Atomic`] marked as `shrink`. /// - /// Only a single [`Atomic`] may shrink. If this (or `ui.wrap_mode()`) is not + /// Only a single [`crate::Atomic`] may shrink. If this (or `ui.wrap_mode()`) is not /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most) /// [`AtomicKind::Text`] will be set to shrink. #[inline] @@ -121,7 +121,7 @@ impl<'a> AtomicLayout<'a> { /// Set the [`Align2`]. /// - /// This will align the [`Atomic`]s within the [`Rect`] returned by [`Ui::allocate_space`]. + /// This will align the [`crate::Atomic`]s within the [`Rect`] returned by [`Ui::allocate_space`]. /// /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) @@ -356,7 +356,7 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { }); } - /// Paint the [`Frame`] and individual [`Atomic`]s. + /// Paint the [`Frame`] and individual [`crate::Atomic`]s. pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { let Self { sized_atomics, diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 2a77b8b1cd3..c70fc443bd5 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -159,6 +159,7 @@ where } } +/// Trait for turning a tuple of [`Atomic`]s into [`Atomics`]. pub trait IntoAtomics<'a> { fn collect(self, atomics: &mut Atomics<'a>); diff --git a/crates/egui/src/atomics/sized_atomic.rs b/crates/egui/src/atomics/sized_atomic.rs index da059f2e45f..e6f7241d5ae 100644 --- a/crates/egui/src/atomics/sized_atomic.rs +++ b/crates/egui/src/atomics/sized_atomic.rs @@ -1,7 +1,7 @@ use crate::SizedAtomicKind; use emath::Vec2; -/// A [`Atomic`] which has been sized. +/// A [`crate::Atomic`] which has been sized. #[derive(Clone, Debug)] pub struct SizedAtomic<'a> { pub(crate) grow: bool, @@ -19,7 +19,7 @@ pub struct SizedAtomic<'a> { } impl SizedAtomic<'_> { - /// Was this [`Atomic`] marked as `grow`? + /// Was this [`crate::Atomic`] marked as `grow`? pub fn is_grow(&self) -> bool { self.grow } diff --git a/crates/egui/src/atomics/sized_atomic_kind.rs b/crates/egui/src/atomics/sized_atomic_kind.rs index 58bf7307629..544cd2df46d 100644 --- a/crates/egui/src/atomics/sized_atomic_kind.rs +++ b/crates/egui/src/atomics/sized_atomic_kind.rs @@ -3,7 +3,7 @@ use emath::Vec2; use epaint::Galley; use std::sync::Arc; -/// A sized [`AtomicKind`]. +/// A sized [`crate::AtomicKind`]. #[derive(Clone, Default, Debug)] pub enum SizedAtomicKind<'a> { #[default] diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index f273874564b..78a060176d1 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,5 +1,5 @@ use crate::{ - Atomic, AtomicExt as _, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, + Atomic, AtomicExt, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, CornerRadius, Frame, Image, IntoAtomics, NumExt as _, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; @@ -59,7 +59,7 @@ impl<'a> Button<'a> { /// Creates a button with an image. The size of the image as displayed is defined by the provided size. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`Atomic::atom_max_height_font_size`]). + /// (using [`AtomicExt::atom_max_height_font_size`]). pub fn image(image: impl Into>) -> Self { Self::opt_image_and_text(Some(image.into()), None) } @@ -67,7 +67,7 @@ impl<'a> Button<'a> { /// Creates a button with an image to the left of the text. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`Atomic::atom_max_height_font_size`]). + /// (using [`AtomicExt::atom_max_height_font_size`]). pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { Self::opt_image_and_text(Some(image.into()), Some(text.into())) } @@ -75,7 +75,7 @@ impl<'a> Button<'a> { /// Create a button with an optional image and optional text. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`Atomic::atom_max_height_font_size`]). + /// (using [`AtomicExt::atom_max_height_font_size`]). pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { From 90ac0d56c65480e4a64baf45ff089dd873591f88 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 8 May 2025 10:47:57 +0200 Subject: [PATCH 58/77] Construct AtomicLayout in Button constructor for performance gains --- crates/egui/src/atomics/atomics.rs | 5 +-- crates/egui/src/widgets/button.rs | 55 ++++++++++++------------------ 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index c70fc443bd5..5f620de90c6 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -91,8 +91,9 @@ impl<'a> Atomics<'a> { }) } - pub fn map(self, f: impl FnMut(Atomic<'a>) -> Atomic<'a>) -> Self { - Atomics(self.0.into_iter().map(f).collect()) + pub fn map_atomics(&mut self, mut f: impl FnMut(Atomic<'a>) -> Atomic<'a>) { + self.iter_mut() + .for_each(|atomic| *atomic = f(std::mem::take(atomic))); } pub fn map_kind(&'a mut self, mut f: F) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 78a060176d1..87452bcf7bd 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ - Atomic, AtomicExt, AtomicKind, AtomicLayout, AtomicLayoutResponse, Atomics, Color32, - CornerRadius, Frame, Image, IntoAtomics, NumExt as _, Response, Sense, Stroke, TextWrapMode, - Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + Atomic, AtomicExt as _, AtomicKind, AtomicLayout, AtomicLayoutResponse, Color32, CornerRadius, + Frame, Image, IntoAtomics, NumExt as _, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, + Widget, WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -24,11 +24,9 @@ use crate::{ /// ``` #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Button<'a> { - atomics: Atomics<'a>, - wrap_mode: Option, + layout: AtomicLayout<'a>, fill: Option, stroke: Option, - sense: Sense, small: bool, frame: Option, min_size: Vec2, @@ -41,11 +39,9 @@ pub struct Button<'a> { impl<'a> Button<'a> { pub fn new(content: impl IntoAtomics<'a>) -> Self { Self { - atomics: content.into_atomics(), - wrap_mode: None, + layout: AtomicLayout::new(content.into_atomics()), fill: None, stroke: None, - sense: Sense::click(), small: false, frame: None, min_size: Vec2::ZERO, @@ -79,10 +75,10 @@ impl<'a> Button<'a> { pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { - button.atomics.push_left(image); + button.layout.push_left(image); } if let Some(text) = text { - button.atomics.push_left(text); + button.layout.push_left(text); } button.limit_image_size = true; button @@ -95,23 +91,20 @@ impl<'a> Button<'a> { /// Note that any `\n` in the text will always produce a new line. #[inline] pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { - self.wrap_mode = Some(wrap_mode); + self.layout = self.layout.wrap_mode(wrap_mode); self } /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`]. #[inline] - pub fn wrap(mut self) -> Self { - self.wrap_mode = Some(TextWrapMode::Wrap); - - self + pub fn wrap(self) -> Self { + self.wrap_mode(TextWrapMode::Wrap) } /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`]. #[inline] - pub fn truncate(mut self) -> Self { - self.wrap_mode = Some(TextWrapMode::Truncate); - self + pub fn truncate(self) -> Self { + self.wrap_mode(TextWrapMode::Truncate) } /// Override background fill color. Note that this will override any on-hover effects. @@ -149,7 +142,7 @@ impl<'a> Button<'a> { /// Change this to a drag-button with `Sense::drag()`. #[inline] pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; + self.layout = self.layout.sense(sense); self } @@ -199,16 +192,16 @@ impl<'a> Button<'a> { AtomicKind::Text(text) => AtomicKind::Text(text.weak()), other => other, }; - self.atomics.push_left(Atomic::grow()); - self.atomics.push_left(atomic); + self.layout.push_left(Atomic::grow()); + self.layout.push_left(atomic); self } /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl Into>) -> Self { - self.atomics.push_left(Atomic::grow()); - self.atomics.push_left(right_text.into()); + self.layout.push_left(Atomic::grow()); + self.layout.push_left(right_text.into()); self } @@ -222,11 +215,9 @@ impl<'a> Button<'a> { /// Show the button and return a [`AtomicLayoutResponse`] for painting custom contents. pub fn atomic_ui(self, ui: &mut Ui) -> AtomicLayoutResponse { let Button { - mut atomics, - wrap_mode, + mut layout, fill, stroke, - sense, small, frame, mut min_size, @@ -241,7 +232,7 @@ impl<'a> Button<'a> { } if limit_image_size { - atomics = atomics.map(|atomic| { + layout.map_atomics(|atomic| { if matches!(&atomic.kind, AtomicKind::Image(_)) { atomic.atom_max_height_font_size(ui) } else { @@ -250,12 +241,7 @@ impl<'a> Button<'a> { }); } - let text = atomics.text(); - - let layout = AtomicLayout::new(atomics) - .wrap_mode(wrap_mode.unwrap_or(ui.wrap_mode())) - .sense(sense) - .min_size(min_size); + let text = layout.text(); let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); @@ -270,6 +256,7 @@ impl<'a> Button<'a> { let mut prepared = layout .frame(Frame::new().inner_margin(button_padding)) + .min_size(min_size) .allocate(ui); let response = if ui.is_rect_visible(prepared.response.rect) { From 6e823be7fd97accb3bf4eea83890783ae98f019e Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 8 May 2025 10:51:35 +0200 Subject: [PATCH 59/77] Docs --- crates/egui/src/widgets/button.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 87452bcf7bd..c6f80ff7aab 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -55,7 +55,7 @@ impl<'a> Button<'a> { /// Creates a button with an image. The size of the image as displayed is defined by the provided size. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`AtomicExt::atom_max_height_font_size`]). + /// (using [`crate::AtomicExt::atom_max_height_font_size`]). pub fn image(image: impl Into>) -> Self { Self::opt_image_and_text(Some(image.into()), None) } @@ -63,7 +63,7 @@ impl<'a> Button<'a> { /// Creates a button with an image to the left of the text. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`AtomicExt::atom_max_height_font_size`]). + /// (using [`crate::AtomicExt::atom_max_height_font_size`]). pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { Self::opt_image_and_text(Some(image.into()), Some(text.into())) } @@ -71,7 +71,7 @@ impl<'a> Button<'a> { /// Create a button with an optional image and optional text. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`AtomicExt::atom_max_height_font_size`]). + /// (using [`crate::AtomicExt::atom_max_height_font_size`]). pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { From 615d22c4f25a4b4db42d876bd0f790133effca91 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 8 May 2025 12:11:54 +0200 Subject: [PATCH 60/77] Ooops set correct sense --- crates/egui/src/widgets/button.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index c6f80ff7aab..13a8fbf6237 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -39,7 +39,7 @@ pub struct Button<'a> { impl<'a> Button<'a> { pub fn new(content: impl IntoAtomics<'a>) -> Self { Self { - layout: AtomicLayout::new(content.into_atomics()), + layout: AtomicLayout::new(content.into_atomics()).sense(Sense::click()), fill: None, stroke: None, small: false, From e258817249f00c41b6e434eaa813a2134973b447 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 8 May 2025 12:34:59 +0200 Subject: [PATCH 61/77] Fix doctest --- crates/egui/src/atomics/atomic_kind.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index 77fb89407d4..d57599114ef 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -53,7 +53,7 @@ pub enum AtomicKind<'a> { /// /// let rect = response.get_rect(id); /// if let Some(rect) = rect { - /// ui.put(*rect, Button::new("⏵")); + /// ui.put(rect, Button::new("⏵")); /// } /// # }); /// ``` From e00d4635dd6cab7f331c54961d7d4235bf17fcdf Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 8 May 2025 12:45:26 +0200 Subject: [PATCH 62/77] Typo --- crates/egui/src/atomics/atomic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index e9089d5430f..6c2a9a4fd91 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -36,7 +36,7 @@ impl Default for Atomic<'_> { impl<'a> Atomic<'a> { /// Create an empty [`Atomic`] marked as `grow`. /// - /// This will expand in size, allowing all proceeding atomics to be left-aligned, + /// This will expand in size, allowing all preceding atomics to be left-aligned, /// and all following atomics to be right-aligned pub fn grow() -> Self { Atomic { From 6d17866510c7a533b5e2898a82866914d587e344 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Fri, 16 May 2025 14:11:06 +0100 Subject: [PATCH 63/77] Fix left/right mixup and add wip test --- crates/egui/src/atomics/atomic_ext.rs | 10 +++++++ crates/egui/src/atomics/atomics.rs | 6 ++-- crates/egui/src/widgets/button.rs | 12 ++++---- crates/egui/src/widgets/checkbox.rs | 2 +- crates/egui/src/widgets/radio_button.rs | 2 +- tests/egui_tests/tests/test_atomics.rs | 38 +++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 tests/egui_tests/tests/test_atomics.rs diff --git a/crates/egui/src/atomics/atomic_ext.rs b/crates/egui/src/atomics/atomic_ext.rs index 54f131f5f17..5982547b01a 100644 --- a/crates/egui/src/atomics/atomic_ext.rs +++ b/crates/egui/src/atomics/atomic_ext.rs @@ -8,9 +8,17 @@ pub trait AtomicExt<'a> { /// If [`Atomic::grow`] is `true`, this will be the minimum width. /// If [`Atomic::shrink`] is `true`, this will be the maximum width. /// If both are true, the width will have no effect. + /// + /// See [`crate::AtomicKind`] docs to see how the size affects the different types. fn atom_size(self, size: Vec2) -> Atomic<'a>; /// Grow this atomic to the available space. + /// + /// This will affect the size of the [`Atomic`] in the main direction. Since + /// [`AtomicLayout`] today only supports horizontal layout, it will affect the width. + /// + /// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the + /// remaining space. fn atom_grow(self, grow: bool) -> Atomic<'a>; /// Shrink this atomic if there isn't enough space. @@ -19,6 +27,8 @@ pub trait AtomicExt<'a> { fn atom_shrink(self, shrink: bool) -> Atomic<'a>; /// Set the maximum size of this atomic. + /// + /// This is mostly used as a fn atom_max_size(self, max_size: Vec2) -> Atomic<'a>; /// Set the maximum width of this atomic. diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 5f620de90c6..402724e8b21 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -16,12 +16,12 @@ impl<'a> Atomics<'a> { } /// Insert a new [`Atomic`] at the end of the list (right side). - pub fn push_left(&mut self, atomic: impl Into>) { + pub fn push_right(&mut self, atomic: impl Into>) { self.0.push(atomic.into()); } /// Insert a new [`Atomic`] at the beginning of the list (left side). - pub fn push_right(&mut self, atomic: impl Into>) { + pub fn push_left(&mut self, atomic: impl Into>) { self.0.insert(0, atomic.into()); } @@ -156,7 +156,7 @@ where T: Into>, { fn collect(self, atomics: &mut Atomics<'a>) { - atomics.push_left(self); + atomics.push_right(self); } } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 13a8fbf6237..855942b90fa 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -75,10 +75,10 @@ impl<'a> Button<'a> { pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { - button.layout.push_left(image); + button.layout.push_right(image); } if let Some(text) = text { - button.layout.push_left(text); + button.layout.push_right(text); } button.limit_image_size = true; button @@ -192,16 +192,16 @@ impl<'a> Button<'a> { AtomicKind::Text(text) => AtomicKind::Text(text.weak()), other => other, }; - self.layout.push_left(Atomic::grow()); - self.layout.push_left(atomic); + self.layout.push_right(Atomic::grow()); + self.layout.push_right(atomic); self } /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl Into>) -> Self { - self.layout.push_left(Atomic::grow()); - self.layout.push_left(right_text.into()); + self.layout.push_right(Atomic::grow()); + self.layout.push_right(right_text.into()); self } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 3e361192dd5..6b772c22373 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -66,7 +66,7 @@ impl Widget for Checkbox<'_> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::checkbox"); - atomics.push_right(Custom(rect_id, icon_size)); + atomics.push_left(Custom(rect_id, icon_size)); let text = atomics.text(); diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index cab8f834cf8..bfac411b36f 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -54,7 +54,7 @@ impl Widget for RadioButton<'_> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::radio_button"); - atomics.push_right(AtomicKind::Custom(rect_id, icon_size)); + atomics.push_left(AtomicKind::Custom(rect_id, icon_size)); let text = atomics.text(); diff --git a/tests/egui_tests/tests/test_atomics.rs b/tests/egui_tests/tests/test_atomics.rs new file mode 100644 index 00000000000..13da693d539 --- /dev/null +++ b/tests/egui_tests/tests/test_atomics.rs @@ -0,0 +1,38 @@ +use egui::{Align, AtomicExt, Button, Layout, Ui, Vec2, Widget}; +use egui_kittest::HarnessBuilder; + +#[test] +fn test_atomics() { + single_test("max_width_and_grow", |ui| { + _ = Button::new(( + "hello my name is".atom_max_width(10.0).atom_grow(true), + "world", + )) + .ui(ui); + }); +} + +fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) { + let mut harness = HarnessBuilder::default() + .with_size(Vec2::new(400.0, 200.0)) + .build_ui(move |ui| { + ui.label("Normal"); + let normal_width = ui.horizontal(&mut f).response.rect.width(); + + ui.label("Justified"); + ui.with_layout( + Layout::left_to_right(Align::Min).with_main_justify(true), + &mut f, + ); + + ui.label("Shrunk"); + ui.scope(|ui| { + ui.set_max_width(normal_width / 2.0); + f(ui); + }); + }); + + // harness.fit_contents(); + + harness.snapshot(name); +} From e6b3d52a3a3480620299625eba2ad2130d990309 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 10:03:55 +0200 Subject: [PATCH 64/77] Remove redundant size Vec2 from AtomicKind::Custom --- crates/egui/src/atomics/atomic.rs | 16 ++++++++++++++-- crates/egui/src/atomics/atomic_ext.rs | 14 +++++++++++++- crates/egui/src/atomics/atomic_kind.rs | 14 +++++--------- crates/egui/src/atomics/atomic_layout.rs | 2 +- crates/egui/src/atomics/sized_atomic_kind.rs | 6 +++--- crates/egui/src/widgets/checkbox.rs | 7 +++---- crates/egui/src/widgets/radio_button.rs | 6 +++--- 7 files changed, 42 insertions(+), 23 deletions(-) diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index 6c2a9a4fd91..c055f0ed98d 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -1,4 +1,4 @@ -use crate::{AtomicKind, SizedAtomic, Ui}; +use crate::{AtomicKind, Id, SizedAtomic, Ui}; use emath::{NumExt as _, Vec2}; use epaint::text::TextWrapMode; @@ -45,6 +45,15 @@ impl<'a> Atomic<'a> { } } + /// Create a [`AtomicKind::Custom`] with a specific size. + pub fn custom(id: Id, size: impl Into) -> Self { + Atomic { + size: Some(size.into()), + kind: AtomicKind::Custom(id), + ..Default::default() + } + } + /// Turn this into a [`SizedAtomic`]. pub fn into_sized( self, @@ -52,10 +61,13 @@ impl<'a> Atomic<'a> { mut available_size: Vec2, mut wrap_mode: Option, ) -> SizedAtomic<'a> { - if !self.shrink { + if !self.shrink && self.max_size.x.is_infinite() { wrap_mode = Some(TextWrapMode::Extend); } available_size = available_size.at_most(self.max_size); + if self.max_size.x.is_finite() { + wrap_mode = Some(TextWrapMode::Truncate); + } let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode); SizedAtomic { diff --git a/crates/egui/src/atomics/atomic_ext.rs b/crates/egui/src/atomics/atomic_ext.rs index 5982547b01a..ac4f977c75d 100644 --- a/crates/egui/src/atomics/atomic_ext.rs +++ b/crates/egui/src/atomics/atomic_ext.rs @@ -2,6 +2,8 @@ use crate::{Atomic, FontSelection, Ui}; use emath::Vec2; /// A trait for conveniently building [`Atomic`]s. +/// +/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`]. pub trait AtomicExt<'a> { /// Set the atomic to a fixed size. /// @@ -23,15 +25,25 @@ pub trait AtomicExt<'a> { /// Shrink this atomic if there isn't enough space. /// + /// This will affect the size of the [`Atomic`] in the main direction. Since + /// [`AtomicLayout`] today only supports horizontal layout, it will affect the width. + /// /// NOTE: Only a single [`Atomic`] may shrink for each widget. + /// + /// If no atomic was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first + /// `AtomKind::Text` is set to shrink. fn atom_shrink(self, shrink: bool) -> Atomic<'a>; /// Set the maximum size of this atomic. /// - /// This is mostly used as a + /// Will not affect the space taken by `grow` (All atomics marked as grow will always grow + /// equally to fill the available space). fn atom_max_size(self, max_size: Vec2) -> Atomic<'a>; /// Set the maximum width of this atomic. + /// + /// Will not affect the space taken by `grow` (All atomics marked as grow will always grow + /// equally to fill the available space). fn atom_max_width(self, max_width: f32) -> Atomic<'a>; /// Set the maximum height of this atomic. diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs index d57599114ef..a0bb4006322 100644 --- a/crates/egui/src/atomics/atomic_kind.rs +++ b/crates/egui/src/atomics/atomic_kind.rs @@ -45,11 +45,11 @@ pub enum AtomicKind<'a> { /// /// Example: /// ``` - /// # use egui::{AtomicKind, Button, Id, __run_test_ui}; + /// # use egui::{AtomicExt, AtomicKind, Atomic, Button, Id, __run_test_ui}; /// # use emath::Vec2; /// # __run_test_ui(|ui| { /// let id = Id::new("my_button"); - /// let response = Button::new(("Hi!", AtomicKind::Custom(id, Vec2::splat(18.0)))).atomic_ui(ui); + /// let response = Button::new(("Hi!", Atomic::custom(id, Vec2::splat(18.0)))).atomic_ui(ui); /// /// let rect = response.get_rect(id); /// if let Some(rect) = rect { @@ -57,7 +57,7 @@ pub enum AtomicKind<'a> { /// } /// # }); /// ``` - Custom(Id, Vec2), + Custom(Id), } impl<'a> AtomicKind<'a> { @@ -69,10 +69,6 @@ impl<'a> AtomicKind<'a> { AtomicKind::Image(image.into()) } - pub fn custom(id: Id, size: Vec2) -> Self { - AtomicKind::Custom(id, size) - } - /// Turn this [`AtomicKind`] into a [`SizedAtomicKind`]. /// /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`]. @@ -87,7 +83,7 @@ impl<'a> AtomicKind<'a> { AtomicKind::Text(text) => { let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); ( - galley.size(), // TODO(lucasmerlin): calculate the preferred size + galley.size(), // TODO(#5762): calculate the preferred size SizedAtomicKind::Text(galley), ) } @@ -96,7 +92,7 @@ impl<'a> AtomicKind<'a> { let size = size.unwrap_or(Vec2::ZERO); (size, SizedAtomicKind::Image(image, size)) } - AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), + AtomicKind::Custom(id) => (Vec2::ZERO, SizedAtomicKind::Custom(id)), AtomicKind::Empty => (Vec2::ZERO, SizedAtomicKind::Empty), } } diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atomic_layout.rs index b4a4e3384a2..0141bf77cc9 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atomic_layout.rs @@ -406,7 +406,7 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { SizedAtomicKind::Image(image, _) => { image.paint_at(ui, rect); } - SizedAtomicKind::Custom(id, _) => { + SizedAtomicKind::Custom(id) => { debug_assert!( !response.custom_rects.iter().any(|(i, _)| *i == id), "Duplicate custom id" diff --git a/crates/egui/src/atomics/sized_atomic_kind.rs b/crates/egui/src/atomics/sized_atomic_kind.rs index 544cd2df46d..644be65fa5c 100644 --- a/crates/egui/src/atomics/sized_atomic_kind.rs +++ b/crates/egui/src/atomics/sized_atomic_kind.rs @@ -10,7 +10,7 @@ pub enum SizedAtomicKind<'a> { Empty, Text(Arc), Image(Image<'a>, Vec2), - Custom(Id, Vec2), + Custom(Id), } impl SizedAtomicKind<'_> { @@ -18,8 +18,8 @@ impl SizedAtomicKind<'_> { pub fn size(&self) -> Vec2 { match self { SizedAtomicKind::Text(galley) => galley.size(), - SizedAtomicKind::Image(_, size) | SizedAtomicKind::Custom(_, size) => *size, - SizedAtomicKind::Empty => Vec2::ZERO, + SizedAtomicKind::Image(_, size) => *size, + SizedAtomicKind::Empty | SizedAtomicKind::Custom(_) => Vec2::ZERO, } } } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 6b772c22373..0ed1063da01 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,7 +1,6 @@ -use crate::AtomicKind::Custom; use crate::{ - epaint, pos2, AtomicLayout, Atomics, Id, IntoAtomics, NumExt as _, Response, Sense, Shape, Ui, - Vec2, Widget, WidgetInfo, WidgetType, + epaint, pos2, Atomic, AtomicLayout, Atomics, Id, IntoAtomics, NumExt as _, Response, Sense, + Shape, Ui, Vec2, Widget, WidgetInfo, WidgetType, }; // TODO(emilk): allow checkbox without a text label @@ -66,7 +65,7 @@ impl Widget for Checkbox<'_> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::checkbox"); - atomics.push_left(Custom(rect_id, icon_size)); + atomics.push_left(Atomic::custom(rect_id, icon_size)); let text = atomics.text(); diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index bfac411b36f..cecfb87dd74 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -1,6 +1,6 @@ use crate::{ - epaint, AtomicKind, AtomicLayout, Atomics, Id, IntoAtomics, NumExt as _, Response, Sense, Ui, - Vec2, Widget, WidgetInfo, WidgetType, + epaint, Atomic, AtomicLayout, Atomics, Id, IntoAtomics, NumExt as _, Response, Sense, Ui, Vec2, + Widget, WidgetInfo, WidgetType, }; /// One out of several alternatives, either selected or not. @@ -54,7 +54,7 @@ impl Widget for RadioButton<'_> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::radio_button"); - atomics.push_left(AtomicKind::Custom(rect_id, icon_size)); + atomics.push_left(Atomic::custom(rect_id, icon_size)); let text = atomics.text(); From 7736c5b0ac69bd00d5bba8e386278c0dacd7254d Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 10:04:03 +0200 Subject: [PATCH 65/77] Add more tests --- tests/egui_tests/tests/snapshots/grow_all.png | 3 ++ .../egui_tests/tests/snapshots/max_width.png | 3 ++ .../tests/snapshots/max_width_and_grow.png | 3 ++ .../tests/snapshots/shrink_first_text.png | 3 ++ .../tests/snapshots/shrink_last_text.png | 3 ++ tests/egui_tests/tests/test_atomics.rs | 50 ++++++++++++++----- 6 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/grow_all.png create mode 100644 tests/egui_tests/tests/snapshots/max_width.png create mode 100644 tests/egui_tests/tests/snapshots/max_width_and_grow.png create mode 100644 tests/egui_tests/tests/snapshots/shrink_first_text.png create mode 100644 tests/egui_tests/tests/snapshots/shrink_last_text.png diff --git a/tests/egui_tests/tests/snapshots/grow_all.png b/tests/egui_tests/tests/snapshots/grow_all.png new file mode 100644 index 00000000000..7ef69ea16d3 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/grow_all.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34f0c49cef96c7c3d08dbe835efd9366a4ced6ad2c6aa7facb0de08fd1a44648 +size 14011 diff --git a/tests/egui_tests/tests/snapshots/max_width.png b/tests/egui_tests/tests/snapshots/max_width.png new file mode 100644 index 00000000000..bab2f387691 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/max_width.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90cfa6e9be28ef538491ad94615e162ecc107df6a320084ec30840a75660ac35 +size 8759 diff --git a/tests/egui_tests/tests/snapshots/max_width_and_grow.png b/tests/egui_tests/tests/snapshots/max_width_and_grow.png new file mode 100644 index 00000000000..077bccbd83f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/max_width_and_grow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:effb4a69a7a6af12614be59a0afb0be2d2ebad402da3d7ee99fa25ae350bf4a0 +size 8761 diff --git a/tests/egui_tests/tests/snapshots/shrink_first_text.png b/tests/egui_tests/tests/snapshots/shrink_first_text.png new file mode 100644 index 00000000000..c9196ea24c8 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/shrink_first_text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf5032b2a08f993ae023934715222fe8d35a3a2e5cc09026d9e7ea3c296a9dc7 +size 11609 diff --git a/tests/egui_tests/tests/snapshots/shrink_last_text.png b/tests/egui_tests/tests/snapshots/shrink_last_text.png new file mode 100644 index 00000000000..038b70a2fd8 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/shrink_last_text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84d0c37a198fb56d8608a201dbe7ad19e7de7802bd5110316b36228e14b5f330 +size 12140 diff --git a/tests/egui_tests/tests/test_atomics.rs b/tests/egui_tests/tests/test_atomics.rs index 13da693d539..dab0002c372 100644 --- a/tests/egui_tests/tests/test_atomics.rs +++ b/tests/egui_tests/tests/test_atomics.rs @@ -1,18 +1,44 @@ -use egui::{Align, AtomicExt, Button, Layout, Ui, Vec2, Widget}; -use egui_kittest::HarnessBuilder; +use egui::{Align, AtomicExt as _, Button, Layout, TextWrapMode, Ui, Vec2}; +use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults}; #[test] fn test_atomics() { - single_test("max_width_and_grow", |ui| { - _ = Button::new(( - "hello my name is".atom_max_width(10.0).atom_grow(true), - "world", - )) - .ui(ui); - }); + let mut results = SnapshotResults::new(); + + results.add(single_test("max_width", |ui| { + ui.add(Button::new(( + "max width not grow".atom_max_width(30.0), + "other text", + ))); + })); + results.add(single_test("max_width_and_grow", |ui| { + ui.add(Button::new(( + "max width and grow".atom_max_width(30.0).atom_grow(true), + "other text", + ))); + })); + results.add(single_test("shrink_first_text", |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ui.add(Button::new(("this should shrink", "this shouldn't"))); + })); + results.add(single_test("shrink_last_text", |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ui.add(Button::new(( + "this shouldn't shrink", + "this should".atom_shrink(true), + ))); + })); + results.add(single_test("grow_all", |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ui.add(Button::new(( + "I grow".atom_grow(true), + "I also grow".atom_grow(true), + "I grow as well".atom_grow(true), + ))); + })); } -fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) { +fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) -> SnapshotResult { let mut harness = HarnessBuilder::default() .with_size(Vec2::new(400.0, 200.0)) .build_ui(move |ui| { @@ -32,7 +58,5 @@ fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) { }); }); - // harness.fit_contents(); - - harness.snapshot(name); + harness.try_snapshot(name) } From 689b65ec75ffdafa88d57f8f9c0489c2be18386b Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 10:31:40 +0200 Subject: [PATCH 66/77] Ensure max_size limits size --- crates/egui/src/atomics/atomic.rs | 15 ++++++++++++++- crates/egui/src/atomics/atomic_ext.rs | 2 ++ .../egui_tests/tests/snapshots/size_max_size.png | 3 +++ tests/egui_tests/tests/test_atomics.rs | 9 +++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/egui_tests/tests/snapshots/size_max_size.png diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atomic.rs index c055f0ed98d..a6fbef63e90 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atomic.rs @@ -14,10 +14,15 @@ use epaint::text::TextWrapMode; /// ``` #[derive(Clone, Debug)] pub struct Atomic<'a> { + /// See [`crate::AtomicExt::atom_size`] pub size: Option, + /// See [`crate::AtomicExt::atom_max_size`] pub max_size: Vec2, + /// See [`crate::AtomicExt::atom_grow`] pub grow: bool, + /// See [`crate::AtomicExt::atom_shrink`] pub shrink: bool, + /// The atom type pub kind: AtomicKind<'a>, } @@ -65,13 +70,21 @@ impl<'a> Atomic<'a> { wrap_mode = Some(TextWrapMode::Extend); } available_size = available_size.at_most(self.max_size); + if let Some(size) = self.size { + available_size = available_size.at_most(size); + } if self.max_size.x.is_finite() { wrap_mode = Some(TextWrapMode::Truncate); } let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode); + + let size = self + .size + .map_or_else(|| kind.size(), |s| s.at_most(self.max_size)); + SizedAtomic { - size: self.size.unwrap_or_else(|| kind.size()), + size, preferred_size: preferred, grow: self.grow, kind, diff --git a/crates/egui/src/atomics/atomic_ext.rs b/crates/egui/src/atomics/atomic_ext.rs index ac4f977c75d..64f53fe536b 100644 --- a/crates/egui/src/atomics/atomic_ext.rs +++ b/crates/egui/src/atomics/atomic_ext.rs @@ -11,6 +11,8 @@ pub trait AtomicExt<'a> { /// If [`Atomic::shrink`] is `true`, this will be the maximum width. /// If both are true, the width will have no effect. /// + /// [`Self::atom_max_size`] will limit size. + /// /// See [`crate::AtomicKind`] docs to see how the size affects the different types. fn atom_size(self, size: Vec2) -> Atomic<'a>; diff --git a/tests/egui_tests/tests/snapshots/size_max_size.png b/tests/egui_tests/tests/snapshots/size_max_size.png new file mode 100644 index 00000000000..3ea8feab005 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/size_max_size.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6a7555290f6121d6e48657e3ae810976b540ee9328909aca2d6c078b3d76ab4 +size 8735 diff --git a/tests/egui_tests/tests/test_atomics.rs b/tests/egui_tests/tests/test_atomics.rs index dab0002c372..85284ce0431 100644 --- a/tests/egui_tests/tests/test_atomics.rs +++ b/tests/egui_tests/tests/test_atomics.rs @@ -36,6 +36,15 @@ fn test_atomics() { "I grow as well".atom_grow(true), ))); })); + results.add(single_test("size_max_size", |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ui.add(Button::new(( + "size and max size" + .atom_size(Vec2::new(80.0, 80.0)) + .atom_max_size(Vec2::new(20.0, 20.0)), + "other text".atom_grow(true), + ))); + })); } fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) -> SnapshotResult { From 443eca8b2d0069da7407888e458896abc7cdf352 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 10:32:00 +0200 Subject: [PATCH 67/77] Change text() to return Cow --- crates/egui/src/atomics/atomics.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atomics.rs index 402724e8b21..ab5c2179aa2 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atomics.rs @@ -1,5 +1,6 @@ use crate::{Atomic, AtomicKind, Image, WidgetText}; use smallvec::SmallVec; +use std::borrow::Cow; use std::ops::{Deref, DerefMut}; // Rarely there should be more than 2 atomics in one Widget. @@ -28,15 +29,16 @@ impl<'a> Atomics<'a> { /// Concatenate and return the text contents. // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. // in a submenu button there is a right text '⏵' which is now passed to the screen reader. - pub fn text(&self) -> Option { - let mut string: Option = None; + pub fn text(&self) -> Option> { + let mut string: Option> = None; for atomic in &self.0 { if let AtomicKind::Text(text) = &atomic.kind { if let Some(string) = &mut string { + let string = string.to_mut(); string.push(' '); string.push_str(text.text()); } else { - string = Some(text.text().to_owned()); + string = Some(Cow::Borrowed(text.text())); } } } From e3af0fb758490d57666d8f314e354218edb84bd9 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 10:53:23 +0200 Subject: [PATCH 68/77] Rename atomic to atom --- .../egui/src/atomics/{atomic.rs => atom.rs} | 56 +++--- crates/egui/src/atomics/atom_ext.rs | 107 ++++++++++++ crates/egui/src/atomics/atom_kind.rs | 120 +++++++++++++ .../{atomic_layout.rs => atom_layout.rs} | 162 +++++++++--------- crates/egui/src/atomics/atomic_ext.rs | 107 ------------ crates/egui/src/atomics/atomic_kind.rs | 120 ------------- .../egui/src/atomics/{atomics.rs => atoms.rs} | 136 +++++++-------- crates/egui/src/atomics/mod.rs | 28 +-- crates/egui/src/atomics/sized_atom.rs | 26 +++ ...ized_atomic_kind.rs => sized_atom_kind.rs} | 12 +- crates/egui/src/atomics/sized_atomic.rs | 26 --- crates/egui/src/containers/menu.rs | 10 +- crates/egui/src/ui.rs | 12 +- crates/egui/src/widgets/button.rs | 52 +++--- crates/egui/src/widgets/checkbox.rs | 18 +- crates/egui/src/widgets/radio_button.rs | 21 +-- .../tests/{test_atomics.rs => test_atoms.rs} | 4 +- tests/egui_tests/tests/test_widgets.rs | 24 ++- 18 files changed, 518 insertions(+), 523 deletions(-) rename crates/egui/src/atomics/{atomic.rs => atom.rs} (63%) create mode 100644 crates/egui/src/atomics/atom_ext.rs create mode 100644 crates/egui/src/atomics/atom_kind.rs rename crates/egui/src/atomics/{atomic_layout.rs => atom_layout.rs} (72%) delete mode 100644 crates/egui/src/atomics/atomic_ext.rs delete mode 100644 crates/egui/src/atomics/atomic_kind.rs rename crates/egui/src/atomics/{atomics.rs => atoms.rs} (50%) create mode 100644 crates/egui/src/atomics/sized_atom.rs rename crates/egui/src/atomics/{sized_atomic_kind.rs => sized_atom_kind.rs} (52%) delete mode 100644 crates/egui/src/atomics/sized_atomic.rs rename tests/egui_tests/tests/{test_atomics.rs => test_atoms.rs} (96%) diff --git a/crates/egui/src/atomics/atomic.rs b/crates/egui/src/atomics/atom.rs similarity index 63% rename from crates/egui/src/atomics/atomic.rs rename to crates/egui/src/atomics/atom.rs index a6fbef63e90..e3045393bcc 100644 --- a/crates/egui/src/atomics/atomic.rs +++ b/crates/egui/src/atomics/atom.rs @@ -1,71 +1,71 @@ -use crate::{AtomicKind, Id, SizedAtomic, Ui}; +use crate::{AtomKind, Id, SizedAtom, Ui}; use emath::{NumExt as _, Vec2}; use epaint::text::TextWrapMode; /// A low-level ui building block. /// /// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience. -/// You can directly call the `a_*` methods on anything that implements `Into`. +/// You can directly call the `a_*` methods on anything that implements `Into`. /// ``` /// # use egui::{Image, emath::Vec2}; -/// use egui::AtomicExt as _; -/// let string_atomic = "Hello".atom_grow(true); -/// let image_atomic = Image::new("some_image_url").atom_size(Vec2::splat(20.0)); +/// use egui::AtomExt as _; +/// let string_atom = "Hello".atom_grow(true); +/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0)); /// ``` #[derive(Clone, Debug)] -pub struct Atomic<'a> { - /// See [`crate::AtomicExt::atom_size`] +pub struct Atom<'a> { + /// See [`crate::AtomExt::atom_size`] pub size: Option, - /// See [`crate::AtomicExt::atom_max_size`] + /// See [`crate::AtomExt::atom_max_size`] pub max_size: Vec2, - /// See [`crate::AtomicExt::atom_grow`] + /// See [`crate::AtomExt::atom_grow`] pub grow: bool, - /// See [`crate::AtomicExt::atom_shrink`] + /// See [`crate::AtomExt::atom_shrink`] pub shrink: bool, /// The atom type - pub kind: AtomicKind<'a>, + pub kind: AtomKind<'a>, } -impl Default for Atomic<'_> { +impl Default for Atom<'_> { fn default() -> Self { - Atomic { + Atom { size: None, max_size: Vec2::INFINITY, grow: false, shrink: false, - kind: AtomicKind::Empty, + kind: AtomKind::Empty, } } } -impl<'a> Atomic<'a> { - /// Create an empty [`Atomic`] marked as `grow`. +impl<'a> Atom<'a> { + /// Create an empty [`Atom`] marked as `grow`. /// - /// This will expand in size, allowing all preceding atomics to be left-aligned, - /// and all following atomics to be right-aligned + /// This will expand in size, allowing all preceding atoms to be left-aligned, + /// and all following atoms to be right-aligned pub fn grow() -> Self { - Atomic { + Atom { grow: true, ..Default::default() } } - /// Create a [`AtomicKind::Custom`] with a specific size. + /// Create a [`AtomKind::Custom`] with a specific size. pub fn custom(id: Id, size: impl Into) -> Self { - Atomic { + Atom { size: Some(size.into()), - kind: AtomicKind::Custom(id), + kind: AtomKind::Custom(id), ..Default::default() } } - /// Turn this into a [`SizedAtomic`]. + /// Turn this into a [`SizedAtom`]. pub fn into_sized( self, ui: &Ui, mut available_size: Vec2, mut wrap_mode: Option, - ) -> SizedAtomic<'a> { + ) -> SizedAtom<'a> { if !self.shrink && self.max_size.x.is_infinite() { wrap_mode = Some(TextWrapMode::Extend); } @@ -83,7 +83,7 @@ impl<'a> Atomic<'a> { .size .map_or_else(|| kind.size(), |s| s.at_most(self.max_size)); - SizedAtomic { + SizedAtom { size, preferred_size: preferred, grow: self.grow, @@ -92,12 +92,12 @@ impl<'a> Atomic<'a> { } } -impl<'a, T> From for Atomic<'a> +impl<'a, T> From for Atom<'a> where - T: Into>, + T: Into>, { fn from(value: T) -> Self { - Atomic { + Atom { kind: value.into(), ..Default::default() } diff --git a/crates/egui/src/atomics/atom_ext.rs b/crates/egui/src/atomics/atom_ext.rs new file mode 100644 index 00000000000..f05d0d56d4e --- /dev/null +++ b/crates/egui/src/atomics/atom_ext.rs @@ -0,0 +1,107 @@ +use crate::{Atom, FontSelection, Ui}; +use emath::Vec2; + +/// A trait for conveniently building [`Atom`]s. +/// +/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`]. +pub trait AtomExt<'a> { + /// Set the atom to a fixed size. + /// + /// If [`Atom::grow`] is `true`, this will be the minimum width. + /// If [`Atom::shrink`] is `true`, this will be the maximum width. + /// If both are true, the width will have no effect. + /// + /// [`Self::atom_max_size`] will limit size. + /// + /// See [`crate::AtomKind`] docs to see how the size affects the different types. + fn atom_size(self, size: Vec2) -> Atom<'a>; + + /// Grow this atom to the available space. + /// + /// This will affect the size of the [`Atom`] in the main direction. Since + /// [`AtomLayout`] today only supports horizontal layout, it will affect the width. + /// + /// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the + /// remaining space. + fn atom_grow(self, grow: bool) -> Atom<'a>; + + /// Shrink this atom if there isn't enough space. + /// + /// This will affect the size of the [`Atom`] in the main direction. Since + /// [`AtomLayout`] today only supports horizontal layout, it will affect the width. + /// + /// NOTE: Only a single [`Atom`] may shrink for each widget. + /// + /// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first + /// `AtomKind::Text` is set to shrink. + fn atom_shrink(self, shrink: bool) -> Atom<'a>; + + /// Set the maximum size of this atom. + /// + /// Will not affect the space taken by `grow` (All atoms marked as grow will always grow + /// equally to fill the available space). + fn atom_max_size(self, max_size: Vec2) -> Atom<'a>; + + /// Set the maximum width of this atom. + /// + /// Will not affect the space taken by `grow` (All atoms marked as grow will always grow + /// equally to fill the available space). + fn atom_max_width(self, max_width: f32) -> Atom<'a>; + + /// Set the maximum height of this atom. + fn atom_max_height(self, max_height: f32) -> Atom<'a>; + + /// Set the max height of this atom to match the font size. + /// + /// This is useful for e.g. limiting the height of icons in buttons. + fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a> + where + Self: Sized, + { + let font_selection = FontSelection::default(); + let font_id = font_selection.resolve(ui.style()); + let height = ui.fonts(|f| f.row_height(&font_id)); + self.atom_max_height(height) + } +} + +impl<'a, T> AtomExt<'a> for T +where + T: Into> + Sized, +{ + fn atom_size(self, size: Vec2) -> Atom<'a> { + let mut atom = self.into(); + atom.size = Some(size); + atom + } + + fn atom_grow(self, grow: bool) -> Atom<'a> { + let mut atom = self.into(); + atom.grow = grow; + atom + } + + fn atom_shrink(self, shrink: bool) -> Atom<'a> { + let mut atom = self.into(); + atom.shrink = shrink; + atom + } + + fn atom_max_size(self, max_size: Vec2) -> Atom<'a> { + let mut atom = self.into(); + atom.max_size = max_size; + atom + } + + fn atom_max_width(self, max_width: f32) -> Atom<'a> { + let mut atom = self.into(); + atom.max_size.x = max_width; + atom + } + + fn atom_max_height(self, max_height: f32) -> Atom<'a> { + let mut atom = self.into(); + atom.max_size.y = max_height; + atom + } +} diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs new file mode 100644 index 00000000000..858c89dfe0f --- /dev/null +++ b/crates/egui/src/atomics/atom_kind.rs @@ -0,0 +1,120 @@ +use crate::{Id, Image, ImageSource, SizedAtomKind, TextStyle, Ui, WidgetText}; +use emath::Vec2; +use epaint::text::TextWrapMode; + +/// The different kinds of [`crate::Atom`]s. +#[derive(Clone, Default, Debug)] +pub enum AtomKind<'a> { + /// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space. + #[default] + Empty, + + /// Text atom. + /// + /// Truncation within [`crate::AtomLayout`] works like this: + /// - + /// - if `wrap_mode` is not Extend + /// - if no atom is `shrink` + /// - the first text atom is selected and will be marked as `shrink` + /// - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode + /// - any other text atoms will have `wrap_mode` extend + /// - if `wrap_mode` is extend, Text will extend as expected. + /// + /// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or + /// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom + /// that is not `shrink` will have unexpected results. + /// + /// The size is determined by converting the [`WidgetText`] into a galley and using the galleys + /// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`] + /// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`. + /// [`crate::AtomExt::atom_max_height`] has no effect on text. + Text(WidgetText), + + /// Image atom. + /// + /// By default the size is determined via [`Image::calc_size`]. + /// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size. + /// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the + /// default font height, which is convenient for icons. + Image(Image<'a>), + + /// For custom rendering. + /// + /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a + /// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content. + /// + /// Example: + /// ``` + /// # use egui::{AtomExt, AtomKind, Atom, 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 rect = response.get_rect(id); + /// if let Some(rect) = rect { + /// ui.put(rect, Button::new("⏵")); + /// } + /// # }); + /// ``` + Custom(Id), +} + +impl<'a> AtomKind<'a> { + pub fn text(text: impl Into) -> Self { + AtomKind::Text(text.into()) + } + + pub fn image(image: impl Into>) -> Self { + AtomKind::Image(image.into()) + } + + /// Turn this [`AtomKind`] into a [`SizedAtomKind`]. + /// + /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`]. + /// The first returned argument is the preferred size. + pub fn into_sized( + self, + ui: &Ui, + available_size: Vec2, + wrap_mode: Option, + ) -> (Vec2, SizedAtomKind<'a>) { + match self { + AtomKind::Text(text) => { + let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); + ( + galley.size(), // TODO(#5762): calculate the preferred size + SizedAtomKind::Text(galley), + ) + } + AtomKind::Image(image) => { + let size = image.load_and_calc_size(ui, available_size); + let size = size.unwrap_or(Vec2::ZERO); + (size, SizedAtomKind::Image(image, size)) + } + AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)), + AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty), + } + } +} + +impl<'a> From> for AtomKind<'a> { + fn from(value: ImageSource<'a>) -> Self { + AtomKind::Image(value.into()) + } +} + +impl<'a> From> for AtomKind<'a> { + fn from(value: Image<'a>) -> Self { + AtomKind::Image(value) + } +} + +impl From for AtomKind<'_> +where + T: Into, +{ + fn from(value: T) -> Self { + AtomKind::Text(value.into()) + } +} diff --git a/crates/egui/src/atomics/atomic_layout.rs b/crates/egui/src/atomics/atom_layout.rs similarity index 72% rename from crates/egui/src/atomics/atomic_layout.rs rename to crates/egui/src/atomics/atom_layout.rs index 0141bf77cc9..c425ec36f76 100644 --- a/crates/egui/src/atomics/atomic_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -1,7 +1,7 @@ -use crate::atomics::ATOMICS_SMALL_VEC_SIZE; +use crate::atomics::ATOMS_SMALL_VEC_SIZE; use crate::{ - AtomicKind, Atomics, Frame, Id, Image, IntoAtomics, Response, Sense, SizedAtomic, - SizedAtomicKind, Ui, Widget, + AtomKind, Atoms, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom, SizedAtomKind, Ui, + Widget, }; use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2}; use epaint::text::TextWrapMode; @@ -12,26 +12,26 @@ use std::sync::Arc; /// Intra-widget layout utility. /// -/// Used to lay out and paint [`crate::Atomic`]s. +/// Used to lay out and paint [`crate::Atom`]s. /// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`]. /// You can use it to make your own widgets. /// -/// Painting the atomics can be split in two phases: -/// - [`AtomicLayout::allocate`] +/// Painting the atoms can be split in two phases: +/// - [`AtomLayout::allocate`] /// - calculates sizes /// - converts texts to [`Galley`]s /// - allocates a [`Response`] -/// - returns a [`AllocatedAtomicLayout`] -/// - [`AllocatedAtomicLayout::paint`] +/// - returns a [`AllocatedAtomLayout`] +/// - [`AllocatedAtomLayout::paint`] /// - paints the [`Frame`] -/// - calculates individual [`crate::Atomic`] positions -/// - paints each single atomic +/// - calculates individual [`crate::Atom`] positions +/// - paints each single atom /// /// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the -/// [`AllocatedAtomicLayout`] for interaction styling. -pub struct AtomicLayout<'a> { +/// [`AllocatedAtomLayout`] for interaction styling. +pub struct AtomLayout<'a> { id: Option, - pub atomics: Atomics<'a>, + pub atoms: Atoms<'a>, gap: Option, pub(crate) frame: Frame, pub(crate) sense: Sense, @@ -41,17 +41,17 @@ pub struct AtomicLayout<'a> { align2: Option, } -impl Default for AtomicLayout<'_> { +impl Default for AtomLayout<'_> { fn default() -> Self { Self::new(()) } } -impl<'a> AtomicLayout<'a> { - pub fn new(atomics: impl IntoAtomics<'a>) -> Self { +impl<'a> AtomLayout<'a> { + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { Self { id: None, - atomics: atomics.into_atomics(), + atoms: atoms.into_atoms(), gap: None, frame: Frame::default(), sense: Sense::hover(), @@ -62,7 +62,7 @@ impl<'a> AtomicLayout<'a> { } } - /// Set the gap between atomics. + /// Set the gap between atoms. /// /// Default: `Spacing::icon_spacing` #[inline] @@ -108,11 +108,11 @@ impl<'a> AtomicLayout<'a> { self } - /// Set the [`TextWrapMode`] for the [`crate::Atomic`] marked as `shrink`. + /// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`. /// - /// Only a single [`crate::Atomic`] may shrink. If this (or `ui.wrap_mode()`) is not + /// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most) - /// [`AtomicKind::Text`] will be set to shrink. + /// [`AtomKind::Text`] will be set to shrink. #[inline] pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { self.wrap_mode = Some(wrap_mode); @@ -121,7 +121,7 @@ impl<'a> AtomicLayout<'a> { /// Set the [`Align2`]. /// - /// This will align the [`crate::Atomic`]s within the [`Rect`] returned by [`Ui::allocate_space`]. + /// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`]. /// /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) @@ -132,18 +132,18 @@ impl<'a> AtomicLayout<'a> { self } - /// [`AtomicLayout::allocate`] and [`AllocatedAtomicLayout::paint`] in one go. - pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { + /// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go. + pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse { self.allocate(ui).paint(ui) } /// Calculate sizes, create [`Galley`]s and allocate a [`Response`]. /// - /// Use the returned [`AllocatedAtomicLayout`] for painting. - pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomicLayout<'a> { + /// Use the returned [`AllocatedAtomLayout`] for painting. + pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> { let Self { id, - mut atomics, + mut atoms, gap, frame, sense, @@ -158,13 +158,13 @@ impl<'a> AtomicLayout<'a> { // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. // If none is found, mark the first text item as `shrink`. if wrap_mode != TextWrapMode::Extend { - let any_shrink = atomics.iter().any(|a| a.shrink); + let any_shrink = atoms.iter().any(|a| a.shrink); if !any_shrink { - let first_text = atomics + let first_text = atoms .iter_mut() - .find(|a| matches!(a.kind, AtomicKind::Text(..))); - if let Some(atomic) = first_text { - atomic.shrink = true; // Will make the text truncate or shrink depending on wrap_mode + .find(|a| matches!(a.kind, AtomKind::Text(..))); + if let Some(atom) = first_text { + atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode } } } @@ -197,13 +197,13 @@ impl<'a> AtomicLayout<'a> { Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()]) }); - if atomics.len() > 1 { - let gap_space = gap * (atomics.len() as f32 - 1.0); + if atoms.len() > 1 { + let gap_space = gap * (atoms.len() as f32 - 1.0); desired_width += gap_space; preferred_width += gap_space; } - for (idx, item) in atomics.into_iter().enumerate() { + for (idx, item) in atoms.into_iter().enumerate() { if item.grow { grow_count += 1; } @@ -258,8 +258,8 @@ impl<'a> AtomicLayout<'a> { response.intrinsic_size = Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size)); - AllocatedAtomicLayout { - sized_atomics: sized_items, + AllocatedAtomLayout { + sized_atoms: sized_items, frame, fallback_text_color, response, @@ -271,10 +271,10 @@ impl<'a> AtomicLayout<'a> { } } -/// Instructions for painting an [`AtomicLayout`]. +/// Instructions for painting an [`AtomLayout`]. #[derive(Clone, Debug)] -pub struct AllocatedAtomicLayout<'a> { - pub sized_atomics: SmallVec<[SizedAtomic<'a>; ATOMICS_SMALL_VEC_SIZE]>, +pub struct AllocatedAtomLayout<'a> { + pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>, pub frame: Frame, pub fallback_text_color: Color32, pub response: Response, @@ -285,18 +285,18 @@ pub struct AllocatedAtomicLayout<'a> { gap: f32, } -impl<'atomic> AllocatedAtomicLayout<'atomic> { - pub fn iter_kinds(&self) -> impl Iterator> { - self.sized_atomics.iter().map(|atomic| &atomic.kind) +impl<'atom> AllocatedAtomLayout<'atom> { + pub fn iter_kinds(&self) -> impl Iterator> { + self.sized_atoms.iter().map(|atom| &atom.kind) } - pub fn iter_kinds_mut(&mut self) -> impl Iterator> { - self.sized_atomics.iter_mut().map(|atomic| &mut atomic.kind) + pub fn iter_kinds_mut(&mut self) -> impl Iterator> { + self.sized_atoms.iter_mut().map(|atom| &mut atom.kind) } - pub fn iter_images(&self) -> impl Iterator> { + pub fn iter_images(&self) -> impl Iterator> { self.iter_kinds().filter_map(|kind| { - if let SizedAtomicKind::Image(image, _) = kind { + if let SizedAtomKind::Image(image, _) = kind { Some(image) } else { None @@ -304,9 +304,9 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { }) } - pub fn iter_images_mut(&mut self) -> impl Iterator> { + pub fn iter_images_mut(&mut self) -> impl Iterator> { self.iter_kinds_mut().filter_map(|kind| { - if let SizedAtomicKind::Image(image, _) = kind { + if let SizedAtomKind::Image(image, _) = kind { Some(image) } else { None @@ -314,9 +314,9 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { }) } - pub fn iter_texts(&self) -> impl Iterator> + use<'atomic, '_> { + pub fn iter_texts(&self) -> impl Iterator> + use<'atom, '_> { self.iter_kinds().filter_map(|kind| { - if let SizedAtomicKind::Text(text) = kind { + if let SizedAtomKind::Text(text) = kind { Some(text) } else { None @@ -324,9 +324,9 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { }) } - pub fn iter_texts_mut(&mut self) -> impl Iterator> + use<'atomic, '_> { + pub fn iter_texts_mut(&mut self) -> impl Iterator> + use<'atom, '_> { self.iter_kinds_mut().filter_map(|kind| { - if let SizedAtomicKind::Text(text) = kind { + if let SizedAtomKind::Text(text) = kind { Some(text) } else { None @@ -336,7 +336,7 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { pub fn map_kind(&mut self, mut f: F) where - F: FnMut(SizedAtomicKind<'atomic>) -> SizedAtomicKind<'atomic>, + F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>, { for kind in self.iter_kinds_mut() { *kind = f(std::mem::take(kind)); @@ -345,21 +345,21 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { pub fn map_images(&mut self, mut f: F) where - F: FnMut(Image<'atomic>) -> Image<'atomic>, + F: FnMut(Image<'atom>) -> Image<'atom>, { self.map_kind(|kind| { - if let SizedAtomicKind::Image(image, size) = kind { - SizedAtomicKind::Image(f(image), size) + if let SizedAtomKind::Image(image, size) = kind { + SizedAtomKind::Image(f(image), size) } else { kind } }); } - /// Paint the [`Frame`] and individual [`crate::Atomic`]s. - pub fn paint(self, ui: &Ui) -> AtomicLayoutResponse { + /// Paint the [`Frame`] and individual [`crate::Atom`]s. + pub fn paint(self, ui: &Ui) -> AtomLayoutResponse { let Self { - sized_atomics, + sized_atoms, frame, fallback_text_color, response, @@ -385,9 +385,9 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { let mut cursor = aligned_rect.left(); - let mut response = AtomicLayoutResponse::empty(response); + let mut response = AtomLayoutResponse::empty(response); - for sized in sized_atomics { + for sized in sized_atoms { let size = sized.size; let growth = if sized.is_grow() { grow_width } else { 0.0 }; @@ -400,20 +400,20 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { let rect = align.align_size_within_rect(size, frame); match sized.kind { - SizedAtomicKind::Text(galley) => { + SizedAtomKind::Text(galley) => { ui.painter().galley(rect.min, galley, fallback_text_color); } - SizedAtomicKind::Image(image, _) => { + SizedAtomKind::Image(image, _) => { image.paint_at(ui, rect); } - SizedAtomicKind::Custom(id) => { + SizedAtomKind::Custom(id) => { debug_assert!( !response.custom_rects.iter().any(|(i, _)| *i == id), "Duplicate custom id" ); response.custom_rects.push((id, rect)); } - SizedAtomicKind::Empty => {} + SizedAtomKind::Empty => {} } } @@ -421,19 +421,19 @@ impl<'atomic> AllocatedAtomicLayout<'atomic> { } } -/// Response from a [`AtomicLayout::show`] or [`AllocatedAtomicLayout::paint`]. +/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`]. /// -/// Use the `custom_rects` together with [`AtomicKind::Custom`] to add child widgets to a widget. +/// Use the `custom_rects` together with [`AtomKind::Custom`] to add child widgets to a widget. /// /// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible. #[derive(Clone, Debug)] -pub struct AtomicLayoutResponse { +pub struct AtomLayoutResponse { pub response: Response, // There should rarely be more than one custom rect. custom_rects: SmallVec<[(Id, Rect); 1]>, } -impl AtomicLayoutResponse { +impl AtomLayoutResponse { pub fn empty(response: Response) -> Self { Self { response, @@ -452,36 +452,36 @@ impl AtomicLayoutResponse { } } -impl Widget for AtomicLayout<'_> { +impl Widget for AtomLayout<'_> { fn ui(self, ui: &mut Ui) -> Response { self.show(ui).response } } -impl<'a> Deref for AtomicLayout<'a> { - type Target = Atomics<'a>; +impl<'a> Deref for AtomLayout<'a> { + type Target = Atoms<'a>; fn deref(&self) -> &Self::Target { - &self.atomics + &self.atoms } } -impl DerefMut for AtomicLayout<'_> { +impl DerefMut for AtomLayout<'_> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.atomics + &mut self.atoms } } -impl<'a> Deref for AllocatedAtomicLayout<'a> { - type Target = [SizedAtomic<'a>]; +impl<'a> Deref for AllocatedAtomLayout<'a> { + type Target = [SizedAtom<'a>]; fn deref(&self) -> &Self::Target { - &self.sized_atomics + &self.sized_atoms } } -impl DerefMut for AllocatedAtomicLayout<'_> { +impl DerefMut for AllocatedAtomLayout<'_> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.sized_atomics + &mut self.sized_atoms } } diff --git a/crates/egui/src/atomics/atomic_ext.rs b/crates/egui/src/atomics/atomic_ext.rs deleted file mode 100644 index 64f53fe536b..00000000000 --- a/crates/egui/src/atomics/atomic_ext.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::{Atomic, FontSelection, Ui}; -use emath::Vec2; - -/// A trait for conveniently building [`Atomic`]s. -/// -/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`]. -pub trait AtomicExt<'a> { - /// Set the atomic to a fixed size. - /// - /// If [`Atomic::grow`] is `true`, this will be the minimum width. - /// If [`Atomic::shrink`] is `true`, this will be the maximum width. - /// If both are true, the width will have no effect. - /// - /// [`Self::atom_max_size`] will limit size. - /// - /// See [`crate::AtomicKind`] docs to see how the size affects the different types. - fn atom_size(self, size: Vec2) -> Atomic<'a>; - - /// Grow this atomic to the available space. - /// - /// This will affect the size of the [`Atomic`] in the main direction. Since - /// [`AtomicLayout`] today only supports horizontal layout, it will affect the width. - /// - /// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the - /// remaining space. - fn atom_grow(self, grow: bool) -> Atomic<'a>; - - /// Shrink this atomic if there isn't enough space. - /// - /// This will affect the size of the [`Atomic`] in the main direction. Since - /// [`AtomicLayout`] today only supports horizontal layout, it will affect the width. - /// - /// NOTE: Only a single [`Atomic`] may shrink for each widget. - /// - /// If no atomic was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first - /// `AtomKind::Text` is set to shrink. - fn atom_shrink(self, shrink: bool) -> Atomic<'a>; - - /// Set the maximum size of this atomic. - /// - /// Will not affect the space taken by `grow` (All atomics marked as grow will always grow - /// equally to fill the available space). - fn atom_max_size(self, max_size: Vec2) -> Atomic<'a>; - - /// Set the maximum width of this atomic. - /// - /// Will not affect the space taken by `grow` (All atomics marked as grow will always grow - /// equally to fill the available space). - fn atom_max_width(self, max_width: f32) -> Atomic<'a>; - - /// Set the maximum height of this atomic. - fn atom_max_height(self, max_height: f32) -> Atomic<'a>; - - /// Set the max height of this atomic to match the font size. - /// - /// This is useful for e.g. limiting the height of icons in buttons. - fn atom_max_height_font_size(self, ui: &Ui) -> Atomic<'a> - where - Self: Sized, - { - let font_selection = FontSelection::default(); - let font_id = font_selection.resolve(ui.style()); - let height = ui.fonts(|f| f.row_height(&font_id)); - self.atom_max_height(height) - } -} - -impl<'a, T> AtomicExt<'a> for T -where - T: Into> + Sized, -{ - fn atom_size(self, size: Vec2) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.size = Some(size); - atomic - } - - fn atom_grow(self, grow: bool) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.grow = grow; - atomic - } - - fn atom_shrink(self, shrink: bool) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.shrink = shrink; - atomic - } - - fn atom_max_size(self, max_size: Vec2) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.max_size = max_size; - atomic - } - - fn atom_max_width(self, max_width: f32) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.max_size.x = max_width; - atomic - } - - fn atom_max_height(self, max_height: f32) -> Atomic<'a> { - let mut atomic = self.into(); - atomic.max_size.y = max_height; - atomic - } -} diff --git a/crates/egui/src/atomics/atomic_kind.rs b/crates/egui/src/atomics/atomic_kind.rs deleted file mode 100644 index a0bb4006322..00000000000 --- a/crates/egui/src/atomics/atomic_kind.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::{Id, Image, ImageSource, SizedAtomicKind, TextStyle, Ui, WidgetText}; -use emath::Vec2; -use epaint::text::TextWrapMode; - -/// The different kinds of [`crate::Atomic`]s. -#[derive(Clone, Default, Debug)] -pub enum AtomicKind<'a> { - /// Empty, that can be used with [`crate::AtomicExt::atom_grow`] to reserve space. - #[default] - Empty, - - /// Text atomic. - /// - /// Truncation within [`crate::AtomicLayout`] works like this: - /// - - /// - if `wrap_mode` is not Extend - /// - if no atomic is `shrink` - /// - the first text atomic is selected and will be marked as `shrink` - /// - the atomic marked as `shrink` will shrink / wrap based on the selected wrap mode - /// - any other text atomics will have `wrap_mode` extend - /// - if `wrap_mode` is extend, Text will extend as expected. - /// - /// Unless [`crate::AtomicExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or - /// [`crate::AtomicLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atomic - /// that is not `shrink` will have unexpected results. - /// - /// The size is determined by converting the [`WidgetText`] into a galley and using the galleys - /// size. You can use [`crate::AtomicExt::atom_size`] to override this, and [`crate::AtomicExt::atom_max_width`] - /// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`. - /// [`crate::AtomicExt::atom_max_height`] has no effect on text. - Text(WidgetText), - - /// Image atomic. - /// - /// By default the size is determined via [`Image::calc_size`]. - /// You can use [`crate::AtomicExt::atom_max_size`] or [`crate::AtomicExt::atom_size`] to customize the size. - /// There is also a helper [`crate::AtomicExt::atom_max_height_font_size`] to set the max height to the - /// default font height, which is convenient for icons. - Image(Image<'a>), - - /// For custom rendering. - /// - /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomicLayoutResponse`] and use a - /// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content. - /// - /// Example: - /// ``` - /// # use egui::{AtomicExt, AtomicKind, Atomic, Button, Id, __run_test_ui}; - /// # use emath::Vec2; - /// # __run_test_ui(|ui| { - /// let id = Id::new("my_button"); - /// let response = Button::new(("Hi!", Atomic::custom(id, Vec2::splat(18.0)))).atomic_ui(ui); - /// - /// let rect = response.get_rect(id); - /// if let Some(rect) = rect { - /// ui.put(rect, Button::new("⏵")); - /// } - /// # }); - /// ``` - Custom(Id), -} - -impl<'a> AtomicKind<'a> { - pub fn text(text: impl Into) -> Self { - AtomicKind::Text(text.into()) - } - - pub fn image(image: impl Into>) -> Self { - AtomicKind::Image(image.into()) - } - - /// Turn this [`AtomicKind`] into a [`SizedAtomicKind`]. - /// - /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`]. - /// The first returned argument is the preferred size. - pub fn into_sized( - self, - ui: &Ui, - available_size: Vec2, - wrap_mode: Option, - ) -> (Vec2, SizedAtomicKind<'a>) { - match self { - AtomicKind::Text(text) => { - let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); - ( - galley.size(), // TODO(#5762): calculate the preferred size - SizedAtomicKind::Text(galley), - ) - } - AtomicKind::Image(image) => { - let size = image.load_and_calc_size(ui, available_size); - let size = size.unwrap_or(Vec2::ZERO); - (size, SizedAtomicKind::Image(image, size)) - } - AtomicKind::Custom(id) => (Vec2::ZERO, SizedAtomicKind::Custom(id)), - AtomicKind::Empty => (Vec2::ZERO, SizedAtomicKind::Empty), - } - } -} - -impl<'a> From> for AtomicKind<'a> { - fn from(value: ImageSource<'a>) -> Self { - AtomicKind::Image(value.into()) - } -} - -impl<'a> From> for AtomicKind<'a> { - fn from(value: Image<'a>) -> Self { - AtomicKind::Image(value) - } -} - -impl From for AtomicKind<'_> -where - T: Into, -{ - fn from(value: T) -> Self { - AtomicKind::Text(value.into()) - } -} diff --git a/crates/egui/src/atomics/atomics.rs b/crates/egui/src/atomics/atoms.rs similarity index 50% rename from crates/egui/src/atomics/atomics.rs rename to crates/egui/src/atomics/atoms.rs index ab5c2179aa2..9085c3c3e63 100644 --- a/crates/egui/src/atomics/atomics.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -1,38 +1,38 @@ -use crate::{Atomic, AtomicKind, Image, WidgetText}; +use crate::{Atom, AtomKind, Image, WidgetText}; use smallvec::SmallVec; use std::borrow::Cow; use std::ops::{Deref, DerefMut}; -// Rarely there should be more than 2 atomics in one Widget. +// Rarely there should be more than 2 atoms in one Widget. // I guess it could happen in a menu button with Image and right text... -pub(crate) const ATOMICS_SMALL_VEC_SIZE: usize = 2; +pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2; -/// A list of [`Atomic`]s. +/// A list of [`Atom`]s. #[derive(Clone, Debug, Default)] -pub struct Atomics<'a>(SmallVec<[Atomic<'a>; ATOMICS_SMALL_VEC_SIZE]>); +pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>); -impl<'a> Atomics<'a> { - pub fn new(content: impl IntoAtomics<'a>) -> Self { - content.into_atomics() +impl<'a> Atoms<'a> { + pub fn new(content: impl IntoAtoms<'a>) -> Self { + content.into_atoms() } - /// Insert a new [`Atomic`] at the end of the list (right side). - pub fn push_right(&mut self, atomic: impl Into>) { - self.0.push(atomic.into()); + /// Insert a new [`Atom`] at the end of the list (right side). + pub fn push_right(&mut self, atom: impl Into>) { + self.0.push(atom.into()); } - /// Insert a new [`Atomic`] at the beginning of the list (left side). - pub fn push_left(&mut self, atomic: impl Into>) { - self.0.insert(0, atomic.into()); + /// Insert a new [`Atom`] at the beginning of the list (left side). + pub fn push_left(&mut self, atom: impl Into>) { + self.0.insert(0, atom.into()); } /// Concatenate and return the text contents. // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. // in a submenu button there is a right text '⏵' which is now passed to the screen reader. - pub fn text(&self) -> Option> { - let mut string: Option> = None; - for atomic in &self.0 { - if let AtomicKind::Text(text) = &atomic.kind { + pub fn text(&self) -> Option> { + let mut string: Option> = None; + for atom in &self.0 { + if let AtomKind::Text(text) = &atom.kind { if let Some(string) = &mut string { let string = string.to_mut(); string.push(' '); @@ -45,17 +45,17 @@ impl<'a> Atomics<'a> { string } - pub fn iter_kinds(&'a self) -> impl Iterator> { - self.0.iter().map(|atomic| &atomic.kind) + pub fn iter_kinds(&'a self) -> impl Iterator> { + self.0.iter().map(|atom| &atom.kind) } - pub fn iter_kinds_mut(&'a mut self) -> impl Iterator> { - self.0.iter_mut().map(|atomic| &mut atomic.kind) + pub fn iter_kinds_mut(&'a mut self) -> impl Iterator> { + self.0.iter_mut().map(|atom| &mut atom.kind) } pub fn iter_images(&'a self) -> impl Iterator> { self.iter_kinds().filter_map(|kind| { - if let AtomicKind::Image(image) = kind { + if let AtomKind::Image(image) = kind { Some(image) } else { None @@ -65,7 +65,7 @@ impl<'a> Atomics<'a> { pub fn iter_images_mut(&'a mut self) -> impl Iterator> { self.iter_kinds_mut().filter_map(|kind| { - if let AtomicKind::Image(image) = kind { + if let AtomKind::Image(image) = kind { Some(image) } else { None @@ -75,7 +75,7 @@ impl<'a> Atomics<'a> { pub fn iter_texts(&'a self) -> impl Iterator { self.iter_kinds().filter_map(|kind| { - if let AtomicKind::Text(text) = kind { + if let AtomKind::Text(text) = kind { Some(text) } else { None @@ -85,7 +85,7 @@ impl<'a> Atomics<'a> { pub fn iter_texts_mut(&'a mut self) -> impl Iterator { self.iter_kinds_mut().filter_map(|kind| { - if let AtomicKind::Text(text) = kind { + if let AtomKind::Text(text) = kind { Some(text) } else { None @@ -93,14 +93,14 @@ impl<'a> Atomics<'a> { }) } - pub fn map_atomics(&mut self, mut f: impl FnMut(Atomic<'a>) -> Atomic<'a>) { + pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) { self.iter_mut() - .for_each(|atomic| *atomic = f(std::mem::take(atomic))); + .for_each(|atom| *atom = f(std::mem::take(atom))); } pub fn map_kind(&'a mut self, mut f: F) where - F: FnMut(AtomicKind<'a>) -> AtomicKind<'a>, + F: FnMut(AtomKind<'a>) -> AtomKind<'a>, { for kind in self.iter_kinds_mut() { *kind = f(std::mem::take(kind)); @@ -112,8 +112,8 @@ impl<'a> Atomics<'a> { F: FnMut(Image<'a>) -> Image<'a>, { self.map_kind(|kind| { - if let AtomicKind::Image(image) = kind { - AtomicKind::Image(f(image)) + if let AtomKind::Image(image) = kind { + AtomKind::Image(f(image)) } else { kind } @@ -125,8 +125,8 @@ impl<'a> Atomics<'a> { F: FnMut(WidgetText) -> WidgetText, { self.map_kind(|kind| { - if let AtomicKind::Text(text) = kind { - AtomicKind::Text(f(text)) + if let AtomKind::Text(text) = kind { + AtomKind::Text(f(text)) } else { kind } @@ -134,86 +134,86 @@ impl<'a> Atomics<'a> { } } -impl<'a> IntoIterator for Atomics<'a> { - type Item = Atomic<'a>; - type IntoIter = smallvec::IntoIter<[Atomic<'a>; ATOMICS_SMALL_VEC_SIZE]>; +impl<'a> IntoIterator for Atoms<'a> { + type Item = Atom<'a>; + type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } -/// Helper trait to convert a tuple of atomics into [`Atomics`]. +/// Helper trait to convert a tuple of atoms into [`Atoms`]. /// /// ``` -/// use egui::{Atomics, Image, IntoAtomics, RichText}; -/// let atomics: Atomics = ( +/// use egui::{Atoms, Image, IntoAtoms, RichText}; +/// let atoms: Atoms = ( /// "Some text", /// RichText::new("Some RichText"), /// Image::new("some_image_url"), -/// ).into_atomics(); +/// ).into_atoms(); /// ``` -impl<'a, T> IntoAtomics<'a> for T +impl<'a, T> IntoAtoms<'a> for T where - T: Into>, + T: Into>, { - fn collect(self, atomics: &mut Atomics<'a>) { - atomics.push_right(self); + fn collect(self, atoms: &mut Atoms<'a>) { + atoms.push_right(self); } } -/// Trait for turning a tuple of [`Atomic`]s into [`Atomics`]. -pub trait IntoAtomics<'a> { - fn collect(self, atomics: &mut Atomics<'a>); +/// Trait for turning a tuple of [`Atom`]s into [`Atoms`]. +pub trait IntoAtoms<'a> { + fn collect(self, atoms: &mut Atoms<'a>); - fn into_atomics(self) -> Atomics<'a> + fn into_atoms(self) -> Atoms<'a> where Self: Sized, { - let mut atomics = Atomics::default(); - self.collect(&mut atomics); - atomics + let mut atoms = Atoms::default(); + self.collect(&mut atoms); + atoms } } -impl<'a> IntoAtomics<'a> for Atomics<'a> { - fn collect(self, atomics: &mut Self) { - atomics.0.extend(self.0); +impl<'a> IntoAtoms<'a> for Atoms<'a> { + fn collect(self, atoms: &mut Self) { + atoms.0.extend(self.0); } } -macro_rules! all_the_atomics { +macro_rules! all_the_atoms { ($($T:ident),*) => { - impl<'a, $($T),*> IntoAtomics<'a> for ($($T),*) + impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*) where - $($T: IntoAtomics<'a>),* + $($T: IntoAtoms<'a>),* { - fn collect(self, _atomics: &mut Atomics<'a>) { + fn collect(self, _atoms: &mut Atoms<'a>) { #[allow(clippy::allow_attributes)] #[allow(non_snake_case)] let ($($T),*) = self; - $($T.collect(_atomics);)* + $($T.collect(_atoms);)* } } }; } -all_the_atomics!(); -all_the_atomics!(T0, T1); -all_the_atomics!(T0, T1, T2); -all_the_atomics!(T0, T1, T2, T3); -all_the_atomics!(T0, T1, T2, T3, T4); -all_the_atomics!(T0, T1, T2, T3, T4, T5); +all_the_atoms!(); +all_the_atoms!(T0, T1); +all_the_atoms!(T0, T1, T2); +all_the_atoms!(T0, T1, T2, T3); +all_the_atoms!(T0, T1, T2, T3, T4); +all_the_atoms!(T0, T1, T2, T3, T4, T5); -impl<'a> Deref for Atomics<'a> { - type Target = [Atomic<'a>]; +impl<'a> Deref for Atoms<'a> { + type Target = [Atom<'a>]; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for Atomics<'_> { +impl DerefMut for Atoms<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } diff --git a/crates/egui/src/atomics/mod.rs b/crates/egui/src/atomics/mod.rs index f44223a60d8..a431e7040f4 100644 --- a/crates/egui/src/atomics/mod.rs +++ b/crates/egui/src/atomics/mod.rs @@ -1,16 +1,16 @@ -mod atomic; -mod atomic_ext; -mod atomic_kind; -mod atomic_layout; +mod atom; +mod atom_ext; +mod atom_kind; +mod atom_layout; #[expect(clippy::module_inception)] -mod atomics; -mod sized_atomic; -mod sized_atomic_kind; +mod atoms; +mod sized_atom; +mod sized_atom_kind; -pub use atomic::*; -pub use atomic_ext::*; -pub use atomic_kind::*; -pub use atomic_layout::*; -pub use atomics::*; -pub use sized_atomic::*; -pub use sized_atomic_kind::*; +pub use atom::*; +pub use atom_ext::*; +pub use atom_kind::*; +pub use atom_layout::*; +pub use atoms::*; +pub use sized_atom::*; +pub use sized_atom_kind::*; diff --git a/crates/egui/src/atomics/sized_atom.rs b/crates/egui/src/atomics/sized_atom.rs new file mode 100644 index 00000000000..50fa443a98c --- /dev/null +++ b/crates/egui/src/atomics/sized_atom.rs @@ -0,0 +1,26 @@ +use crate::SizedAtomKind; +use emath::Vec2; + +/// A [`crate::Atom`] which has been sized. +#[derive(Clone, Debug)] +pub struct SizedAtom<'a> { + pub(crate) grow: bool, + + /// The size of the atom. + /// + /// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by + /// size.x + gap. + pub size: Vec2, + + /// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`. + pub preferred_size: Vec2, + + pub kind: SizedAtomKind<'a>, +} + +impl SizedAtom<'_> { + /// Was this [`crate::Atom`] marked as `grow`? + pub fn is_grow(&self) -> bool { + self.grow + } +} diff --git a/crates/egui/src/atomics/sized_atomic_kind.rs b/crates/egui/src/atomics/sized_atom_kind.rs similarity index 52% rename from crates/egui/src/atomics/sized_atomic_kind.rs rename to crates/egui/src/atomics/sized_atom_kind.rs index 644be65fa5c..ff8da163197 100644 --- a/crates/egui/src/atomics/sized_atomic_kind.rs +++ b/crates/egui/src/atomics/sized_atom_kind.rs @@ -3,9 +3,9 @@ use emath::Vec2; use epaint::Galley; use std::sync::Arc; -/// A sized [`crate::AtomicKind`]. +/// A sized [`crate::AtomKind`]. #[derive(Clone, Default, Debug)] -pub enum SizedAtomicKind<'a> { +pub enum SizedAtomKind<'a> { #[default] Empty, Text(Arc), @@ -13,13 +13,13 @@ pub enum SizedAtomicKind<'a> { Custom(Id), } -impl SizedAtomicKind<'_> { +impl SizedAtomKind<'_> { /// Get the calculated size. pub fn size(&self) -> Vec2 { match self { - SizedAtomicKind::Text(galley) => galley.size(), - SizedAtomicKind::Image(_, size) => *size, - SizedAtomicKind::Empty | SizedAtomicKind::Custom(_) => Vec2::ZERO, + SizedAtomKind::Text(galley) => galley.size(), + SizedAtomKind::Image(_, size) => *size, + SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO, } } } diff --git a/crates/egui/src/atomics/sized_atomic.rs b/crates/egui/src/atomics/sized_atomic.rs deleted file mode 100644 index e6f7241d5ae..00000000000 --- a/crates/egui/src/atomics/sized_atomic.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::SizedAtomicKind; -use emath::Vec2; - -/// A [`crate::Atomic`] which has been sized. -#[derive(Clone, Debug)] -pub struct SizedAtomic<'a> { - pub(crate) grow: bool, - - /// The size of the atomic. - /// - /// Used for placing this atomic in [`crate::AtomicLayout`], the cursor will advance by - /// size.x + gap. - pub size: Vec2, - - /// Preferred size of the atomic. This is used to calculate `Response::intrinsic_size`. - pub preferred_size: Vec2, - - pub kind: SizedAtomicKind<'a>, -} - -impl SizedAtomic<'_> { - /// Was this [`crate::Atomic`] marked as `grow`? - pub fn is_grow(&self) -> bool { - self.grow - } -} diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 0de6f861480..a4b3e3cdde6 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -1,6 +1,6 @@ use crate::style::StyleModifier; use crate::{ - Button, Color32, Context, Frame, Id, InnerResponse, IntoAtomics, Layout, Popup, + Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup, PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _, }; use emath::{vec2, Align, RectAlign, Vec2}; @@ -243,8 +243,8 @@ pub struct MenuButton<'a> { } impl<'a> MenuButton<'a> { - pub fn new(text: impl IntoAtomics<'a>) -> Self { - Self::from_button(Button::new(text.into_atomics())) + pub fn new(text: impl IntoAtoms<'a>) -> Self { + Self::from_button(Button::new(text.into_atoms())) } /// Set the config for the menu. @@ -293,8 +293,8 @@ impl<'a> SubMenuButton<'a> { /// The default right arrow symbol: `"⏵"` pub const RIGHT_ARROW: &'static str = "⏵"; - pub fn new(text: impl IntoAtomics<'a>) -> Self { - Self::from_button(Button::new(text.into_atomics()).right_text("⏵")) + pub fn new(text: impl IntoAtoms<'a>) -> Self { + Self::from_button(Button::new(text.into_atoms()).right_text("⏵")) } /// Create a new submenu button from a [`Button`]. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 7df68321943..6f941c3cce3 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -25,7 +25,7 @@ use crate::{ color_picker, Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link, RadioButton, SelectableLabel, Separator, Spinner, TextEdit, Widget, }, - Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtomics, + Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtoms, LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, WidgetRect, WidgetText, @@ -2055,7 +2055,7 @@ impl Ui { /// ``` #[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "] #[inline] - pub fn button<'a>(&mut self, text: impl IntoAtomics<'a>) -> Response { + pub fn button<'a>(&mut self, text: impl IntoAtoms<'a>) -> Response { Button::new(text).ui(self) } @@ -2073,7 +2073,7 @@ impl Ui { /// /// See also [`Self::toggle_value`]. #[inline] - pub fn checkbox<'a>(&mut self, checked: &'a mut bool, text: impl IntoAtomics<'a>) -> Response { + pub fn checkbox<'a>(&mut self, checked: &'a mut bool, text: impl IntoAtoms<'a>) -> Response { Checkbox::new(checked, text).ui(self) } @@ -2095,7 +2095,7 @@ impl Ui { /// Often you want to use [`Self::radio_value`] instead. #[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "] #[inline] - pub fn radio<'a>(&mut self, selected: bool, text: impl IntoAtomics<'a>) -> Response { + pub fn radio<'a>(&mut self, selected: bool, text: impl IntoAtoms<'a>) -> Response { RadioButton::new(selected, text).ui(self) } @@ -2122,7 +2122,7 @@ impl Ui { &mut self, current_value: &mut Value, alternative: Value, - text: impl IntoAtomics<'a>, + text: impl IntoAtoms<'a>, ) -> Response { let mut response = self.radio(*current_value == alternative, text); if response.clicked() && *current_value != alternative { @@ -3043,7 +3043,7 @@ impl Ui { /// See also: [`Self::close`] and [`Response::context_menu`]. pub fn menu_button<'a, R>( &mut self, - content: impl IntoAtomics<'a>, + content: impl IntoAtoms<'a>, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse> { let (response, inner) = if menu::is_in_menu(self) { diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 855942b90fa..df89b50fb47 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ - Atomic, AtomicExt as _, AtomicKind, AtomicLayout, AtomicLayoutResponse, Color32, CornerRadius, - Frame, Image, IntoAtomics, NumExt as _, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, - Widget, WidgetInfo, WidgetText, WidgetType, + Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame, + Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, + WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -24,7 +24,7 @@ use crate::{ /// ``` #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Button<'a> { - layout: AtomicLayout<'a>, + layout: AtomLayout<'a>, fill: Option, stroke: Option, small: bool, @@ -37,9 +37,9 @@ pub struct Button<'a> { } impl<'a> Button<'a> { - pub fn new(content: impl IntoAtomics<'a>) -> Self { + pub fn new(content: impl IntoAtoms<'a>) -> Self { Self { - layout: AtomicLayout::new(content.into_atomics()).sense(Sense::click()), + layout: AtomLayout::new(content.into_atoms()).sense(Sense::click()), fill: None, stroke: None, small: false, @@ -55,7 +55,7 @@ impl<'a> Button<'a> { /// Creates a button with an image. The size of the image as displayed is defined by the provided size. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`crate::AtomicExt::atom_max_height_font_size`]). + /// (using [`crate::AtomExt::atom_max_height_font_size`]). pub fn image(image: impl Into>) -> Self { Self::opt_image_and_text(Some(image.into()), None) } @@ -63,7 +63,7 @@ impl<'a> Button<'a> { /// Creates a button with an image to the left of the text. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`crate::AtomicExt::atom_max_height_font_size`]). + /// (using [`crate::AtomExt::atom_max_height_font_size`]). pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { Self::opt_image_and_text(Some(image.into()), Some(text.into())) } @@ -71,7 +71,7 @@ impl<'a> Button<'a> { /// Create a button with an optional image and optional text. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height - /// (using [`crate::AtomicExt::atom_max_height_font_size`]). + /// (using [`crate::AtomExt::atom_max_height_font_size`]). pub fn opt_image_and_text(image: Option>, text: Option) -> Self { let mut button = Self::new(()); if let Some(image) = image { @@ -186,21 +186,21 @@ impl<'a> Button<'a> { /// /// See also [`Self::right_text`]. #[inline] - pub fn shortcut_text(mut self, shortcut_text: impl Into>) -> Self { - let mut atomic = shortcut_text.into(); - atomic.kind = match atomic.kind { - AtomicKind::Text(text) => AtomicKind::Text(text.weak()), + pub fn shortcut_text(mut self, shortcut_text: impl Into>) -> Self { + let mut atom = shortcut_text.into(); + atom.kind = match atom.kind { + AtomKind::Text(text) => AtomKind::Text(text.weak()), other => other, }; - self.layout.push_right(Atomic::grow()); - self.layout.push_right(atomic); + self.layout.push_right(Atom::grow()); + self.layout.push_right(atom); self } /// Show some text on the right side of the button. #[inline] - pub fn right_text(mut self, right_text: impl Into>) -> Self { - self.layout.push_right(Atomic::grow()); + pub fn right_text(mut self, right_text: impl Into>) -> Self { + self.layout.push_right(Atom::grow()); self.layout.push_right(right_text.into()); self } @@ -212,8 +212,8 @@ impl<'a> Button<'a> { self } - /// Show the button and return a [`AtomicLayoutResponse`] for painting custom contents. - pub fn atomic_ui(self, ui: &mut Ui) -> AtomicLayoutResponse { + /// Show the button and return a [`AtomLayoutResponse`] for painting custom contents. + pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse { let Button { mut layout, fill, @@ -232,16 +232,16 @@ impl<'a> Button<'a> { } if limit_image_size { - layout.map_atomics(|atomic| { - if matches!(&atomic.kind, AtomicKind::Image(_)) { - atomic.atom_max_height_font_size(ui) + layout.map_atoms(|atom| { + if matches!(&atom.kind, AtomKind::Image(_)) { + atom.atom_max_height_font_size(ui) } else { - atomic + atom } }); } - let text = layout.text(); + let text = layout.text().map(String::from); let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); @@ -284,7 +284,7 @@ impl<'a> Button<'a> { prepared.paint(ui) } else { - AtomicLayoutResponse::empty(prepared.response) + AtomLayoutResponse::empty(prepared.response) }; response.response.widget_info(|| { @@ -301,6 +301,6 @@ impl<'a> Button<'a> { impl Widget for Button<'_> { fn ui(self, ui: &mut Ui) -> Response { - self.atomic_ui(ui).response + self.atom_ui(ui).response } } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 0ed1063da01..cfd0c3795cf 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,6 +1,6 @@ use crate::{ - epaint, pos2, Atomic, AtomicLayout, Atomics, Id, IntoAtomics, NumExt as _, Response, Sense, - Shape, Ui, Vec2, Widget, WidgetInfo, WidgetType, + epaint, pos2, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, + Vec2, Widget, WidgetInfo, WidgetType, }; // TODO(emilk): allow checkbox without a text label @@ -19,15 +19,15 @@ use crate::{ #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Checkbox<'a> { checked: &'a mut bool, - atomics: Atomics<'a>, + atoms: Atoms<'a>, indeterminate: bool, } impl<'a> Checkbox<'a> { - pub fn new(checked: &'a mut bool, atomics: impl IntoAtomics<'a>) -> Self { + pub fn new(checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Self { Checkbox { checked, - atomics: atomics.into_atomics(), + atoms: atoms.into_atoms(), indeterminate: false, } } @@ -51,7 +51,7 @@ impl Widget for Checkbox<'_> { fn ui(self, ui: &mut Ui) -> Response { let Checkbox { checked, - mut atomics, + mut atoms, indeterminate, } = self; @@ -65,11 +65,11 @@ impl Widget for Checkbox<'_> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::checkbox"); - atomics.push_left(Atomic::custom(rect_id, icon_size)); + atoms.push_left(Atom::custom(rect_id, icon_size)); - let text = atomics.text(); + let text = atoms.text().map(String::from); - let mut prepared = AtomicLayout::new(atomics) + let mut prepared = AtomLayout::new(atoms) .sense(Sense::click()) .min_size(min_size) .allocate(ui); diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index cecfb87dd74..3151d624496 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -1,6 +1,6 @@ use crate::{ - epaint, Atomic, AtomicLayout, Atomics, Id, IntoAtomics, NumExt as _, Response, Sense, Ui, Vec2, - Widget, WidgetInfo, WidgetType, + epaint, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget, + WidgetInfo, WidgetType, }; /// One out of several alternatives, either selected or not. @@ -25,24 +25,21 @@ use crate::{ #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct RadioButton<'a> { checked: bool, - atomics: Atomics<'a>, + atoms: Atoms<'a>, } impl<'a> RadioButton<'a> { - pub fn new(checked: bool, text: impl IntoAtomics<'a>) -> Self { + pub fn new(checked: bool, text: impl IntoAtoms<'a>) -> Self { Self { checked, - atomics: text.into_atomics(), + atoms: text.into_atoms(), } } } impl Widget for RadioButton<'_> { fn ui(self, ui: &mut Ui) -> Response { - let Self { - checked, - mut atomics, - } = self; + let Self { checked, mut atoms } = self; let spacing = &ui.spacing(); let icon_width = spacing.icon_width; @@ -54,11 +51,11 @@ impl Widget for RadioButton<'_> { let mut icon_size = Vec2::splat(icon_width); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::radio_button"); - atomics.push_left(Atomic::custom(rect_id, icon_size)); + atoms.push_left(Atom::custom(rect_id, icon_size)); - let text = atomics.text(); + let text = atoms.text().map(String::from); - let mut prepared = AtomicLayout::new(atomics) + let mut prepared = AtomLayout::new(atoms) .sense(Sense::click()) .min_size(min_size) .allocate(ui); diff --git a/tests/egui_tests/tests/test_atomics.rs b/tests/egui_tests/tests/test_atoms.rs similarity index 96% rename from tests/egui_tests/tests/test_atomics.rs rename to tests/egui_tests/tests/test_atoms.rs index 85284ce0431..abc9f2d0546 100644 --- a/tests/egui_tests/tests/test_atomics.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -1,8 +1,8 @@ -use egui::{Align, AtomicExt as _, Button, Layout, TextWrapMode, Ui, Vec2}; +use egui::{Align, AtomExt as _, Button, Layout, TextWrapMode, Ui, Vec2}; use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults}; #[test] -fn test_atomics() { +fn test_atoms() { let mut results = SnapshotResults::new(); results.add(single_test("max_width", |ui| { diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 13fc212ed1c..110eff81003 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,9 +1,8 @@ use egui::load::SizedTexture; use egui::{ - include_image, Align, AtomicExt as _, AtomicLayout, Button, Color32, ColorImage, Direction, - DragValue, Event, Grid, IntoAtomics as _, Layout, PointerButton, Pos2, Response, Slider, - Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, - Widget as _, + include_image, Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, + DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, Pos2, Response, Slider, Stroke, + StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, }; use egui_kittest::kittest::{by, Node, Queryable as _}; use egui_kittest::{Harness, SnapshotResult, SnapshotResults}; @@ -95,23 +94,22 @@ fn widget_tests() { ); let source = include_image!("../../../crates/eframe/data/icon.png"); - let interesting_atomics = vec![ - ("minimal", ("Hello World!").into_atomics()), + let interesting_atoms = vec![ + ("minimal", ("Hello World!").into_atoms()), ( "image", - (source.clone().atom_max_height(12.0), "With Image").into_atomics(), + (source.clone().atom_max_height(12.0), "With Image").into_atoms(), ), ( "multi_grow", - ("g".atom_grow(true), "2", "g".atom_grow(true), "4").into_atomics(), + ("g".atom_grow(true), "2", "g".atom_grow(true), "4").into_atoms(), ), ]; - for atomics in interesting_atomics { - results.add(test_widget_layout( - &format!("atomics_{}", atomics.0), - |ui| AtomicLayout::new(atomics.1.clone()).ui(ui), - )); + for atoms in interesting_atoms { + results.add(test_widget_layout(&format!("atoms_{}", atoms.0), |ui| { + AtomLayout::new(atoms.1.clone()).ui(ui) + })); } } From 17f61c317f6b48e91d7fddd21120d9d1bec15fbc Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 10:56:08 +0200 Subject: [PATCH 69/77] Rename IntoAtoms params to atoms --- crates/egui/src/atomics/atoms.rs | 4 ++-- crates/egui/src/containers/menu.rs | 8 ++++---- crates/egui/src/ui.rs | 22 +++++++++++----------- crates/egui/src/widgets/button.rs | 4 ++-- crates/egui/src/widgets/radio_button.rs | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 9085c3c3e63..3752ace7047 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -12,8 +12,8 @@ pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2; pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>); impl<'a> Atoms<'a> { - pub fn new(content: impl IntoAtoms<'a>) -> Self { - content.into_atoms() + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { + atoms.into_atoms() } /// Insert a new [`Atom`] at the end of the list (right side). diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index a4b3e3cdde6..4fe06477d87 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -243,8 +243,8 @@ pub struct MenuButton<'a> { } impl<'a> MenuButton<'a> { - pub fn new(text: impl IntoAtoms<'a>) -> Self { - Self::from_button(Button::new(text.into_atoms())) + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { + Self::from_button(Button::new(atoms.into_atoms())) } /// Set the config for the menu. @@ -293,8 +293,8 @@ impl<'a> SubMenuButton<'a> { /// The default right arrow symbol: `"⏵"` pub const RIGHT_ARROW: &'static str = "⏵"; - pub fn new(text: impl IntoAtoms<'a>) -> Self { - Self::from_button(Button::new(text.into_atoms()).right_text("⏵")) + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { + Self::from_button(Button::new(atoms.into_atoms()).right_text("⏵")) } /// Create a new submenu button from a [`Button`]. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 6f941c3cce3..bc2ed860df2 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2055,8 +2055,8 @@ impl Ui { /// ``` #[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "] #[inline] - pub fn button<'a>(&mut self, text: impl IntoAtoms<'a>) -> Response { - Button::new(text).ui(self) + pub fn button<'a>(&mut self, atoms: impl IntoAtoms<'a>) -> Response { + Button::new(atoms).ui(self) } /// A button as small as normal body text. @@ -2073,8 +2073,8 @@ impl Ui { /// /// See also [`Self::toggle_value`]. #[inline] - pub fn checkbox<'a>(&mut self, checked: &'a mut bool, text: impl IntoAtoms<'a>) -> Response { - Checkbox::new(checked, text).ui(self) + pub fn checkbox<'a>(&mut self, checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Response { + Checkbox::new(checked, atoms).ui(self) } /// Acts like a checkbox, but looks like a [`SelectableLabel`]. @@ -2095,8 +2095,8 @@ impl Ui { /// Often you want to use [`Self::radio_value`] instead. #[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "] #[inline] - pub fn radio<'a>(&mut self, selected: bool, text: impl IntoAtoms<'a>) -> Response { - RadioButton::new(selected, text).ui(self) + pub fn radio<'a>(&mut self, selected: bool, atoms: impl IntoAtoms<'a>) -> Response { + RadioButton::new(selected, atoms).ui(self) } /// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`. @@ -2122,9 +2122,9 @@ impl Ui { &mut self, current_value: &mut Value, alternative: Value, - text: impl IntoAtoms<'a>, + atoms: impl IntoAtoms<'a>, ) -> Response { - let mut response = self.radio(*current_value == alternative, text); + let mut response = self.radio(*current_value == alternative, atoms); if response.clicked() && *current_value != alternative { *current_value = alternative; response.mark_changed(); @@ -3043,13 +3043,13 @@ impl Ui { /// See also: [`Self::close`] and [`Response::context_menu`]. pub fn menu_button<'a, R>( &mut self, - content: impl IntoAtoms<'a>, + atoms: impl IntoAtoms<'a>, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse> { let (response, inner) = if menu::is_in_menu(self) { - menu::SubMenuButton::new(content).ui(self, add_contents) + menu::SubMenuButton::new(atoms).ui(self, add_contents) } else { - menu::MenuButton::new(content).ui(self, add_contents) + menu::MenuButton::new(atoms).ui(self, add_contents) }; InnerResponse::new(inner.map(|i| i.inner), response) } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index df89b50fb47..aa75eabd844 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -37,9 +37,9 @@ pub struct Button<'a> { } impl<'a> Button<'a> { - pub fn new(content: impl IntoAtoms<'a>) -> Self { + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { Self { - layout: AtomLayout::new(content.into_atoms()).sense(Sense::click()), + layout: AtomLayout::new(atoms.into_atoms()).sense(Sense::click()), fill: None, stroke: None, small: false, diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index 3151d624496..f63273206de 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -29,10 +29,10 @@ pub struct RadioButton<'a> { } impl<'a> RadioButton<'a> { - pub fn new(checked: bool, text: impl IntoAtoms<'a>) -> Self { + pub fn new(checked: bool, atoms: impl IntoAtoms<'a>) -> Self { Self { checked, - atoms: text.into_atoms(), + atoms: atoms.into_atoms(), } } } From fae0e9108bce5c3505bc219446d68db95f9df2b6 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 11:01:44 +0200 Subject: [PATCH 70/77] Doc --- crates/egui/src/atomics/atom_layout.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index c425ec36f76..64137be8ea8 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -95,6 +95,9 @@ impl<'a> AtomLayout<'a> { } /// Set the minimum size of the Widget. + /// + /// This will find and expand atoms with `grow: true`. + /// If there are no growable atoms then everything will be left-aligned. #[inline] pub fn min_size(mut self, size: Vec2) -> Self { self.min_size = size; From dcc6bec0222a27719328858cb07111b8b3c1a0c5 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 11:12:54 +0200 Subject: [PATCH 71/77] Rename get_rect to rect --- crates/egui/src/atomics/atom_kind.rs | 2 +- crates/egui/src/atomics/atom_layout.rs | 9 +++++---- crates/egui/src/widgets/checkbox.rs | 2 +- crates/egui/src/widgets/radio_button.rs | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 858c89dfe0f..2672e646b41 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -51,7 +51,7 @@ pub enum AtomKind<'a> { /// let id = Id::new("my_button"); /// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui); /// - /// let rect = response.get_rect(id); + /// let rect = response.rect(id); /// if let Some(rect) = rect { /// ui.put(rect, Button::new("⏵")); /// } diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 64137be8ea8..2b2ab6f1116 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -426,9 +426,7 @@ impl<'atom> AllocatedAtomLayout<'atom> { /// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`]. /// -/// Use the `custom_rects` together with [`AtomKind::Custom`] to add child widgets to a widget. -/// -/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible. +/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`]. #[derive(Clone, Debug)] pub struct AtomLayoutResponse { pub response: Response, @@ -448,7 +446,10 @@ impl AtomLayoutResponse { self.custom_rects.iter().copied() } - pub fn get_rect(&self, id: Id) -> Option { + /// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets. + /// + /// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible. + pub fn rect(&self, id: Id) -> Option { self.custom_rects .iter() .find_map(|(i, r)| if *i == id { Some(*r) } else { None }) diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index cfd0c3795cf..f7498de5af6 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -101,7 +101,7 @@ impl Widget for Checkbox<'_> { prepared.fallback_text_color = visuals.text_color(); let response = prepared.paint(ui); - if let Some(rect) = response.get_rect(rect_id) { + if let Some(rect) = response.rect(rect_id) { let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); ui.painter().add(epaint::RectShape::new( big_icon_rect.expand(visuals.expansion), diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index f63273206de..53dda399f4b 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -76,7 +76,7 @@ impl Widget for RadioButton<'_> { prepared.fallback_text_color = visuals.text_color(); let response = prepared.paint(ui); - if let Some(rect) = response.get_rect(rect_id) { + if let Some(rect) = response.rect(rect_id) { let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); let painter = ui.painter(); From 21ec937c6fde1327ae65b670b3be8d9e2d863e62 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 11:13:48 +0200 Subject: [PATCH 72/77] Remove clippy expect --- crates/egui/src/atomics/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/egui/src/atomics/mod.rs b/crates/egui/src/atomics/mod.rs index a431e7040f4..7c8922c97ed 100644 --- a/crates/egui/src/atomics/mod.rs +++ b/crates/egui/src/atomics/mod.rs @@ -2,7 +2,6 @@ mod atom; mod atom_ext; mod atom_kind; mod atom_layout; -#[expect(clippy::module_inception)] mod atoms; mod sized_atom; mod sized_atom_kind; From 5c780e7dbd7d8e78e4d3a805942b655949337f4a Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 11:15:38 +0200 Subject: [PATCH 73/77] Space --- crates/egui/src/atomics/atom.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index e3045393bcc..f8f7b666842 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -16,12 +16,16 @@ use epaint::text::TextWrapMode; pub struct Atom<'a> { /// See [`crate::AtomExt::atom_size`] pub size: Option, + /// See [`crate::AtomExt::atom_max_size`] pub max_size: Vec2, + /// See [`crate::AtomExt::atom_grow`] pub grow: bool, + /// See [`crate::AtomExt::atom_shrink`] pub shrink: bool, + /// The atom type pub kind: AtomKind<'a>, } From 6919bf38865998c3ba9025a5e6598fa1b194c056 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 11:19:08 +0200 Subject: [PATCH 74/77] Docs --- crates/egui/src/atomics/atom_ext.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/atomics/atom_ext.rs b/crates/egui/src/atomics/atom_ext.rs index f05d0d56d4e..0c34544d8fe 100644 --- a/crates/egui/src/atomics/atom_ext.rs +++ b/crates/egui/src/atomics/atom_ext.rs @@ -19,7 +19,7 @@ pub trait AtomExt<'a> { /// Grow this atom to the available space. /// /// This will affect the size of the [`Atom`] in the main direction. Since - /// [`AtomLayout`] today only supports horizontal layout, it will affect the width. + /// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width. /// /// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the /// remaining space. @@ -28,7 +28,7 @@ pub trait AtomExt<'a> { /// Shrink this atom if there isn't enough space. /// /// This will affect the size of the [`Atom`] in the main direction. Since - /// [`AtomLayout`] today only supports horizontal layout, it will affect the width. + /// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width. /// /// NOTE: Only a single [`Atom`] may shrink for each widget. /// From 356026cb682cf5292c9eb4c45854268419cebf1c Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 11:35:31 +0200 Subject: [PATCH 75/77] Update snapshots --- crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png | 2 +- crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Scene.png | 4 ++-- .../snapshots/layout/{atomics_image.png => atoms_image.png} | 0 .../layout/{atomics_minimal.png => atoms_minimal.png} | 0 .../layout/{atomics_multi_grow.png => atoms_multi_grow.png} | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename tests/egui_tests/tests/snapshots/layout/{atomics_image.png => atoms_image.png} (100%) rename tests/egui_tests/tests/snapshots/layout/{atomics_minimal.png => atoms_minimal.png} (100%) rename tests/egui_tests/tests/snapshots/layout/{atomics_multi_grow.png => atoms_multi_grow.png} (100%) diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png index 526dc7ace0d..394bea644b9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c69c211061663cd17756eb0ad5a7720ed883047dbcedb39c493c544cfc644ed3 +oid sha256:0c975c8b646425878b704f32198286010730746caf5d463ca8cbcfe539922816 size 99087 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png index e2899160b5d..7800f5f5f05 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18fe761145335a60b1eeb1f7f2072224df86f0e2006caa09d1f3cc4bd263d90c -size 46560 +oid sha256:c47a19d1f56fcc4c30c7e88aada2a50e038d66c1b591b4646b86c11bffb3c66f +size 46563 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 212a7ccd4d0..ea8f9c85782 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fcfee082fe1dcbb7515ca6e3d5457e71fecf91a3efc4f76906a32fdb588adb4 -size 35096 +oid sha256:cdff6256488f3a40c65a3d73c0635377bf661c57927bce4c853b2a5f3b33274e +size 35121 diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_image.png b/tests/egui_tests/tests/snapshots/layout/atoms_image.png similarity index 100% rename from tests/egui_tests/tests/snapshots/layout/atomics_image.png rename to tests/egui_tests/tests/snapshots/layout/atoms_image.png diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_minimal.png b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png similarity index 100% rename from tests/egui_tests/tests/snapshots/layout/atomics_minimal.png rename to tests/egui_tests/tests/snapshots/layout/atoms_minimal.png diff --git a/tests/egui_tests/tests/snapshots/layout/atomics_multi_grow.png b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png similarity index 100% rename from tests/egui_tests/tests/snapshots/layout/atomics_multi_grow.png rename to tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png From 168fd86b0ef25bce71c65892de9f1f273c41733a Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Jun 2025 12:00:05 +0200 Subject: [PATCH 76/77] Add comment --- crates/egui/src/atomics/atom_layout.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 2b2ab6f1116..a25a4b7c6bd 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -392,6 +392,8 @@ impl<'atom> AllocatedAtomLayout<'atom> { for sized in sized_atoms { let size = sized.size; + // TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors + // https://github.com/emilk/egui/pull/5830#discussion_r2079627864 let growth = if sized.is_grow() { grow_width } else { 0.0 }; let frame = aligned_rect From 64960418178dfb4103ec7fb7756169bfbd47ac3f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 12 Jun 2025 13:18:33 +0200 Subject: [PATCH 77/77] Fix doc --- crates/egui/src/atomics/atom.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index f8f7b666842..4f4b5b75032 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -5,7 +5,7 @@ use epaint::text::TextWrapMode; /// A low-level ui building block. /// /// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience. -/// You can directly call the `a_*` methods on anything that implements `Into`. +/// You can directly call the `atom_*` methods on anything that implements `Into`. /// ``` /// # use egui::{Image, emath::Vec2}; /// use egui::AtomExt as _;