1use crate::{
2 AtomKind, Atoms, Direction, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense,
3 SizedAtom, SizedAtomKind, Stroke, Ui, Widget, text_selection::LabelSelectionState,
4};
5use core::ops::{Deref, DerefMut};
6use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
7use epaint::text::TextWrapMode;
8use epaint::{Color32, Galley};
9use smallvec::SmallVec;
10use std::sync::Arc;
11
12#[inline]
14fn main_cross_axis(direction: Direction) -> (usize, usize) {
15 let main = usize::from(!direction.is_horizontal());
16 (main, 1 - main)
17}
18
19#[inline]
21fn main_cross_vec(direction: Direction, main: f32, cross: f32) -> Vec2 {
22 if direction.is_horizontal() {
23 Vec2::new(main, cross)
24 } else {
25 Vec2::new(cross, main)
26 }
27}
28
29#[inline]
32fn main_cross_rect(direction: Direction, aligned_rect: Rect, min_main: f32, max_main: f32) -> Rect {
33 if direction.is_horizontal() {
34 Rect::from_x_y_ranges(min_main..=max_main, aligned_rect.y_range())
35 } else {
36 Rect::from_x_y_ranges(aligned_rect.x_range(), min_main..=max_main)
37 }
38}
39
40#[derive(Clone)]
60pub struct AtomLayout<'a> {
61 pub(crate) id: Option<Id>,
62 pub atoms: Atoms<'a>,
63 gap: Option<f32>,
64 pub(crate) frame: Frame,
65 pub(crate) sense: Sense,
66 selectable: bool,
67 fallback_text_color: Option<Color32>,
68 fallback_font: Option<FontSelection>,
69 min_size: Vec2,
70 max_size: Vec2,
71 wrap_mode: Option<TextWrapMode>,
72 align2: Option<Align2>,
73 direction: Direction,
74}
75
76impl Default for AtomLayout<'_> {
77 fn default() -> Self {
78 Self::new(())
79 }
80}
81
82impl<'a> AtomLayout<'a> {
83 pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
84 Self {
85 id: None,
86 atoms: atoms.into_atoms(),
87 gap: None,
88 frame: Frame::default(),
89 sense: Sense::hover(),
90 selectable: false,
91 fallback_text_color: None,
92 fallback_font: None,
93 min_size: Vec2::ZERO,
94 max_size: Vec2::INFINITY,
95 wrap_mode: None,
96 align2: None,
97 direction: Direction::LeftToRight,
98 }
99 }
100
101 #[inline]
105 pub fn gap(mut self, gap: f32) -> Self {
106 self.gap = Some(gap);
107 self
108 }
109
110 #[inline]
112 pub fn frame(mut self, frame: Frame) -> Self {
113 self.frame = frame;
114 self
115 }
116
117 #[inline]
119 pub fn sense(mut self, sense: Sense) -> Self {
120 self.sense = sense;
121 self
122 }
123
124 #[inline]
131 pub fn selectable(mut self, selectable: bool) -> Self {
132 self.selectable = selectable;
133 self
134 }
135
136 #[inline]
140 pub fn fallback_text_color(mut self, color: Color32) -> Self {
141 self.fallback_text_color = Some(color);
142 self
143 }
144
145 #[inline]
147 pub fn fallback_font(mut self, font: impl Into<FontSelection>) -> Self {
148 self.fallback_font = Some(font.into());
149 self
150 }
151
152 #[inline]
157 pub fn min_size(mut self, size: Vec2) -> Self {
158 self.min_size = size;
159 self
160 }
161
162 #[inline]
166 pub fn max_size(mut self, size: Vec2) -> Self {
167 self.max_size = size;
168 self
169 }
170
171 #[inline]
175 pub fn max_width(mut self, width: f32) -> Self {
176 self.max_size.x = width;
177 self
178 }
179
180 #[inline]
184 pub fn max_height(mut self, height: f32) -> Self {
185 self.max_size.y = height;
186 self
187 }
188
189 #[inline]
191 pub fn id(mut self, id: Id) -> Self {
192 self.id = Some(id);
193 self
194 }
195
196 #[inline]
202 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
203 self.wrap_mode = Some(wrap_mode);
204 self
205 }
206
207 #[inline]
215 pub fn align2(mut self, align2: Align2) -> Self {
216 self.align2 = Some(align2);
217 self
218 }
219
220 #[inline]
229 pub fn direction(mut self, direction: Direction) -> Self {
230 self.direction = direction;
231 self
232 }
233
234 pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
236 self.allocate(ui).paint(ui)
237 }
238
239 pub fn measure(self, ui: &Ui, available_size: Vec2) -> SizedAtomLayout<'a> {
251 let Self {
252 id,
253 mut atoms,
254 gap,
255 frame,
256 mut sense,
257 selectable,
258 fallback_text_color,
259 min_size,
260 mut max_size,
261 wrap_mode,
262 align2,
263 fallback_font,
264 direction,
265 } = self;
266
267 let fallback_font = fallback_font.unwrap_or_default();
268
269 if selectable {
270 let allow_drag_to_select = ui.input(|i| !i.has_touch_screen());
273 let mut select_sense = if allow_drag_to_select {
274 Sense::click_and_drag()
275 } else {
276 Sense::click()
277 };
278 select_sense -= Sense::FOCUSABLE;
279 sense |= select_sense;
280 }
281
282 let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
283
284 if wrap_mode != TextWrapMode::Extend {
287 let any_shrink = atoms.any_shrink();
288 if !any_shrink {
289 let first_text = atoms
290 .iter_mut()
291 .find(|a| matches!(a.kind, AtomKind::Text(..)));
292 if let Some(atom) = first_text {
293 atom.shrink = true; }
295 }
296 }
297
298 let id = id.unwrap_or_else(|| ui.next_auto_id());
299
300 let fallback_text_color =
301 fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
302 let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing);
303
304 if direction.is_horizontal() {
308 if ui.layout().horizontal_justify() {
309 max_size.x = f32::INFINITY;
310 }
311 } else if ui.layout().vertical_justify() {
312 max_size.y = f32::INFINITY;
313 }
314
315 let available_size = available_size.at_most(max_size).at_least(min_size);
316
317 let available_inner_size = available_size - frame.total_margin().sum();
319
320 let (main_axis, cross_axis) = main_cross_axis(direction);
325
326 let mut inner_main = 0.0;
327
328 let mut intrinsic_main = 0.0;
331 let mut intrinsic_cross: f32 = 0.0;
332
333 let mut cross_size: f32 = 0.0;
334
335 let mut sized_items = Vec::new();
336
337 let mut grow_count = 0;
338
339 let mut shrink_item = None;
340
341 let align2 = align2.unwrap_or_else(|| {
342 Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()])
343 });
344
345 if atoms.len() > 1 {
346 let gap_space = gap * (atoms.len() as f32 - 1.0);
347 inner_main += gap_space;
348 intrinsic_main += gap_space;
349 }
350
351 for (idx, item) in atoms.into_iter().enumerate() {
352 if item.grow {
353 grow_count += 1;
354 }
355 if item.shrink {
356 debug_assert!(
357 shrink_item.is_none(),
358 "Only one atomic may be marked as shrink. {item:?}"
359 );
360 if shrink_item.is_none() {
361 shrink_item = Some((idx, item));
362 continue;
363 }
364 }
365 let sized = item.into_sized(
366 ui,
367 available_inner_size,
368 Some(wrap_mode),
369 fallback_font.clone(),
370 );
371 let size = sized.size;
372
373 inner_main += size[main_axis];
374 intrinsic_main += sized.intrinsic_size[main_axis];
375
376 cross_size = cross_size.at_least(size[cross_axis]);
377 intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]);
378
379 sized_items.push(sized);
380 }
381
382 if let Some((index, item)) = shrink_item {
383 let available_size_for_shrink_item = main_cross_vec(
385 direction,
386 available_inner_size[main_axis] - inner_main,
387 available_inner_size[cross_axis],
388 );
389
390 let sized = item.into_sized(
391 ui,
392 available_size_for_shrink_item,
393 Some(wrap_mode),
394 fallback_font,
395 );
396 let size = sized.size;
397
398 inner_main += size[main_axis];
399 intrinsic_main += sized.intrinsic_size[main_axis];
400
401 cross_size = cross_size.at_least(size[cross_axis]);
402 intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]);
403
404 sized_items.insert(index, sized);
405 }
406
407 let margin = frame.total_margin();
408 let inner_size = main_cross_vec(direction, inner_main, cross_size);
409 let outer_size = (inner_size + margin.sum()).at_least(min_size);
410 let intrinsic_size = (main_cross_vec(direction, intrinsic_main, intrinsic_cross)
411 + margin.sum())
412 .at_least(min_size);
413
414 SizedAtomLayout {
415 sized_atoms: sized_items,
416 frame,
417 fallback_text_color,
418 id,
419 sense,
420 outer_size,
421 intrinsic_size,
422 grow_count,
423 inner_size,
424 align2,
425 gap,
426 direction,
427 selectable,
428 }
429 }
430
431 pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
435 let sized = self.measure(ui, ui.available_size());
436
437 let (_, rect) = ui.allocate_space(sized.outer_size);
438 let mut response = ui.interact(rect, sized.id, sized.sense);
439 response.set_intrinsic_size(sized.intrinsic_size);
440
441 AllocatedAtomLayout { sized, response }
442 }
443}
444
445#[derive(Clone, Debug)]
451pub struct SizedAtomLayout<'a> {
452 id: Id,
454
455 sense: Sense,
457
458 pub(crate) outer_size: Vec2,
462
463 inner_size: Vec2,
465
466 sized_atoms: Vec<SizedAtom<'a>>,
468
469 pub frame: Frame,
471
472 pub fallback_text_color: Color32,
474
475 pub(crate) intrinsic_size: Vec2,
478
479 grow_count: usize,
481
482 align2: Align2,
484
485 gap: f32,
487
488 direction: Direction,
490
491 selectable: bool,
492}
493
494#[derive(Clone, Debug)]
499pub struct AllocatedAtomLayout<'a> {
500 pub sized: SizedAtomLayout<'a>,
502
503 pub response: Response,
504}
505
506impl<'atom> SizedAtomLayout<'atom> {
507 pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
508 self.sized_atoms.iter().map(|atom| &atom.kind)
509 }
510
511 pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
512 self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
513 }
514
515 pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
516 self.iter_kinds().filter_map(|kind| {
517 if let SizedAtomKind::Image { image, size: _ } = kind {
518 Some(image)
519 } else {
520 None
521 }
522 })
523 }
524
525 pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
526 self.iter_kinds_mut().filter_map(|kind| {
527 if let SizedAtomKind::Image { image, size: _ } = kind {
528 Some(image)
529 } else {
530 None
531 }
532 })
533 }
534
535 pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
536 self.iter_kinds().filter_map(|kind| {
537 if let SizedAtomKind::Text(text) = kind {
538 Some(text)
539 } else {
540 None
541 }
542 })
543 }
544
545 pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
546 self.iter_kinds_mut().filter_map(|kind| {
547 if let SizedAtomKind::Text(text) = kind {
548 Some(text)
549 } else {
550 None
551 }
552 })
553 }
554
555 pub fn map_kind<F>(&mut self, mut f: F)
556 where
557 F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
558 {
559 for kind in self.iter_kinds_mut() {
560 *kind = f(core::mem::take(kind));
561 }
562 }
563
564 pub fn map_images<F>(&mut self, mut f: F)
565 where
566 F: FnMut(Image<'atom>) -> Image<'atom>,
567 {
568 self.map_kind(|kind| {
569 if let SizedAtomKind::Image { image, size } = kind {
570 SizedAtomKind::Image {
571 image: f(image),
572 size,
573 }
574 } else {
575 kind
576 }
577 });
578 }
579
580 pub fn paint_at(self, ui: &Ui, rect: Rect, response: Response) -> AtomLayoutResponse {
586 let Self {
587 mut sized_atoms,
588 frame,
589 fallback_text_color,
590 grow_count,
591 inner_size,
592 align2,
593 gap,
594 direction,
595 selectable,
596 ..
597 } = self;
598
599 let inner_rect = rect - frame.total_margin();
600
601 ui.painter().add(frame.paint(inner_rect));
602
603 let (main_axis, cross_axis) = main_cross_axis(direction);
604
605 let main_to_fill = inner_rect.size()[main_axis];
607 let inner_main = inner_size[main_axis];
608 let extra_space = f32::max(main_to_fill - inner_main, 0.0);
609 let grow_main = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
610
611 let block_main = if grow_count > 0 {
614 main_to_fill
615 } else {
616 inner_main
617 };
618 let block_size = main_cross_vec(direction, block_main, inner_size[cross_axis]);
619 let aligned_rect = align2.align_size_within_rect(block_size, inner_rect);
620
621 if matches!(direction, Direction::RightToLeft | Direction::BottomUp) {
624 sized_atoms.reverse();
625 }
626
627 let mut cursor = aligned_rect.min.to_vec2()[main_axis];
629
630 let mut response = AtomLayoutResponse::empty(response);
631
632 for sized in sized_atoms {
633 let size = sized.size;
634 let growth = if sized.is_grow() { grow_main } else { 0.0 };
637
638 let atom_main = size[main_axis] + growth;
639
640 let cell = main_cross_rect(direction, aligned_rect, cursor, cursor + atom_main);
642 cursor += atom_main + gap;
643 let item_rect = sized.align.align_size_within_rect(size, cell);
644
645 if let Some(id) = sized.id {
646 debug_assert!(
647 !response.custom_rects.iter().any(|(i, _)| *i == id),
648 "Duplicate custom id"
649 );
650 response.custom_rects.push((id, item_rect));
651 }
652
653 match sized.kind {
654 SizedAtomKind::Text(galley) => {
655 if selectable {
656 LabelSelectionState::label_text_selection(
660 ui,
661 &response.response,
662 item_rect.min,
663 galley,
664 fallback_text_color,
665 Stroke::NONE,
666 );
667 } else {
668 ui.painter()
669 .galley(item_rect.min, galley, fallback_text_color);
670 }
671 }
672 SizedAtomKind::Image { image, size: _ } => {
673 image.paint_at(ui, item_rect);
674 }
675 SizedAtomKind::Empty { .. } => {}
676 SizedAtomKind::Layout(layout) => {
677 let layout_response = ui.interact(cell, layout.id, layout.sense);
680 layout.paint_at(ui, cell, layout_response);
681 }
682 }
683 }
684
685 response
686 }
687}
688
689impl AllocatedAtomLayout<'_> {
690 pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
692 let rect = self.response.rect;
693 self.sized.paint_at(ui, rect, self.response)
694 }
695}
696
697#[derive(Clone, Debug)]
701pub struct AtomLayoutResponse {
702 pub response: Response,
703 custom_rects: SmallVec<[(Id, Rect); 1]>,
705}
706
707impl AtomLayoutResponse {
708 pub fn empty(response: Response) -> Self {
709 Self {
710 response,
711 custom_rects: Default::default(),
712 }
713 }
714
715 pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
716 self.custom_rects.iter().copied()
717 }
718
719 pub fn rect(&self, id: Id) -> Option<Rect> {
723 self.custom_rects
724 .iter()
725 .find_map(|(i, r)| if *i == id { Some(*r) } else { None })
726 }
727}
728
729impl Deref for AtomLayoutResponse {
730 type Target = Response;
731
732 fn deref(&self) -> &Self::Target {
733 &self.response
734 }
735}
736
737impl DerefMut for AtomLayoutResponse {
738 fn deref_mut(&mut self) -> &mut Self::Target {
739 &mut self.response
740 }
741}
742
743impl Widget for AtomLayout<'_> {
744 fn ui(self, ui: &mut Ui) -> Response {
745 self.show(ui).response
746 }
747}
748
749impl<'a> Deref for AtomLayout<'a> {
750 type Target = Atoms<'a>;
751
752 fn deref(&self) -> &Self::Target {
753 &self.atoms
754 }
755}
756
757impl DerefMut for AtomLayout<'_> {
758 fn deref_mut(&mut self) -> &mut Self::Target {
759 &mut self.atoms
760 }
761}
762
763impl<'a> Deref for SizedAtomLayout<'a> {
764 type Target = [SizedAtom<'a>];
765
766 fn deref(&self) -> &Self::Target {
767 &self.sized_atoms
768 }
769}
770
771impl DerefMut for SizedAtomLayout<'_> {
772 fn deref_mut(&mut self) -> &mut Self::Target {
773 &mut self.sized_atoms
774 }
775}
776
777impl<'a> Deref for AllocatedAtomLayout<'a> {
778 type Target = SizedAtomLayout<'a>;
779
780 fn deref(&self) -> &Self::Target {
781 &self.sized
782 }
783}
784
785impl DerefMut for AllocatedAtomLayout<'_> {
786 fn deref_mut(&mut self) -> &mut Self::Target {
787 &mut self.sized
788 }
789}