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

Combined example

This commit is contained in:
lucasmerlin
2026-06-09 14:17:30 +02:00
parent 6b37504b30
commit ef5abd1f81
8 changed files with 471 additions and 146 deletions

View File

@@ -454,8 +454,6 @@ impl<'a> AtomLayout<'a> {
let mut cross_size: f32 = 0.0; let mut cross_size: f32 = 0.0;
let mut sized_items = Vec::new();
let mut grow_count = 0; let mut grow_count = 0;
let mut shrink_item = None; let mut shrink_item = None;
@@ -470,6 +468,35 @@ impl<'a> AtomLayout<'a> {
intrinsic_main += gap_space; intrinsic_main += gap_space;
} }
// A `grow` nested `Layout` in a non-wrapping layout is measured *after* its siblings, at
// the main-axis space they leave over, instead of at the full available width. A fill
// layout (e.g. a wrapping gallery) expands to whatever width it is measured at, so sizing
// it at the full width inflates it past its own cell — and because the line is then already
// over-full, `grow` has no slack to pull it back, so it overflows its fixed siblings.
// Wrapping layouts wrap onto new lines instead of overflowing, so they keep the simpler
// full-width measurement.
let defer_grow =
|item: &crate::Atom<'a>| !wrap && item.grow && matches!(item.kind, AtomKind::Layout(_));
// Measured atoms are parked by index so `sized_items` stays in atom order even though we
// size the shrink item and deferred `grow` items out of order.
let mut slots: Vec<Option<SizedAtom<'a>>> =
std::iter::repeat_with(|| None).take(atoms.len()).collect();
let mut deferred_grow: Vec<usize> = Vec::new();
let measure_into = |slot: &mut Option<SizedAtom<'a>>,
sized: SizedAtom<'a>,
inner_main: &mut f32,
intrinsic_main: &mut f32,
cross_size: &mut f32,
intrinsic_cross: &mut f32| {
*inner_main += sized.size[main_axis];
*intrinsic_main += sized.intrinsic_size[main_axis];
*cross_size = cross_size.at_least(sized.size[cross_axis]);
*intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]);
*slot = Some(sized);
};
for (idx, item) in atoms.iter().enumerate() { for (idx, item) in atoms.iter().enumerate() {
if item.grow { if item.grow {
grow_count += 1; grow_count += 1;
@@ -487,6 +514,10 @@ impl<'a> AtomLayout<'a> {
continue; continue;
} }
} }
if defer_grow(item) {
deferred_grow.push(idx);
continue;
}
let sized = item.as_sized( let sized = item.as_sized(
ui, ui,
available_inner_size, available_inner_size,
@@ -494,15 +525,14 @@ impl<'a> AtomLayout<'a> {
fallback_font.clone(), fallback_font.clone(),
cache, cache,
); );
let size = sized.size; measure_into(
&mut slots[idx],
inner_main += size[main_axis]; sized,
intrinsic_main += sized.intrinsic_size[main_axis]; &mut inner_main,
&mut intrinsic_main,
cross_size = cross_size.at_least(size[cross_axis]); &mut cross_size,
intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]); &mut intrinsic_cross,
);
sized_items.push(sized);
} }
if let Some((index, item)) = shrink_item { if let Some((index, item)) = shrink_item {
@@ -521,7 +551,7 @@ impl<'a> AtomLayout<'a> {
ui, ui,
available_size_for_shrink_item, available_size_for_shrink_item,
Some(wrap_mode), Some(wrap_mode),
fallback_font, fallback_font.clone(),
cache, cache,
) )
} else { } else {
@@ -531,21 +561,51 @@ impl<'a> AtomLayout<'a> {
ui, ui,
available_size_for_shrink_item, available_size_for_shrink_item,
Some(wrap_mode), Some(wrap_mode),
fallback_font, fallback_font.clone(),
cache, cache,
) )
}; };
let size = sized.size; measure_into(
&mut slots[index],
inner_main += size[main_axis]; sized,
intrinsic_main += sized.intrinsic_size[main_axis]; &mut inner_main,
&mut intrinsic_main,
cross_size = cross_size.at_least(size[cross_axis]); &mut cross_size,
intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]); &mut intrinsic_cross,
);
sized_items.insert(index, sized);
} }
// Deferred `grow` layouts share whatever main-axis space the other atoms left over. Split
// it across *all* `grow` atoms (matching how `paint_at` distributes the slack), so a
// deferred layout never claims more than its fair share and the line can't overflow.
if !deferred_grow.is_empty() {
let leftover = (available_inner_size[main_axis] - inner_main).max(0.0);
let share = leftover / grow_count.max(1) as f32;
let grow_available = main_cross_vec(direction, share, available_inner_size[cross_axis]);
for idx in std::mem::take(&mut deferred_grow) {
let sized = atoms[idx].as_sized(
ui,
grow_available,
Some(wrap_mode),
fallback_font.clone(),
cache,
);
measure_into(
&mut slots[idx],
sized,
&mut inner_main,
&mut intrinsic_main,
&mut cross_size,
&mut intrinsic_cross,
);
}
}
let mut sized_items: Vec<SizedAtom<'a>> = slots
.into_iter()
.map(|slot| slot.expect("every atom is measured into its slot"))
.collect();
// Group the (flat) sized atoms into lines. Without wrapping that's a single line // Group the (flat) sized atoms into lines. Without wrapping that's a single line
// spanning everything, which reproduces the previous single-line behavior exactly. // spanning everything, which reproduces the previous single-line behavior exactly.
let mut lines = if wrap { let mut lines = if wrap {

View File

@@ -1,4 +1,7 @@
use crate::{Atom, AtomExt, AtomKind, AtomLayout, Atoms, Button, Color32, Context, Id, InnerResponse, IntoAtoms, Layout, Response, Sense, Spacing, Style, Ui, UiBuilder, Visuals, Widget, WidgetRect}; use crate::{
Atom, AtomExt, AtomKind, AtomLayout, Atoms, Button, Color32, Context, Id, InnerResponse,
IntoAtoms, Layout, Response, Sense, Spacing, Style, Ui, UiBuilder, Visuals, Widget, WidgetRect,
};
use emath::{Align, Pos2, Rect, Vec2}; use emath::{Align, Pos2, Rect, Vec2};
use epaint::Direction; use epaint::Direction;
@@ -138,7 +141,7 @@ impl<'ui, 'layout> AtomUi<'ui, 'layout> {
pub fn add(&mut self, mut config: Atom<'layout>, widget: impl AtomWidget<'layout>) -> Response { pub fn add(&mut self, mut config: Atom<'layout>, widget: impl AtomWidget<'layout>) -> Response {
let (layout, response) = widget.show_for(self.ctx); let (layout, response) = widget.show_for(self.ctx);
config.kind = AtomKind::Layout(Box::new(layout)); config.kind = AtomKind::Layout(std::rc::Rc::new(layout));
self.layout.push_right(config); self.layout.push_right(config);
response response
@@ -156,7 +159,7 @@ impl<'ui, 'layout> AtomUi<'ui, 'layout> {
inner, inner,
response: child.response(), response: child.response(),
}; };
atom.kind = AtomKind::Layout(Box::new(child.layout)); atom.kind = AtomKind::Layout(std::rc::Rc::new(child.layout));
self.layout.push_right(atom); self.layout.push_right(atom);
response response
} }

View File

@@ -2,16 +2,16 @@ mod atom;
mod atom_ext; mod atom_ext;
mod atom_kind; mod atom_kind;
mod atom_layout; mod atom_layout;
mod atom_widget;
mod atoms; mod atoms;
mod sized_atom; mod sized_atom;
mod sized_atom_kind; mod sized_atom_kind;
mod atom_widget;
pub use atom::*; pub use atom::*;
pub use atom_ext::*; pub use atom_ext::*;
pub use atom_kind::*; pub use atom_kind::*;
pub use atom_layout::*; pub use atom_layout::*;
pub use atom_widget::*;
pub use atoms::*; pub use atoms::*;
pub use sized_atom::*; pub use sized_atom::*;
pub use sized_atom_kind::*; pub use sized_atom_kind::*;
pub use atom_widget::*;

View File

@@ -1,6 +1,12 @@
use epaint::Margin; use epaint::Margin;
use crate::{Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame, Id, Image, IntoAtoms, NumExt as _, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetRect, WidgetText, WidgetType, widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState}, AtomWidget, impl_widget_for_atom_widget, AtomWidgetContext}; use crate::{
Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, AtomWidget, AtomWidgetContext,
Color32, CornerRadius, Frame, Id, Image, IntoAtoms, NumExt as _, Rect, Response, Sense, Stroke,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetRect, WidgetText, WidgetType,
impl_widget_for_atom_widget,
widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState},
};
/// Clickable button with text. /// Clickable button with text.
/// ///
@@ -357,9 +363,6 @@ impl<'a> AtomWidget<'a> for Button<'a> {
layout = layout.min_size(min_size); layout = layout.min_size(min_size);
if let Some(cursor) = ui.visuals().interact_cursor if let Some(cursor) = ui.visuals().interact_cursor
&& response.hovered() && response.hovered()
{ {

View File

@@ -1,6 +1,8 @@
use std::collections::BTreeSet;
use egui::{ use egui::{
Align2, Atom, AtomExt as _, AtomLayout, Atoms, Color32, CornerRadius, Direction, Frame, Margin, Align2, AtomExt as _, AtomLayout, AtomUi, Button, Color32, CornerRadius, Direction, Frame,
RichText, Stroke, TextWrapMode, Vec2, Margin, RichText, Stroke, TextWrapMode, Vec2, atom,
}; };
// A small palette used to colour-code the cards. // A small palette used to colour-code the cards.
@@ -63,14 +65,34 @@ const CARDS: &[Card] = &[
}, },
]; ];
/// A responsive card gallery built entirely out of [`AtomLayout`]s. /// Colours pulled from the [`egui::Visuals`] up front, so the card builders (which only see an
/// [`AtomUi`]) don't need to reach back into the [`egui::Ui`].
#[derive(Clone, Copy)]
struct CardTheme {
card_fill: Color32,
card_stroke: Stroke,
chip_fill_base: Color32,
}
/// A responsive card gallery built with the [`AtomUi`] widget API.
/// ///
/// Think of it as flexbox: the outer list is a wrapping row of `grow` cards, and inside each card /// Think of it as nested flexbox, all assembled by adding widgets to an [`AtomUi`] (never
/// a column holds a mock image, a title, a description and a wrapping row of `grow` tags. Resize /// hand-building [`egui::Atoms`]):
/// the window to watch the cards and tags reflow. /// - a top-level row of [ tag-filter sidebar · card gallery ],
/// - the gallery is a wrapping row of `grow` cards,
/// - each card is a column with a wrapping row of `grow` tags and a footer of real [`Button`]s.
///
/// Resize the window to watch every level reflow, and click sidebar tags to filter the cards.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Default)] #[derive(Default)]
pub struct AtomLayoutDemo {} pub struct AtomLayoutDemo {
/// Titles of the cards the user has "liked" — proves the footer buttons are real, clickable
/// widgets with their own [`egui::Response`].
liked: BTreeSet<String>,
/// Tags selected in the sidebar. A card is shown if this is empty or the card has one of them.
selected_tags: BTreeSet<String>,
}
impl crate::Demo for AtomLayoutDemo { impl crate::Demo for AtomLayoutDemo {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
@@ -91,130 +113,272 @@ impl crate::Demo for AtomLayoutDemo {
impl crate::View for AtomLayoutDemo { impl crate::View for AtomLayoutDemo {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
ui.small( ui.small(
"A responsive card gallery — one wrapping AtomLayout of grow cards, each itself a \ "Nested flexbox built entirely with the AtomUi widget API: a tag-filter sidebar next \
column with a wrapping row of grow tags. Resize the window to watch it reflow.", to a wrapping gallery of grow cards, each a column with a wrapping row of grow tags \
and a footer of real Button widgets. Resize the window to watch every level reflow.",
); );
ui.add_space(8.0); ui.add_space(8.0);
let theme = CardTheme {
card_fill: ui.visuals().faint_bg_color,
card_stroke: ui.visuals().widgets.noninteractive.bg_stroke,
chip_fill_base: ui.visuals().window_fill(),
};
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.id_salt("cards_scroll") .id_salt("cards_scroll")
.auto_shrink([false, false]) .auto_shrink([false, false])
.show(ui, |ui| { .show(ui, |ui| {
let mut cards = Atoms::default(); let full_width = ui.available_width();
for c in CARDS { let gap = 12.0;
cards.push_right(card(ui, c)); // Split the row between sidebar and gallery: the sidebar is locked to this fraction
} // of the width (minus the gap) and the `grow` gallery fills the rest.
AtomLayout::new(cards) let sidebar_width = (full_width - gap) / 4.0;
.wrap(true) let Self {
.gap(12.0) liked,
.align2(Align2::LEFT_TOP) selected_tags,
// Fill the available width so the `grow` cards stretch to share each row. } = self;
.min_size(Vec2::new(ui.available_width(), 0.0)) // Top-level flex row: [ sidebar · gallery ]. `min_size` makes the row fill the
.show(ui); // available width so the `grow` gallery expands beside the fixed-width sidebar.
ui.atom_builder(
AtomLayout::new(())
.gap(gap)
.align2(Align2::LEFT_TOP)
.min_size(Vec2::new(full_width, 0.0)),
|root| {
sidebar(root, theme, selected_tags, sidebar_width);
gallery(root, theme, liked, selected_tags);
},
);
}); });
} }
} }
/// One card: a vertical column of [ mock image · title · description · tags · footer ]. Marked /// The tag-filter sidebar: a header above a wrapping, justified row of tag [`Button`]s — laid out
/// `grow` so cards share each row's width; the contents re-wrap to the grown width automatically. /// exactly like the in-card tags. Clicking a tag toggles it in `selected_tags`, filtering the
fn card(ui: &egui::Ui, card: &Card) -> Atom<'static> { /// gallery.
// Mock image: an empty layout with a coloured fill. It's a nested layout, so it stretches to fn sidebar<'a>(
// the full card width. root: &mut AtomUi<'_, 'a>,
let image = Atom::layout( theme: CardTheme,
AtomLayout::new(()) selected_tags: &mut BTreeSet<String>,
.frame( width: f32,
Frame::new() ) {
.fill(card.accent.gamma_multiply(0.8)) let frame = Frame::new()
.corner_radius(CornerRadius::same(6)), .fill(theme.card_fill)
) .stroke(theme.card_stroke)
.min_size(Vec2::new(0.0, 96.0)),
);
// Tags: a wrapping row where each tag grows to justify the line.
let mut tag_atoms = Atoms::default();
for t in card.tags {
tag_atoms.push_right(tag(ui, card.accent, t).atom_grow(true));
}
let tags = Atom::layout(
AtomLayout::new(tag_atoms)
.wrap(true)
.gap(4.0)
.align2(Align2::LEFT_TOP),
);
// Footer: Like / Share buttons that split the card width.
let footer = Atom::layout(
AtomLayout::new((
footer_button(ui, "♥ Like").atom_grow(true),
footer_button(ui, "↗ Share").atom_grow(true),
))
.gap(6.0),
);
let card_frame = Frame::new()
.fill(ui.visuals().faint_bg_color)
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
.corner_radius(CornerRadius::same(8)) .corner_radius(CornerRadius::same(8))
.inner_margin(Margin::same(8)); .inner_margin(Margin::same(8));
let column = AtomLayout::new(( root.scope_builder(
image, AtomLayout::new(())
RichText::new(card.title) .direction(Direction::TopDown)
.strong()
.atom_align(Align2::LEFT_CENTER),
RichText::new(card.description)
.small()
.weak()
.atom_align(Align2::LEFT_TOP)
// `shrink` lets the description wrap to the card width instead of its full text width
// dictating how wide the card has to be.
.atom_shrink(true),
tags,
// A `grow` spacer eats any leftover vertical space, pinning the footer to the bottom — so
// footers line up across cards of different heights (cards in a row are equal height).
Atom::grow(),
footer,
))
.direction(Direction::TopDown)
.frame(card_frame)
.gap(6.0)
.wrap_mode(TextWrapMode::Wrap)
// Stretch full-width pieces (image, footer) to the card's grown width.
.cross_justify(true)
.align2(Align2::LEFT_TOP);
// `atom_max_width` sets the card's natural (flex-basis) width; `grow` lets it stretch to share
// the row, and the core re-measures the contents at that grown width so they reflow.
Atom::layout(column).atom_grow(true).atom_max_width(230.0)
}
/// A flat footer button (e.g. Like / Share).
fn footer_button(ui: &egui::Ui, text: &str) -> Atom<'static> {
let visuals = &ui.visuals().widgets.inactive;
let frame = Frame::new()
.inner_margin(Margin::symmetric(8, 4))
.corner_radius(CornerRadius::same(6))
.fill(visuals.bg_fill)
.stroke(visuals.bg_stroke);
Atom::layout(
AtomLayout::new(RichText::new(text.to_owned()).color(ui.visuals().weak_text_color()))
.frame(frame) .frame(frame)
.align2(Align2::CENTER_CENTER), .gap(6.0)
) // Stretch the header and tag row to the sidebar's width.
.cross_justify(true)
.align2(Align2::LEFT_TOP)
// Lock the sidebar to `width`; the `grow` gallery fills the rest of the row.
.min_size(Vec2::new(width, 0.0)),
// `min_size` (above) and `atom_max_width` pin the sidebar to `width` exactly: the min stops
// it shrinking and the max stops the wrapping tag row from expanding past it.
atom().atom_max_width(width),
|bar| {
bar.add(
atom().atom_align(Align2::LEFT_CENTER),
AtomLayout::new(RichText::new("Filter by tag").strong()),
);
// A wrapping row where each tag button grows to justify the line — just like the tags
// inside each card.
bar.scope_builder(
AtomLayout::new(())
.wrap(true)
.gap(4.0)
.align2(Align2::LEFT_TOP),
atom(),
|tags| {
for tag in all_tags() {
let selected = selected_tags.contains(tag);
// `grow` spacers either side center the label in the grown button.
let button =
Button::new((atom().atom_grow(true), tag, atom().atom_grow(true)))
.selected(selected);
if tags.add(atom().atom_grow(true), button).clicked() {
if selected {
selected_tags.remove(tag);
} else {
selected_tags.insert(tag.to_owned());
}
}
}
},
);
// Reset filter. Disabling it when nothing is selected would need enabled-state plumbing
// through AtomUi, so keep it simple and always clear.
if !selected_tags.is_empty()
&& bar
.add(
atom(),
Button::new((atom().atom_grow(true), "Clear", atom().atom_grow(true))),
)
.clicked()
{
selected_tags.clear();
}
},
);
} }
/// A small filled tag chip. /// The card gallery: a `grow`, wrapping row of cards filtered by the selected tags.
fn tag(ui: &egui::Ui, accent: Color32, text: &str) -> Atom<'static> { fn gallery<'a>(
let bg = ui.visuals().window_fill(); root: &mut AtomUi<'_, 'a>,
theme: CardTheme,
liked: &mut BTreeSet<String>,
selected_tags: &BTreeSet<String>,
) {
root.scope_builder(
AtomLayout::new(())
.wrap(true)
.gap(12.0)
.align2(Align2::LEFT_TOP),
// `grow` lets the gallery fill the width left over by the sidebar; the core re-measures it
// at that grown width, so the cards inside wrap to fit.
atom().atom_grow(true),
|cards| {
for c in CARDS {
if card_matches(c, selected_tags) {
card(cards, c, theme, liked);
}
}
},
);
}
/// A card is shown when no tag is selected, or it carries at least one of the selected tags.
fn card_matches(c: &Card, selected_tags: &BTreeSet<String>) -> bool {
selected_tags.is_empty() || c.tags.iter().any(|t| selected_tags.contains(*t))
}
/// All tags across every card, de-duplicated and sorted (a `BTreeSet` iterates in order).
fn all_tags() -> Vec<&'static str> {
CARDS
.iter()
.flat_map(|c| c.tags.iter().copied())
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
/// One card: a vertical column of [ mock image · title · description · tags · footer ]. The
/// wrapper atom is marked `grow` so cards share each row's width; `atom_max_width` sets its natural
/// (flex-basis) width, and the core re-measures the contents at the grown width so they reflow.
fn card<'a>(
cards: &mut AtomUi<'_, 'a>,
c: &'static Card,
theme: CardTheme,
liked: &mut BTreeSet<String>,
) {
let card_frame = Frame::new()
.fill(theme.card_fill)
.stroke(theme.card_stroke)
.corner_radius(CornerRadius::same(8))
.inner_margin(Margin::same(8));
cards.scope_builder(
AtomLayout::new(())
.direction(Direction::TopDown)
.frame(card_frame)
.gap(6.0)
.wrap_mode(TextWrapMode::Wrap)
// Stretch full-width pieces (image, footer) to the card's grown width.
.cross_justify(true)
.align2(Align2::LEFT_TOP),
atom().atom_grow(true).atom_max_width(230.0),
|col| {
// Mock image: an empty nested layout with a coloured fill. `cross_justify` stretches it
// to the full card width.
col.add(
atom(),
AtomLayout::new(())
.frame(
Frame::new()
.fill(c.accent.gamma_multiply(0.8))
.corner_radius(CornerRadius::same(6)),
)
.min_size(Vec2::new(0.0, 96.0)),
);
// Title.
col.add(
atom().atom_align(Align2::LEFT_CENTER),
AtomLayout::new(RichText::new(c.title).strong()),
);
// Description. `shrink` lets it wrap to the card width instead of its full text width
// dictating how wide the card has to be; the nested layout needs its own `wrap_mode`
// because nested layouts don't inherit the column's.
col.add(
atom().atom_shrink(true).atom_align(Align2::LEFT_TOP),
AtomLayout::new(RichText::new(c.description).small().weak())
.wrap_mode(TextWrapMode::Wrap),
);
// Tags: a wrapping row where each tag grows to justify the line.
col.scope_builder(
AtomLayout::new(())
.wrap(true)
.gap(4.0)
.align2(Align2::LEFT_TOP),
atom(),
|tags| {
for t in c.tags {
tags.add(atom().atom_grow(true), tag_chip(theme, c.accent, t));
}
},
);
// A `grow` spacer eats any leftover vertical space, pinning the footer to the bottom —
// so footers line up across cards of different heights (cards in a row are equal
// height).
col.add(atom().atom_grow(true), atom());
// Footer: real Like / Share buttons that split the card width.
col.scope_builder(AtomLayout::new(()).gap(6.0), atom(), |footer| {
let is_liked = liked.contains(c.title);
// `grow` spacers on either side of the label center it in the grown button.
let like_text = if is_liked { "♥ Liked" } else { "♡ Like" };
if footer
.add(
atom().atom_grow(true),
Button::new((atom().atom_grow(true), like_text, atom().atom_grow(true))),
)
.clicked()
{
if is_liked {
liked.remove(c.title);
} else {
liked.insert(c.title.to_owned());
}
}
footer.add(
atom().atom_grow(true),
Button::new((atom().atom_grow(true), "↗ Share", atom().atom_grow(true))),
);
});
},
);
}
/// A small filled tag chip (decorative, accent-coloured).
fn tag_chip<'a>(theme: CardTheme, accent: Color32, text: &str) -> AtomLayout<'a> {
let frame = Frame::new() let frame = Frame::new()
.inner_margin(Margin::symmetric(6, 1)) .inner_margin(Margin::symmetric(6, 1))
.corner_radius(CornerRadius::same(8)) .corner_radius(CornerRadius::same(8))
.fill(bg.lerp_to_gamma(accent, 0.22)) .fill(theme.chip_fill_base.lerp_to_gamma(accent, 0.22))
.stroke(Stroke::new(1.0, accent.gamma_multiply(0.6))); .stroke(Stroke::new(1.0, accent.gamma_multiply(0.6)));
Atom::layout( AtomLayout::new(RichText::new(text.to_owned()).small().color(accent))
AtomLayout::new(RichText::new(text.to_owned()).small().color(accent)) .frame(frame)
.frame(frame) .align2(Align2::CENTER_CENTER)
.align2(Align2::CENTER_CENTER) .gap(0.0)
.gap(0.0),
)
} }

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:829557a546a642f7ae6808ada89fb02d7062537d0c428ad4e314557d9adb994c oid sha256:e08edc240db2ea09b5b36f1fa1772a17999248898848651a56c25d135dfa2dea
size 78507 size 81763

View File

@@ -0,0 +1,95 @@
//! Repro for a wrapping bug: a `grow`, wrapping nested [`AtomLayout`] placed next to a fixed-width
//! sibling (think: the atom-layout demo's tag-filter sidebar + card gallery) wraps "too late" and
//! overflows the container.
//!
//! The layout under test is a non-wrapping row of `[ fixed sidebar · grow wrapping gallery ]`,
//! forced to the container width via `min_size`. We render it at a range of shrinking container
//! widths and assert the laid-out width never exceeds the container.
use egui::{Atom, AtomExt as _, AtomLayout, Atoms, CornerRadius, Frame, Margin, Stroke, Vec2};
use egui_kittest::Harness;
use std::cell::Cell;
const SIDEBAR_WIDTH: f32 = 120.0;
const GAP: f32 = 12.0;
fn chip_frame() -> Frame {
Frame::new()
.inner_margin(Margin::symmetric(6, 2))
.corner_radius(CornerRadius::same(4))
.stroke(Stroke::new(1.0, egui::Color32::GRAY))
}
/// A single grow chip, like a tag button in the demo.
fn chip(word: &str) -> Atom<'static> {
Atom::layout(AtomLayout::new(word.to_owned()).frame(chip_frame())).atom_grow(true)
}
/// `[ fixed sidebar · grow wrapping gallery of chips ]`, forced to `width` via `min_size`.
fn root(width: f32) -> AtomLayout<'static> {
let words = [
"aurora",
"night",
"long-exposure",
"iceland",
"winter",
"jungle",
"macro",
"wildlife",
"humid",
"sahara",
"golden-hour",
"minimal",
];
let mut chips = Atoms::default();
for word in words {
chips.push_right(chip(word));
}
let gallery = Atom::layout(AtomLayout::new(chips).wrap(true).gap(6.0)).atom_grow(true);
let sidebar =
Atom::layout(AtomLayout::new("sidebar").frame(chip_frame())).atom_max_width(SIDEBAR_WIDTH);
AtomLayout::new((sidebar, gallery))
.gap(GAP)
.min_size(Vec2::new(width, 0.0))
}
/// Render `root` at the given container width and return the laid-out (allocated) width.
fn laid_out_width(width: f32) -> f32 {
let measured = Cell::new(0.0_f32);
let mut harness = Harness::builder()
.with_size(Vec2::new(width, 600.0))
.build_ui(|ui| {
let avail = ui.available_width();
let response = root(avail).show(ui);
measured.set(response.response.rect.width());
});
harness.run();
measured.get()
}
#[test]
fn atom_wrap_no_overflow_when_shrinking() {
let mut failures = Vec::new();
// Shrink the container step by step.
for width in (260..=820).rev().step_by(40) {
let w = width as f32;
let laid_out = laid_out_width(w);
let overflow = laid_out - w;
println!("container {w:6.1} -> laid out {laid_out:7.1} (overflow {overflow:+.1})");
// Allow a pixel of rounding slack.
if overflow > 1.0 {
failures.push(format!(
"container {w:.1}: laid out {laid_out:.1} (overflow {overflow:.1}px)"
));
}
}
assert!(
failures.is_empty(),
"the wrapping layout overflowed its container:\n{}",
failures.join("\n")
);
}

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e43b410ab0a06b40da5ad9c7fd105d11201d44a3afd63fd213429cc39013c0d4 oid sha256:d1aac2317afe29ae297b9082d236432327df4f5210b91b0d5694ae2deec765e1
size 6964 size 6964