1use emath::GuiRounding as _;
6
7use crate::{
8 Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
9 Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
10};
11
12#[derive(Clone, Copy, Debug)]
17#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
18pub struct AreaState {
19 pub pivot_pos: Option<Pos2>,
21
22 pub pivot: Align2,
24
25 #[cfg_attr(feature = "serde", serde(skip))]
32 pub size: Option<Vec2>,
33
34 pub interactable: bool,
36
37 #[cfg_attr(feature = "serde", serde(skip))]
41 pub last_became_visible_at: Option<f64>,
42}
43
44impl Default for AreaState {
45 fn default() -> Self {
46 Self {
47 pivot_pos: None,
48 pivot: Align2::LEFT_TOP,
49 size: None,
50 interactable: true,
51 last_became_visible_at: None,
52 }
53 }
54}
55
56impl AreaState {
57 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
59 ctx.memory(|mem| mem.areas().get(id).copied())
61 }
62
63 pub fn left_top_pos(&self) -> Pos2 {
65 let pivot_pos = self.pivot_pos.unwrap_or_default();
66 let size = self.size.unwrap_or_default();
67 pos2(
68 pivot_pos.x - self.pivot.x().to_factor() * size.x,
69 pivot_pos.y - self.pivot.y().to_factor() * size.y,
70 )
71 .round_ui()
72 }
73
74 pub fn set_left_top_pos(&mut self, pos: Pos2) {
76 let size = self.size.unwrap_or_default();
77 self.pivot_pos = Some(pos2(
78 pos.x + self.pivot.x().to_factor() * size.x,
79 pos.y + self.pivot.y().to_factor() * size.y,
80 ));
81 }
82
83 pub fn rect(&self) -> Rect {
85 let size = self.size.unwrap_or_default();
86 Rect::from_min_size(self.left_top_pos(), size).round_ui()
87 }
88}
89
90#[must_use = "You should call .show()"]
106#[derive(Clone, Debug)]
107pub struct Area {
108 pub(crate) id: Id,
109 info: UiStackInfo,
110 sense: Option<Sense>,
111 movable: bool,
112 interactable: bool,
113 enabled: bool,
114 constrain: bool,
115 constrain_rect: Option<Rect>,
116 order: Order,
117 default_pos: Option<Pos2>,
118 default_size: Vec2,
119 pivot: Align2,
120 anchor: Option<(Align2, Vec2)>,
121 new_pos: Option<Pos2>,
122 fade_in: bool,
123 layout: Layout,
124 sizing_pass: bool,
125}
126
127impl WidgetWithState for Area {
128 type State = AreaState;
129}
130
131impl Area {
132 pub fn new(id: Id) -> Self {
134 Self {
135 id,
136 info: UiStackInfo::new(UiKind::GenericArea),
137 sense: None,
138 movable: true,
139 interactable: true,
140 constrain: true,
141 constrain_rect: None,
142 enabled: true,
143 order: Order::Middle,
144 default_pos: None,
145 default_size: Vec2::NAN,
146 new_pos: None,
147 pivot: Align2::LEFT_TOP,
148 anchor: None,
149 fade_in: true,
150 layout: Layout::default(),
151 sizing_pass: false,
152 }
153 }
154
155 #[inline]
159 pub fn id(mut self, id: Id) -> Self {
160 self.id = id;
161 self
162 }
163
164 #[inline]
168 pub fn kind(mut self, kind: UiKind) -> Self {
169 self.info = UiStackInfo::new(kind);
170 self
171 }
172
173 #[inline]
177 pub fn info(mut self, info: UiStackInfo) -> Self {
178 self.info = info;
179 self
180 }
181
182 pub fn layer(&self) -> LayerId {
183 LayerId::new(self.order, self.id)
184 }
185
186 #[inline]
191 pub fn enabled(mut self, enabled: bool) -> Self {
192 self.enabled = enabled;
193 self
194 }
195
196 #[inline]
198 pub fn movable(mut self, movable: bool) -> Self {
199 self.movable = movable;
200 self.interactable |= movable;
201 self
202 }
203
204 pub fn is_enabled(&self) -> bool {
205 self.enabled
206 }
207
208 pub fn is_movable(&self) -> bool {
209 self.movable && self.enabled
210 }
211
212 #[inline]
218 pub fn interactable(mut self, interactable: bool) -> Self {
219 self.interactable = interactable;
220 self.movable &= interactable;
221 self
222 }
223
224 #[inline]
228 pub fn sense(mut self, sense: Sense) -> Self {
229 self.sense = Some(sense);
230 self
231 }
232
233 #[inline]
235 pub fn order(mut self, order: Order) -> Self {
236 self.order = order;
237 self
238 }
239
240 #[inline]
241 pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
242 self.default_pos = Some(default_pos.into());
243 self
244 }
245
246 #[inline]
256 pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
257 self.default_size = default_size.into();
258 self
259 }
260
261 #[inline]
263 pub fn default_width(mut self, default_width: f32) -> Self {
264 self.default_size.x = default_width;
265 self
266 }
267
268 #[inline]
270 pub fn default_height(mut self, default_height: f32) -> Self {
271 self.default_size.y = default_height;
272 self
273 }
274
275 #[inline]
277 pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
278 self.new_pos = Some(fixed_pos.into());
279 self.movable = false;
280 self
281 }
282
283 #[inline]
287 pub fn constrain(mut self, constrain: bool) -> Self {
288 self.constrain = constrain;
289 self
290 }
291
292 #[inline]
296 pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
297 self.constrain = true;
298 self.constrain_rect = Some(constrain_rect);
299 self
300 }
301
302 #[inline]
310 pub fn pivot(mut self, pivot: Align2) -> Self {
311 self.pivot = pivot;
312 self
313 }
314
315 #[inline]
317 pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
318 self.new_pos = Some(current_pos.into());
319 self
320 }
321
322 #[inline]
334 pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
335 self.anchor = Some((align, offset.into()));
336 self.movable(false)
337 }
338
339 pub(crate) fn get_pivot(&self) -> Align2 {
340 if let Some((pivot, _)) = self.anchor {
341 pivot
342 } else {
343 Align2::LEFT_TOP
344 }
345 }
346
347 #[inline]
351 pub fn fade_in(mut self, fade_in: bool) -> Self {
352 self.fade_in = fade_in;
353 self
354 }
355
356 #[inline]
358 pub fn layout(mut self, layout: Layout) -> Self {
359 self.layout = layout;
360 self
361 }
362
363 #[inline]
379 pub fn sizing_pass(mut self, resize: bool) -> Self {
380 self.sizing_pass = resize;
381 self
382 }
383}
384
385pub(crate) struct Prepared {
386 info: Option<UiStackInfo>,
387 layer_id: LayerId,
388 state: AreaState,
389 move_response: Response,
390 enabled: bool,
391 constrain: bool,
392 constrain_rect: Rect,
393
394 sizing_pass: bool,
400
401 fade_in: bool,
402 layout: Layout,
403}
404
405impl Area {
406 pub fn show<R>(
407 self,
408 ctx: &Context,
409 add_contents: impl FnOnce(&mut Ui) -> R,
410 ) -> InnerResponse<R> {
411 let mut prepared = self.begin(ctx);
412 let mut content_ui = prepared.content_ui(ctx);
413 let inner = add_contents(&mut content_ui);
414 let response = prepared.end(ctx, content_ui);
415 InnerResponse { inner, response }
416 }
417
418 pub(crate) fn begin(self, ctx: &Context) -> Prepared {
419 let Self {
420 id,
421 info,
422 sense,
423 movable,
424 order,
425 interactable,
426 enabled,
427 default_pos,
428 default_size,
429 new_pos,
430 pivot,
431 anchor,
432 constrain,
433 constrain_rect,
434 fade_in,
435 layout,
436 sizing_pass: force_sizing_pass,
437 } = self;
438
439 let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
440
441 let layer_id = LayerId::new(order, id);
442
443 let state = AreaState::load(ctx, id);
444 let mut sizing_pass = state.is_none();
445 let mut state = state.unwrap_or(AreaState {
446 pivot_pos: None,
447 pivot,
448 size: None,
449 interactable,
450 last_became_visible_at: None,
451 });
452 if force_sizing_pass {
453 sizing_pass = true;
454 state.size = None;
455 }
456 state.pivot = pivot;
457 state.interactable = interactable;
458 if let Some(new_pos) = new_pos {
459 state.pivot_pos = Some(new_pos);
460 }
461 state.pivot_pos.get_or_insert_with(|| {
462 default_pos.unwrap_or_else(|| automatic_area_position(ctx, layer_id))
463 });
464 state.interactable = interactable;
465
466 let size = *state.size.get_or_insert_with(|| {
467 sizing_pass = true;
468
469 let mut size = default_size;
471
472 let default_area_size = ctx.style().spacing.default_area_size;
473 if size.x.is_nan() {
474 size.x = default_area_size.x;
475 }
476 if size.y.is_nan() {
477 size.y = default_area_size.y;
478 }
479
480 if constrain {
481 size = size.at_most(constrain_rect.size());
482 }
483
484 size
485 });
486
487 let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
489
490 if !visible_last_frame || state.last_became_visible_at.is_none() {
491 state.last_became_visible_at = Some(ctx.input(|i| i.time));
492 }
493
494 if let Some((anchor, offset)) = anchor {
495 state.set_left_top_pos(
496 anchor
497 .align_size_within_rect(size, constrain_rect)
498 .left_top()
499 + offset,
500 );
501 }
502
503 let mut move_response = {
505 let interact_id = layer_id.id.with("move");
506 let sense = sense.unwrap_or_else(|| {
507 if movable {
508 Sense::drag()
509 } else if interactable {
510 Sense::click() } else {
512 Sense::hover()
513 }
514 });
515
516 let move_response = ctx.create_widget(
517 WidgetRect {
518 id: interact_id,
519 layer_id,
520 rect: state.rect(),
521 interact_rect: state.rect().intersect(constrain_rect),
522 sense,
523 enabled,
524 },
525 true,
526 );
527
528 if movable && move_response.dragged() {
529 if let Some(pivot_pos) = &mut state.pivot_pos {
530 *pivot_pos += move_response.drag_delta();
531 }
532 }
533
534 if (move_response.dragged() || move_response.clicked())
535 || pointer_pressed_on_area(ctx, layer_id)
536 || !ctx.memory(|m| m.areas().visible_last_frame(&layer_id))
537 {
538 ctx.memory_mut(|m| m.areas_mut().move_to_top(layer_id));
539 ctx.request_repaint();
540 }
541
542 move_response
543 };
544
545 if constrain {
546 state.set_left_top_pos(
547 Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
548 );
549 }
550
551 state.set_left_top_pos(state.left_top_pos());
552
553 move_response.rect = state.rect();
555 move_response.interact_rect = state.rect();
556
557 Prepared {
558 info: Some(info),
559 layer_id,
560 state,
561 move_response,
562 enabled,
563 constrain,
564 constrain_rect,
565 sizing_pass,
566 fade_in,
567 layout,
568 }
569 }
570}
571
572impl Prepared {
573 pub(crate) fn state(&self) -> &AreaState {
574 &self.state
575 }
576
577 pub(crate) fn state_mut(&mut self) -> &mut AreaState {
578 &mut self.state
579 }
580
581 pub(crate) fn constrain(&self) -> bool {
582 self.constrain
583 }
584
585 pub(crate) fn constrain_rect(&self) -> Rect {
586 self.constrain_rect
587 }
588
589 pub(crate) fn content_ui(&mut self, ctx: &Context) -> Ui {
590 let max_rect = self.state.rect();
591
592 let mut ui_builder = UiBuilder::new()
593 .ui_stack_info(self.info.take().unwrap_or_default())
594 .layer_id(self.layer_id)
595 .max_rect(max_rect)
596 .layout(self.layout)
597 .closable();
598
599 if !self.enabled {
600 ui_builder = ui_builder.disabled();
601 }
602 if self.sizing_pass {
603 ui_builder = ui_builder.sizing_pass().invisible();
604 }
605
606 let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
607 ui.set_clip_rect(self.constrain_rect); if self.fade_in {
610 if let Some(last_became_visible_at) = self.state.last_became_visible_at {
611 let age =
612 ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
613 let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
614 let opacity = emath::easing::quadratic_out(opacity); ui.multiply_opacity(opacity);
616 if opacity < 1.0 {
617 ctx.request_repaint();
618 }
619 }
620 }
621
622 ui
623 }
624
625 pub(crate) fn with_widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
626 self.move_response.widget_info(make_info);
627 }
628
629 pub(crate) fn id(&self) -> Id {
630 self.move_response.id
631 }
632
633 #[expect(clippy::needless_pass_by_value)] pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
635 let Self {
636 info: _,
637 layer_id,
638 mut state,
639 move_response: mut response,
640 sizing_pass,
641 ..
642 } = self;
643
644 state.size = Some(content_ui.min_size());
645
646 let final_rect = state.rect();
649 response.rect = final_rect;
650 response.interact_rect = final_rect;
651
652 if content_ui.should_close() {
655 response.set_close();
656 }
657
658 ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
659
660 if sizing_pass {
661 ctx.request_repaint();
663 }
664
665 response
666 }
667}
668
669fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
670 if let Some(pointer_pos) = ctx.pointer_interact_pos() {
671 let any_pressed = ctx.input(|i| i.pointer.any_pressed());
672 any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
673 } else {
674 false
675 }
676}
677
678fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
679 let mut existing: Vec<Rect> = ctx.memory(|mem| {
680 mem.areas()
681 .visible_windows()
682 .filter(|(id, _)| id != &layer_id) .filter(|(_, state)| state.pivot_pos.is_some() && state.size.is_some())
684 .map(|(_, state)| state.rect())
685 .collect()
686 });
687 existing.sort_by_key(|r| r.left().round() as i32);
688
689 let available_rect = ctx.available_rect();
692
693 let spacing = 16.0;
694 let left = available_rect.left() + spacing;
695 let top = available_rect.top() + spacing;
696
697 if existing.is_empty() {
698 return pos2(left, top);
699 }
700
701 let mut column_bbs = vec![existing[0]];
703
704 for &rect in &existing {
705 let current_column_bb = column_bbs.last_mut().unwrap();
706 if rect.left() < current_column_bb.right() {
707 *current_column_bb |= rect;
709 } else {
710 column_bbs.push(rect);
712 }
713 }
714
715 {
716 let mut x = left;
718 for col_bb in &column_bbs {
719 let available = col_bb.left() - x;
720 if available >= 300.0 {
721 return pos2(x, top);
722 }
723 x = col_bb.right() + spacing;
724 }
725 }
726
727 for col_bb in &column_bbs {
729 if col_bb.bottom() < available_rect.center().y {
730 return pos2(col_bb.left(), col_bb.bottom() + spacing);
731 }
732 }
733
734 let rightmost = column_bbs.last().unwrap().right();
736 if rightmost + 200.0 < available_rect.right() {
737 return pos2(rightmost + spacing, top);
738 }
739
740 let mut best_pos = pos2(left, column_bbs[0].bottom() + spacing);
742 for col_bb in &column_bbs {
743 let col_pos = pos2(col_bb.left(), col_bb.bottom() + spacing);
744 if col_pos.y < best_pos.y {
745 best_pos = col_pos;
746 }
747 }
748 best_pos
749}