1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 22:30:03 -04:00

Add AtomLayout::direction

This commit is contained in:
lucasmerlin
2026-06-02 17:17:30 +02:00
parent 5e8bd371bc
commit 63218a9db6

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
AtomKind, Atoms, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom, AtomKind, Atoms, Direction, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense,
SizedAtomKind, Ui, Widget, SizedAtom, SizedAtomKind, Ui, Widget,
}; };
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2}; use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
use epaint::text::TextWrapMode; use epaint::text::TextWrapMode;
@@ -9,6 +9,47 @@ use smallvec::SmallVec;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
/// The main-axis component of `v` for `direction` (x for horizontal, y for vertical).
#[inline]
fn main_axis(direction: Direction, v: Vec2) -> f32 {
if direction.is_horizontal() {
v.x
} else {
v.y
}
}
/// The cross-axis component of `v` for `direction` (y for horizontal, x for vertical).
#[inline]
fn cross_axis(direction: Direction, v: Vec2) -> f32 {
if direction.is_horizontal() {
v.y
} else {
v.x
}
}
/// Build a [`Vec2`] from `main`/`cross` components for `direction`.
#[inline]
fn main_cross_vec(direction: Direction, main: f32, cross: f32) -> Vec2 {
if direction.is_horizontal() {
Vec2::new(main, cross)
} else {
Vec2::new(cross, main)
}
}
/// Build a cell [`Rect`] spanning `block` fully on the cross axis and `[min_main, max_main]`
/// along the main axis.
#[inline]
fn main_cross_rect(direction: Direction, block: Rect, min_main: f32, max_main: f32) -> Rect {
if direction.is_horizontal() {
Rect::from_x_y_ranges(min_main..=max_main, block.y_range())
} else {
Rect::from_x_y_ranges(block.x_range(), min_main..=max_main)
}
}
/// Intra-widget layout utility. /// Intra-widget layout utility.
/// ///
/// Used to lay out and paint [`crate::Atom`]s. /// Used to lay out and paint [`crate::Atom`]s.
@@ -30,7 +71,7 @@ use std::sync::Arc;
/// [`AllocatedAtomLayout`] for interaction styling. /// [`AllocatedAtomLayout`] for interaction styling.
#[derive(Clone)] #[derive(Clone)]
pub struct AtomLayout<'a> { pub struct AtomLayout<'a> {
id: Option<Id>, pub(crate) id: Option<Id>,
pub atoms: Atoms<'a>, pub atoms: Atoms<'a>,
gap: Option<f32>, gap: Option<f32>,
pub(crate) frame: Frame, pub(crate) frame: Frame,
@@ -41,6 +82,7 @@ pub struct AtomLayout<'a> {
max_size: Vec2, max_size: Vec2,
wrap_mode: Option<TextWrapMode>, wrap_mode: Option<TextWrapMode>,
align2: Option<Align2>, align2: Option<Align2>,
direction: Direction,
} }
impl Default for AtomLayout<'_> { impl Default for AtomLayout<'_> {
@@ -63,6 +105,7 @@ impl<'a> AtomLayout<'a> {
max_size: Vec2::INFINITY, max_size: Vec2::INFINITY,
wrap_mode: None, wrap_mode: None,
align2: None, align2: None,
direction: Direction::LeftToRight,
} }
} }
@@ -173,6 +216,20 @@ impl<'a> AtomLayout<'a> {
self self
} }
/// Set the [`Direction`] the [`crate::Atom`]s are laid out along.
///
/// The default is [`Direction::LeftToRight`] (a horizontal row). Use
/// [`Direction::TopDown`] (or [`Direction::BottomUp`]) to stack atoms vertically.
///
/// The main axis (the direction) is where `grow`/`shrink` and the gap apply; the cross axis
/// is sized to the largest atom. [`Self::align2`] positions the whole block within the
/// allocated [`Rect`].
#[inline]
pub fn direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
/// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go. /// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go.
pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse { pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
self.allocate(ui).paint(ui) self.allocate(ui).paint(ui)
@@ -202,6 +259,7 @@ impl<'a> AtomLayout<'a> {
wrap_mode, wrap_mode,
align2, align2,
fallback_font, fallback_font,
direction,
} = self; } = self;
let fallback_font = fallback_font.unwrap_or_default(); let fallback_font = fallback_font.unwrap_or_default();
@@ -230,8 +288,13 @@ impl<'a> AtomLayout<'a> {
// max_size has no effect in justified layouts. If we'd limit the available size here, // max_size has no effect in justified layouts. If we'd limit the available size here,
// the content would be sized differently than the frame which would look weird. // the content would be sized differently than the frame which would look weird.
if ui.layout().horizontal_justify() { // This only applies along the main axis (the direction we lay atoms out along).
max_size.x = f32::INFINITY; if direction.is_horizontal() {
if ui.layout().horizontal_justify() {
max_size.x = f32::INFINITY;
}
} else if ui.layout().vertical_justify() {
max_size.y = f32::INFINITY;
} }
let available_size = available_size.at_most(max_size).at_least(min_size); let available_size = available_size.at_most(max_size).at_least(min_size);
@@ -239,14 +302,18 @@ impl<'a> AtomLayout<'a> {
// The size available for the content // The size available for the content
let available_inner_size = available_size - frame.total_margin().sum(); let available_inner_size = available_size - frame.total_margin().sum();
let mut inner_width = 0.0; // We work in main/cross axis terms so the same code handles horizontal and vertical
// layouts. For a horizontal `direction`, main = x and cross = y; for vertical it's
// swapped. `grow`/`shrink`/`gap` apply along the main axis; the cross axis is sized to
// the largest atom.
let mut inner_main = 0.0;
// intrinsic width / height is the ideal size of the widget, e.g. the size where the // intrinsic main / cross is the ideal size of the widget, e.g. the size where the
// text is not wrapped. Used to set Response::intrinsic_size. // text is not wrapped. Used to set Response::intrinsic_size.
let mut intrinsic_width = 0.0; let mut intrinsic_main = 0.0;
let mut intrinsic_height = 0.0; let mut intrinsic_cross: f32 = 0.0;
let mut height: f32 = 0.0; let mut cross_size: f32 = 0.0;
let mut sized_items = Vec::new(); let mut sized_items = Vec::new();
@@ -260,8 +327,8 @@ impl<'a> AtomLayout<'a> {
if atoms.len() > 1 { if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0); let gap_space = gap * (atoms.len() as f32 - 1.0);
inner_width += gap_space; inner_main += gap_space;
intrinsic_width += gap_space; intrinsic_main += gap_space;
} }
for (idx, item) in atoms.into_iter().enumerate() { for (idx, item) in atoms.into_iter().enumerate() {
@@ -286,19 +353,22 @@ impl<'a> AtomLayout<'a> {
); );
let size = sized.size; let size = sized.size;
inner_width += size.x; inner_main += main_axis(direction, size);
intrinsic_width += sized.intrinsic_size.x; intrinsic_main += main_axis(direction, sized.intrinsic_size);
height = height.at_least(size.y); cross_size = cross_size.at_least(cross_axis(direction, size));
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y); intrinsic_cross = intrinsic_cross.at_least(cross_axis(direction, sized.intrinsic_size));
sized_items.push(sized); sized_items.push(sized);
} }
if let Some((index, item)) = shrink_item { if let Some((index, item)) = shrink_item {
// The `shrink` item gets the remaining space // The `shrink` item gets the remaining space along the main axis.
let available_size_for_shrink_item = let available_size_for_shrink_item = main_cross_vec(
Vec2::new(available_inner_size.x - inner_width, available_inner_size.y); direction,
main_axis(direction, available_inner_size) - inner_main,
cross_axis(direction, available_inner_size),
);
let sized = item.into_sized( let sized = item.into_sized(
ui, ui,
@@ -308,20 +378,21 @@ impl<'a> AtomLayout<'a> {
); );
let size = sized.size; let size = sized.size;
inner_width += size.x; inner_main += main_axis(direction, size);
intrinsic_width += sized.intrinsic_size.x; intrinsic_main += main_axis(direction, sized.intrinsic_size);
height = height.at_least(size.y); cross_size = cross_size.at_least(cross_axis(direction, size));
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y); intrinsic_cross = intrinsic_cross.at_least(cross_axis(direction, sized.intrinsic_size));
sized_items.insert(index, sized); sized_items.insert(index, sized);
} }
let margin = frame.total_margin(); let margin = frame.total_margin();
let inner_size = Vec2::new(inner_width, height); let inner_size = main_cross_vec(direction, inner_main, cross_size);
let outer_size = (inner_size + margin.sum()).at_least(min_size); let outer_size = (inner_size + margin.sum()).at_least(min_size);
let intrinsic_size = let intrinsic_size = (main_cross_vec(direction, intrinsic_main, intrinsic_cross)
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size); + margin.sum())
.at_least(min_size);
SizedAtomLayout { SizedAtomLayout {
sized_atoms: sized_items, sized_atoms: sized_items,
@@ -335,6 +406,7 @@ impl<'a> AtomLayout<'a> {
inner_size, inner_size,
align2, align2,
gap, gap,
direction,
} }
} }
@@ -394,6 +466,9 @@ pub struct SizedAtomLayout<'a> {
/// The gap between each [`crate::Atom`] /// The gap between each [`crate::Atom`]
gap: f32, gap: f32,
/// The axis the atoms are laid out along. The main axis carries `grow`/`shrink`/`gap`.
direction: Direction,
} }
/// Instructions for painting an [`AtomLayout`]. /// Instructions for painting an [`AtomLayout`].
@@ -489,13 +564,14 @@ impl<'atom> SizedAtomLayout<'atom> {
/// becomes the base of the returned [`AtomLayoutResponse`]. /// becomes the base of the returned [`AtomLayoutResponse`].
pub fn paint_at(self, ui: &Ui, rect: Rect, response: Response) -> AtomLayoutResponse { pub fn paint_at(self, ui: &Ui, rect: Rect, response: Response) -> AtomLayoutResponse {
let Self { let Self {
sized_atoms, mut sized_atoms,
frame, frame,
fallback_text_color, fallback_text_color,
grow_count, grow_count,
inner_size, inner_size,
align2, align2,
gap, gap,
direction,
.. ..
} = self; } = self;
@@ -503,17 +579,30 @@ impl<'atom> SizedAtomLayout<'atom> {
ui.painter().add(frame.paint(inner_rect)); ui.painter().add(frame.paint(inner_rect));
let width_to_fill = inner_rect.width(); // We position atoms along the main axis (the `direction`) and span the cross axis.
let extra_space = f32::max(width_to_fill - inner_size.x, 0.0); let main_to_fill = main_axis(direction, inner_rect.size());
let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui(); let inner_main = main_axis(direction, inner_size);
let extra_space = f32::max(main_to_fill - inner_main, 0.0);
let grow_main = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
let aligned_rect = if grow_count > 0 { // When something grows, the block fills the available main extent; otherwise it's the
align2.align_size_within_rect(Vec2::new(width_to_fill, inner_size.y), inner_rect) // content's inner size. `align2` then positions the block within `inner_rect`.
let block_main = if grow_count > 0 {
main_to_fill
} else { } else {
align2.align_size_within_rect(inner_size, inner_rect) inner_main
}; };
let block_size = main_cross_vec(direction, block_main, cross_axis(direction, inner_size));
let aligned_rect = align2.align_size_within_rect(block_size, inner_rect);
let mut cursor = aligned_rect.left(); // For reversed directions the first atom sits at the far end, so we lay them out in
// reverse and otherwise share the same forward cursor logic.
if matches!(direction, Direction::RightToLeft | Direction::BottomUp) {
sized_atoms.reverse();
}
// The cursor walks the main axis from the start (left/top) of the aligned block.
let mut cursor = main_axis(direction, aligned_rect.min.to_vec2());
let mut response = AtomLayoutResponse::empty(response); let mut response = AtomLayoutResponse::empty(response);
@@ -521,34 +610,41 @@ impl<'atom> SizedAtomLayout<'atom> {
let size = sized.size; let size = sized.size;
// TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors // TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
// https://github.com/emilk/egui/pull/5830#discussion_r2079627864 // https://github.com/emilk/egui/pull/5830#discussion_r2079627864
let growth = if sized.is_grow() { grow_width } else { 0.0 }; let growth = if sized.is_grow() { grow_main } else { 0.0 };
let frame = aligned_rect let atom_main = main_axis(direction, size) + growth;
.with_min_x(cursor)
.with_max_x(cursor + size.x + growth); // The cell spans the cross axis fully and `atom_main` along the main axis.
cursor = frame.right() + gap; let cell = main_cross_rect(direction, aligned_rect, cursor, cursor + atom_main);
let rect = sized.align.align_size_within_rect(size, frame); cursor += atom_main + gap;
let item_rect = sized.align.align_size_within_rect(size, cell);
if let Some(id) = sized.id { if let Some(id) = sized.id {
debug_assert!( debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id), !response.custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id" "Duplicate custom id"
); );
response.custom_rects.push((id, rect)); response.custom_rects.push((id, item_rect));
} }
match sized.kind { match sized.kind {
SizedAtomKind::Text(galley) => { SizedAtomKind::Text(galley) => {
ui.painter().galley(rect.min, galley, fallback_text_color); ui.painter()
.galley(item_rect.min, galley, fallback_text_color);
} }
SizedAtomKind::Image { image, size: _ } => { SizedAtomKind::Image { image, size: _ } => {
image.paint_at(ui, rect); image.paint_at(ui, item_rect);
} }
SizedAtomKind::Empty { .. } => {} SizedAtomKind::Empty { .. } => {}
SizedAtomKind::Layout(layout) => { SizedAtomKind::Layout(layout) => {
// TODO(lucasmerlin): Add some kind of justify flag to AtomLayout // Hand the nested layout the full (possibly grown) cell extent along the main
let layout_response = ui.interact(frame, layout.id, layout.sense); // axis so its own `grow` atoms can expand, while keeping its measured cross
layout.paint_at(ui, frame, layout_response); // size and honoring this atom's alignment within the cell.
let layout_size =
main_cross_vec(direction, atom_main, cross_axis(direction, size));
let layout_rect = sized.align.align_size_within_rect(layout_size, cell);
let layout_response = ui.interact(cell, layout.id, layout.sense);
layout.paint_at(ui, cell, layout_response);
} }
} }
} }