use crate::{
AtomKind, AtomPaintArgs, Atoms, Direction, FontSelection, Frame, IdSalt, Image, IntoAtoms,
Response, SizedAtom, SizedAtomKind, Stroke, Ui, WidgetAtom,
text_selection::LabelSelectionState,
};
use alloc::sync::Arc;
use core::ops::{Deref, DerefMut};
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
use epaint::text::TextWrapMode;
use epaint::{Color32, Galley};
use smallvec::SmallVec;
#[inline]
fn main_cross_axis(direction: Direction) -> (usize, usize) {
let main = usize::from(!direction.is_horizontal());
(main, 1 - main)
}
#[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)
}
}
#[inline]
fn main_cross_rect(direction: Direction, aligned_rect: Rect, min_main: f32, max_main: f32) -> Rect {
if direction.is_horizontal() {
Rect::from_x_y_ranges(min_main..=max_main, aligned_rect.y_range())
} else {
Rect::from_x_y_ranges(aligned_rect.x_range(), min_main..=max_main)
}
}
pub type CustomRects = SmallVec<[(IdSalt, Rect); 1]>;
#[derive(Clone)]
pub struct ContainerAtom<'a> {
pub atoms: Atoms<'a>,
gap: Option<f32>,
pub(crate) frame: Frame,
fallback_text_color: Option<Color32>,
fallback_font: Option<FontSelection>,
min_size: Vec2,
max_size: Vec2,
wrap_mode: Option<TextWrapMode>,
align2: Option<Align2>,
direction: Direction,
}
impl Default for ContainerAtom<'_> {
fn default() -> Self {
Self::new(())
}
}
impl<'a> ContainerAtom<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
atoms: atoms.into_atoms(),
gap: None,
frame: Frame::default(),
fallback_text_color: None,
fallback_font: None,
min_size: Vec2::ZERO,
max_size: Vec2::INFINITY,
wrap_mode: None,
align2: None,
direction: Direction::LeftToRight,
}
}
pub fn measure(self, ui: &Ui, available_size: Vec2) -> SizedContainerAtom<'a> {
let Self {
mut atoms,
gap,
frame,
fallback_text_color,
min_size,
mut max_size,
wrap_mode,
align2,
fallback_font,
direction,
} = self;
let fallback_font = fallback_font.unwrap_or_default();
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
if wrap_mode != TextWrapMode::Extend {
let any_shrink = atoms.any_shrink();
if !any_shrink {
let first_text = atoms
.iter_mut()
.find(|a| matches!(a.kind, AtomKind::Text(..)));
if let Some(atom) = first_text {
atom.shrink = true; }
}
}
let fallback_text_color =
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing);
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_inner_size = available_size - frame.total_margin().sum();
let (main_axis, cross_axis) = main_cross_axis(direction);
let mut inner_main = 0.0;
let mut intrinsic_main = 0.0;
let mut intrinsic_cross: f32 = 0.0;
let mut cross_size: 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(|| ui.layout().align2());
if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
inner_main += gap_space;
intrinsic_main += gap_space;
}
let auto_id = ui.next_auto_id();
for (idx, mut item) in atoms.into_iter().enumerate() {
if let AtomKind::Widget(widget) = &mut item.kind {
widget.id.get_or_insert_with(|| auto_id.with(idx));
}
if item.grow {
grow_count += 1;
}
if item.shrink {
debug_assert!(
shrink_item.is_none(),
"Only one atomic may be marked as shrink. {item:?}"
);
if shrink_item.is_none() {
shrink_item = Some((idx, item));
continue;
}
}
let sized = item.into_sized(
ui,
available_inner_size,
Some(wrap_mode),
fallback_font.clone(),
);
let size = sized.size;
inner_main += size[main_axis];
intrinsic_main += sized.intrinsic_size[main_axis];
cross_size = cross_size.at_least(size[cross_axis]);
intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]);
sized_items.push(sized);
}
if let Some((index, item)) = shrink_item {
let available_size_for_shrink_item = main_cross_vec(
direction,
available_inner_size[main_axis] - inner_main,
available_inner_size[cross_axis],
);
let sized = item.into_sized(
ui,
available_size_for_shrink_item,
Some(wrap_mode),
fallback_font,
);
let size = sized.size;
inner_main += size[main_axis];
intrinsic_main += sized.intrinsic_size[main_axis];
cross_size = cross_size.at_least(size[cross_axis]);
intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]);
sized_items.insert(index, sized);
}
let margin = frame.total_margin();
let inner_size = main_cross_vec(direction, inner_main, cross_size);
let outer_size = (inner_size + margin.sum()).at_least(min_size);
let intrinsic_size = (main_cross_vec(direction, intrinsic_main, intrinsic_cross)
+ margin.sum())
.at_least(min_size);
SizedContainerAtom {
sized_atoms: sized_items,
frame,
fallback_text_color,
outer_size,
intrinsic_size,
grow_count,
inner_size,
align2,
gap,
direction,
}
}
}
macro_rules! shared_container_builders {
(
$(
$(#[$meta:meta])*
fn $name:ident($self:ident, $($arg:ident: $arg_ty:ty),* $(,)?) $body:block
)*
) => {
impl<'a> ContainerAtom<'a> {
$(
$(#[$meta])*
#[inline]
pub fn $name(mut $self, $($arg: $arg_ty),*) -> Self {
$body
$self
}
)*
}
impl<'a> WidgetAtom<'a> {
$(
$(#[$meta])*
#[inline]
pub fn $name(mut self, $($arg: $arg_ty),*) -> Self {
self.container = self.container.$name($($arg),*);
self
}
)*
}
};
}
shared_container_builders! {
fn gap(self, gap: f32) {
self.gap = Some(gap);
}
fn fallback_gap(self, gap: f32) {
self.gap = self.gap.or(Some(gap));
}
fn direction(self, direction: Direction) {
self.direction = direction;
}
fn frame(self, frame: Frame) {
self.frame = frame;
}
fn fallback_text_color(self, color: Color32) {
self.fallback_text_color = Some(color);
}
fn fallback_font(self, font: impl Into<FontSelection>) {
self.fallback_font = Some(font.into());
}
fn min_size(self, size: Vec2) {
self.min_size = size;
}
fn max_size(self, size: Vec2) {
self.max_size = size;
}
fn max_width(self, width: f32) {
self.max_size.x = width;
}
fn max_height(self, height: f32) {
self.max_size.y = height;
}
fn wrap_mode(self, wrap_mode: TextWrapMode) {
self.wrap_mode = Some(wrap_mode);
}
fn align2(self, align2: Align2) {
self.align2 = Some(align2);
}
}
#[derive(Clone, Debug)]
pub struct SizedContainerAtom<'a> {
pub(crate) outer_size: Vec2,
inner_size: Vec2,
sized_atoms: Vec<SizedAtom<'a>>,
pub frame: Frame,
pub fallback_text_color: Color32,
pub(crate) intrinsic_size: Vec2,
grow_count: usize,
align2: Align2,
gap: f32,
direction: Direction,
}
impl<'atom> SizedContainerAtom<'atom> {
pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
self.sized_atoms.iter().map(|atom| &atom.kind)
}
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
}
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Image { image, size: _ } = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Image { image, size: _ } = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn map_kind<F>(&mut self, mut f: F)
where
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
{
for kind in self.iter_kinds_mut() {
*kind = f(core::mem::take(kind));
}
}
pub fn map_images<F>(&mut self, mut f: F)
where
F: FnMut(Image<'atom>) -> Image<'atom>,
{
self.map_kind(|kind| {
if let SizedAtomKind::Image { image, size } = kind {
SizedAtomKind::Image {
image: f(image),
size,
}
} else {
kind
}
});
}
pub fn paint_at(self, ui: &Ui, rect: Rect) -> CustomRects {
self.paint_at_with_selection(ui, rect, None)
}
pub(crate) fn paint_at_with_selection(
self,
ui: &Ui,
rect: Rect,
selection_response: Option<&Response>,
) -> CustomRects {
let Self {
mut sized_atoms,
frame,
fallback_text_color,
grow_count,
inner_size,
align2,
gap,
direction,
..
} = self;
let inner_rect = rect - frame.total_margin();
ui.painter().add(frame.paint(inner_rect));
let (main_axis, cross_axis) = main_cross_axis(direction);
let main_to_fill = inner_rect.size()[main_axis];
let inner_main = inner_size[main_axis];
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 block_main = if grow_count > 0 {
main_to_fill
} else {
inner_main
};
let block_size = main_cross_vec(direction, block_main, inner_size[cross_axis]);
let aligned_rect = align2.align_size_within_rect(block_size, inner_rect);
if matches!(direction, Direction::RightToLeft | Direction::BottomUp) {
sized_atoms.reverse();
}
let mut cursor = aligned_rect.min.to_vec2()[main_axis];
let mut custom_rects = CustomRects::new();
for sized in sized_atoms {
let size = sized.size;
let growth = if sized.is_grow() { grow_main } else { 0.0 };
let atom_main = size[main_axis] + growth;
let cell = main_cross_rect(direction, aligned_rect, cursor, cursor + atom_main);
cursor += atom_main + gap;
let item_rect = sized.align.align_size_within_rect(size, cell);
if let Some(id) = sized.id {
debug_assert!(
!custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
custom_rects.push((id, item_rect));
}
match sized.kind {
SizedAtomKind::Text(galley) => {
if let Some(response) = selection_response {
LabelSelectionState::label_text_selection(
ui,
response,
item_rect.min,
galley,
fallback_text_color,
Stroke::NONE,
);
} else {
ui.painter()
.galley(item_rect.min, galley, fallback_text_color);
}
}
SizedAtomKind::Image { image, size: _ } => {
image.paint_at(ui, item_rect);
}
SizedAtomKind::Empty { .. } => {}
SizedAtomKind::Paint { paint, size: _ } => {
paint(
ui,
AtomPaintArgs {
rect: item_rect,
fallback_text_color,
},
);
}
SizedAtomKind::Widget(widget) => {
widget.paint_at(ui, cell);
}
SizedAtomKind::Container(container) => {
container.paint_at(ui, cell);
}
}
}
custom_rects
}
}
impl<'a> Deref for ContainerAtom<'a> {
type Target = Atoms<'a>;
fn deref(&self) -> &Self::Target {
&self.atoms
}
}
impl DerefMut for ContainerAtom<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.atoms
}
}
impl<'a> Deref for SizedContainerAtom<'a> {
type Target = [SizedAtom<'a>];
fn deref(&self) -> &Self::Target {
&self.sized_atoms
}
}
impl DerefMut for SizedContainerAtom<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sized_atoms
}
}