1use crate::{
6 emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
7 Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
8};
9
10#[derive(Clone, Copy, Debug)]
15#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
16pub struct AreaState {
17 pub pivot_pos: Option<Pos2>,
19
20 pub pivot: Align2,
22
23 #[cfg_attr(feature = "serde", serde(skip))]
30 pub size: Option<Vec2>,
31
32 pub interactable: bool,
34
35 #[cfg_attr(feature = "serde", serde(skip))]
39 pub last_became_visible_at: Option<f64>,
40}
41
42impl Default for AreaState {
43 fn default() -> Self {
44 Self {
45 pivot_pos: None,
46 pivot: Align2::LEFT_TOP,
47 size: None,
48 interactable: true,
49 last_became_visible_at: None,
50 }
51 }
52}
53
54impl AreaState {
55 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
57 ctx.memory(|mem| mem.areas().get(id).copied())
59 }
60
61 pub fn left_top_pos(&self) -> Pos2 {
63 let pivot_pos = self.pivot_pos.unwrap_or_default();
64 let size = self.size.unwrap_or_default();
65 pos2(
66 pivot_pos.x - self.pivot.x().to_factor() * size.x,
67 pivot_pos.y - self.pivot.y().to_factor() * size.y,
68 )
69 }
70
71 pub fn set_left_top_pos(&mut self, pos: Pos2) {
73 let size = self.size.unwrap_or_default();
74 self.pivot_pos = Some(pos2(
75 pos.x + self.pivot.x().to_factor() * size.x,
76 pos.y + self.pivot.y().to_factor() * size.y,
77 ));
78 }
79
80 pub fn rect(&self) -> Rect {
82 let size = self.size.unwrap_or_default();
83 Rect::from_min_size(self.left_top_pos(), size)
84 }
85}
86
87#[must_use = "You should call .show()"]
103#[derive(Clone, Copy, Debug)]
104pub struct Area {
105 pub(crate) id: Id,
106 kind: UiKind,
107 sense: Option<Sense>,
108 movable: bool,
109 interactable: bool,
110 enabled: bool,
111 constrain: bool,
112 constrain_rect: Option<Rect>,
113 order: Order,
114 default_pos: Option<Pos2>,
115 default_size: Vec2,
116 pivot: Align2,
117 anchor: Option<(Align2, Vec2)>,
118 new_pos: Option<Pos2>,
119 fade_in: bool,
120}
121
122impl WidgetWithState for Area {
123 type State = AreaState;
124}
125
126impl Area {
127 pub fn new(id: Id) -> Self {
129 Self {
130 id,
131 kind: UiKind::GenericArea,
132 sense: None,
133 movable: true,
134 interactable: true,
135 constrain: true,
136 constrain_rect: None,
137 enabled: true,
138 order: Order::Middle,
139 default_pos: None,
140 default_size: Vec2::NAN,
141 new_pos: None,
142 pivot: Align2::LEFT_TOP,
143 anchor: None,
144 fade_in: true,
145 }
146 }
147
148 #[inline]
152 pub fn id(mut self, id: Id) -> Self {
153 self.id = id;
154 self
155 }
156
157 #[inline]
161 pub fn kind(mut self, kind: UiKind) -> Self {
162 self.kind = kind;
163 self
164 }
165
166 pub fn layer(&self) -> LayerId {
167 LayerId::new(self.order, self.id)
168 }
169
170 #[inline]
175 pub fn enabled(mut self, enabled: bool) -> Self {
176 self.enabled = enabled;
177 self
178 }
179
180 #[inline]
182 pub fn movable(mut self, movable: bool) -> Self {
183 self.movable = movable;
184 self.interactable |= movable;
185 self
186 }
187
188 pub fn is_enabled(&self) -> bool {
189 self.enabled
190 }
191
192 pub fn is_movable(&self) -> bool {
193 self.movable && self.enabled
194 }
195
196 #[inline]
202 pub fn interactable(mut self, interactable: bool) -> Self {
203 self.interactable = interactable;
204 self.movable &= interactable;
205 self
206 }
207
208 #[inline]
212 pub fn sense(mut self, sense: Sense) -> Self {
213 self.sense = Some(sense);
214 self
215 }
216
217 #[inline]
219 pub fn order(mut self, order: Order) -> Self {
220 self.order = order;
221 self
222 }
223
224 #[inline]
225 pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
226 self.default_pos = Some(default_pos.into());
227 self
228 }
229
230 #[inline]
240 pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
241 self.default_size = default_size.into();
242 self
243 }
244
245 #[inline]
247 pub fn default_width(mut self, default_width: f32) -> Self {
248 self.default_size.x = default_width;
249 self
250 }
251
252 #[inline]
254 pub fn default_height(mut self, default_height: f32) -> Self {
255 self.default_size.y = default_height;
256 self
257 }
258
259 #[inline]
261 pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
262 self.new_pos = Some(fixed_pos.into());
263 self.movable = false;
264 self
265 }
266
267 #[inline]
271 pub fn constrain(mut self, constrain: bool) -> Self {
272 self.constrain = constrain;
273 self
274 }
275
276 #[inline]
280 pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
281 self.constrain = true;
282 self.constrain_rect = Some(constrain_rect);
283 self
284 }
285
286 #[inline]
294 pub fn pivot(mut self, pivot: Align2) -> Self {
295 self.pivot = pivot;
296 self
297 }
298
299 #[inline]
301 pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
302 self.new_pos = Some(current_pos.into());
303 self
304 }
305
306 #[inline]
318 pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
319 self.anchor = Some((align, offset.into()));
320 self.movable(false)
321 }
322
323 pub(crate) fn get_pivot(&self) -> Align2 {
324 if let Some((pivot, _)) = self.anchor {
325 pivot
326 } else {
327 Align2::LEFT_TOP
328 }
329 }
330
331 #[inline]
335 pub fn fade_in(mut self, fade_in: bool) -> Self {
336 self.fade_in = fade_in;
337 self
338 }
339}
340
341pub(crate) struct Prepared {
342 kind: UiKind,
343 layer_id: LayerId,
344 state: AreaState,
345 move_response: Response,
346 enabled: bool,
347 constrain: bool,
348 constrain_rect: Rect,
349
350 sizing_pass: bool,
356
357 fade_in: bool,
358}
359
360impl Area {
361 pub fn show<R>(
362 self,
363 ctx: &Context,
364 add_contents: impl FnOnce(&mut Ui) -> R,
365 ) -> InnerResponse<R> {
366 let prepared = self.begin(ctx);
367 let mut content_ui = prepared.content_ui(ctx);
368 let inner = add_contents(&mut content_ui);
369 let response = prepared.end(ctx, content_ui);
370 InnerResponse { inner, response }
371 }
372
373 pub(crate) fn begin(self, ctx: &Context) -> Prepared {
374 let Self {
375 id,
376 kind,
377 sense,
378 movable,
379 order,
380 interactable,
381 enabled,
382 default_pos,
383 default_size,
384 new_pos,
385 pivot,
386 anchor,
387 constrain,
388 constrain_rect,
389 fade_in,
390 } = self;
391
392 let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
393
394 let layer_id = LayerId::new(order, id);
395
396 let state = AreaState::load(ctx, id);
397 let mut sizing_pass = state.is_none();
398 let mut state = state.unwrap_or(AreaState {
399 pivot_pos: None,
400 pivot,
401 size: None,
402 interactable,
403 last_became_visible_at: None,
404 });
405 state.pivot = pivot;
406 state.interactable = interactable;
407 if let Some(new_pos) = new_pos {
408 state.pivot_pos = Some(new_pos);
409 }
410 state.pivot_pos.get_or_insert_with(|| {
411 default_pos.unwrap_or_else(|| automatic_area_position(ctx, layer_id))
412 });
413 state.interactable = interactable;
414
415 let size = *state.size.get_or_insert_with(|| {
416 sizing_pass = true;
417
418 let mut size = default_size;
420
421 let default_area_size = ctx.style().spacing.default_area_size;
422 if size.x.is_nan() {
423 size.x = default_area_size.x;
424 }
425 if size.y.is_nan() {
426 size.y = default_area_size.y;
427 }
428
429 if constrain {
430 size = size.at_most(constrain_rect.size());
431 }
432
433 size
434 });
435
436 let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
438
439 if !visible_last_frame || state.last_became_visible_at.is_none() {
440 state.last_became_visible_at = Some(ctx.input(|i| i.time));
441 }
442
443 if let Some((anchor, offset)) = anchor {
444 state.set_left_top_pos(
445 anchor
446 .align_size_within_rect(size, constrain_rect)
447 .left_top()
448 + offset,
449 );
450 }
451
452 let mut move_response = {
454 let interact_id = layer_id.id.with("move");
455 let sense = sense.unwrap_or_else(|| {
456 if movable {
457 Sense::drag()
458 } else if interactable {
459 Sense::click() } else {
461 Sense::hover()
462 }
463 });
464
465 let move_response = ctx.create_widget(
466 WidgetRect {
467 id: interact_id,
468 layer_id,
469 rect: state.rect(),
470 interact_rect: state.rect().intersect(constrain_rect),
471 sense,
472 enabled,
473 },
474 true,
475 );
476
477 if movable && move_response.dragged() {
478 if let Some(pivot_pos) = &mut state.pivot_pos {
479 *pivot_pos += move_response.drag_delta();
480 }
481 }
482
483 if (move_response.dragged() || move_response.clicked())
484 || pointer_pressed_on_area(ctx, layer_id)
485 || !ctx.memory(|m| m.areas().visible_last_frame(&layer_id))
486 {
487 ctx.memory_mut(|m| m.areas_mut().move_to_top(layer_id));
488 ctx.request_repaint();
489 }
490
491 move_response
492 };
493
494 if constrain {
495 state.set_left_top_pos(
496 ctx.constrain_window_rect_to_area(state.rect(), constrain_rect)
497 .min,
498 );
499 }
500
501 state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
502
503 move_response.rect = state.rect();
505 move_response.interact_rect = state.rect();
506
507 Prepared {
508 kind,
509 layer_id,
510 state,
511 move_response,
512 enabled,
513 constrain,
514 constrain_rect,
515 sizing_pass,
516 fade_in,
517 }
518 }
519}
520
521impl Prepared {
522 pub(crate) fn state(&self) -> &AreaState {
523 &self.state
524 }
525
526 pub(crate) fn state_mut(&mut self) -> &mut AreaState {
527 &mut self.state
528 }
529
530 pub(crate) fn constrain(&self) -> bool {
531 self.constrain
532 }
533
534 pub(crate) fn constrain_rect(&self) -> Rect {
535 self.constrain_rect
536 }
537
538 pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
539 let max_rect = self.state.rect();
540
541 let mut ui_builder = UiBuilder::new()
542 .ui_stack_info(UiStackInfo::new(self.kind))
543 .layer_id(self.layer_id)
544 .max_rect(max_rect);
545
546 if !self.enabled {
547 ui_builder = ui_builder.disabled();
548 }
549 if self.sizing_pass {
550 ui_builder = ui_builder.sizing_pass().invisible();
551 }
552
553 let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
554 ui.set_clip_rect(self.constrain_rect); if self.fade_in {
557 if let Some(last_became_visible_at) = self.state.last_became_visible_at {
558 let age =
559 ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
560 let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
561 let opacity = emath::easing::quadratic_out(opacity); ui.multiply_opacity(opacity);
563 if opacity < 1.0 {
564 ctx.request_repaint();
565 }
566 }
567 }
568
569 ui
570 }
571
572 pub(crate) fn with_widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
573 self.move_response.widget_info(make_info);
574 }
575
576 pub(crate) fn id(&self) -> Id {
577 self.move_response.id
578 }
579
580 #[allow(clippy::needless_pass_by_value)] pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
582 let Self {
583 kind: _,
584 layer_id,
585 mut state,
586 move_response: mut response,
587 sizing_pass,
588 ..
589 } = self;
590
591 state.size = Some(content_ui.min_size());
592
593 let final_rect = state.rect();
596 response.rect = final_rect;
597 response.interact_rect = final_rect;
598
599 ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
600
601 if sizing_pass {
602 ctx.request_repaint();
604 }
605
606 response
607 }
608}
609
610fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
611 if let Some(pointer_pos) = ctx.pointer_interact_pos() {
612 let any_pressed = ctx.input(|i| i.pointer.any_pressed());
613 any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
614 } else {
615 false
616 }
617}
618
619fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
620 let mut existing: Vec<Rect> = ctx.memory(|mem| {
621 mem.areas()
622 .visible_windows()
623 .filter(|(id, _)| id != &layer_id) .filter(|(_, state)| state.pivot_pos.is_some() && state.size.is_some())
625 .map(|(_, state)| state.rect())
626 .collect()
627 });
628 existing.sort_by_key(|r| r.left().round() as i32);
629
630 let available_rect = ctx.available_rect();
633
634 let spacing = 16.0;
635 let left = available_rect.left() + spacing;
636 let top = available_rect.top() + spacing;
637
638 if existing.is_empty() {
639 return pos2(left, top);
640 }
641
642 let mut column_bbs = vec![existing[0]];
644
645 for &rect in &existing {
646 let current_column_bb = column_bbs.last_mut().unwrap();
647 if rect.left() < current_column_bb.right() {
648 *current_column_bb = current_column_bb.union(rect);
650 } else {
651 column_bbs.push(rect);
653 }
654 }
655
656 {
657 let mut x = left;
659 for col_bb in &column_bbs {
660 let available = col_bb.left() - x;
661 if available >= 300.0 {
662 return pos2(x, top);
663 }
664 x = col_bb.right() + spacing;
665 }
666 }
667
668 for col_bb in &column_bbs {
670 if col_bb.bottom() < available_rect.center().y {
671 return pos2(col_bb.left(), col_bb.bottom() + spacing);
672 }
673 }
674
675 let rightmost = column_bbs.last().unwrap().right();
677 if rightmost + 200.0 < available_rect.right() {
678 return pos2(rightmost + spacing, top);
679 }
680
681 let mut best_pos = pos2(left, column_bbs[0].bottom() + spacing);
683 for col_bb in &column_bbs {
684 let col_pos = pos2(col_bb.left(), col_bb.bottom() + spacing);
685 if col_pos.y < best_pos.y {
686 best_pos = col_pos;
687 }
688 }
689 best_pos
690}