1use crate::theme::{BLACK, DARK_GRAY, LIGHT_GRAY, WHITE};
14use egui::{
15 pos2, vec2, Align2, Color32, Context, FontFamily, FontId, Frame, Id, InnerResponse, LayerId,
16 Order, Painter, Pos2, Rect, Sense, Stroke, Ui, UiBuilder, Vec2,
17};
18
19const BORDER: f32 = 1.0;
21const TITLE_HEIGHT: f32 = 23.0;
22const CLOSE_SIZE: f32 = 15.0;
23const CLOSE_INSET: f32 = 4.0;
24const RESIZE_HEIGHT: f32 = 9.0;
25const MIN_SIZE: Vec2 = vec2(100.0, 60.0);
26const DEFAULT_RECT: Rect = Rect {
27 min: pos2(32.0, 32.0),
28 max: pos2(352.0, 272.0),
29};
30
31pub fn title_colors(key: bool) -> (Color32, Color32) {
34 if key {
35 (BLACK, WHITE)
36 } else {
37 (DARK_GRAY, LIGHT_GRAY)
38 }
39}
40
41pub fn is_key(ctx: &Context, id: Id) -> bool {
44 ctx.data(|d| d.get_temp::<Id>(key_slot())) == Some(id)
45}
46
47pub fn make_key(ctx: &Context, id: Id) {
49 ctx.data_mut(|d| d.insert_temp(key_slot(), id));
50}
51
52fn key_slot() -> Id {
54 Id::new("nxwin::key")
55}
56
57pub fn stored_rect(ctx: &Context, id: Id) -> Option<Rect> {
61 ctx.data(|d| d.get_temp::<Rect>(rect_slot(id)))
62}
63
64fn rect_slot(id: Id) -> Id {
66 id.with("nxwin_rect")
67}
68
69#[must_use = "call .show(ctx, ...)"]
72pub struct NxWindow<'open> {
73 id: Id,
74 title: String,
75 open: Option<&'open mut bool>,
76 default_rect: Rect,
77 rect: Option<Rect>,
78 min_size: Vec2,
79 bounds: Option<Rect>,
80 resizable: bool,
81}
82
83impl<'open> NxWindow<'open> {
84 pub fn new(id_source: impl std::hash::Hash, title: impl Into<String>) -> Self {
87 Self {
88 id: Id::new(("nxwin", id_source)),
89 title: title.into(),
90 open: None,
91 default_rect: DEFAULT_RECT,
92 rect: None,
93 min_size: MIN_SIZE,
94 bounds: None,
95 resizable: true,
96 }
97 }
98
99 pub fn id(&self) -> Id {
101 self.id
102 }
103
104 pub fn closable(mut self, open: &'open mut bool) -> Self {
107 self.open = Some(open);
108 self
109 }
110
111 #[cfg_attr(
113 not(test),
114 expect(dead_code, reason = "the app's windows all own their rect (.rect)")
115 )]
116 pub fn default_rect(mut self, rect: Rect) -> Self {
117 self.default_rect = rect;
118 self
119 }
120
121 pub fn rect(mut self, rect: Rect) -> Self {
125 self.rect = Some(rect);
126 self
127 }
128
129 pub fn min_size(mut self, min_size: Vec2) -> Self {
132 self.min_size = min_size;
133 self
134 }
135
136 pub fn bounds(mut self, bounds: Rect) -> Self {
139 self.bounds = Some(bounds);
140 self
141 }
142
143 pub fn resizable(mut self, resizable: bool) -> Self {
145 self.resizable = resizable;
146 self
147 }
148
149 pub fn show<R>(
152 self,
153 ctx: &Context,
154 add_contents: impl FnOnce(&mut Ui) -> R,
155 ) -> Option<InnerResponse<R>> {
156 let Self {
157 id,
158 title,
159 open,
160 default_rect,
161 rect,
162 min_size,
163 bounds,
164 resizable,
165 } = self;
166 if matches!(open, Some(false)) {
167 return None;
168 }
169 let closable = open.is_some();
170 let mut rect = rect
171 .or_else(|| stored_rect(ctx, id))
172 .unwrap_or(default_rect);
173 rect.max = rect.min + rect.size().max(min_size);
174
175 if let Some(r) = ctx.read_response(id.with("title")) {
178 rect = rect.translate(r.drag_delta());
179 }
180 let screen = bounds.unwrap_or_else(|| ctx.screen_rect());
181 if let Some(r) = ctx
182 .read_response(id.with("nx_resize"))
183 .filter(|r| r.dragged())
184 {
185 let zone_id = id.with("resize_zone");
189 let zone = if r.drag_started() {
190 let x = r
191 .interact_pointer_pos()
192 .map_or(0.0, |p| p.x - r.rect.left());
193 let zone = if x * 3.0 < r.rect.width() {
194 0
195 } else if x * 3.0 < r.rect.width() * 2.0 {
196 1
197 } else {
198 2
199 };
200 ctx.data_mut(|d| d.insert_temp(zone_id, zone));
201 zone
202 } else {
203 ctx.data(|d| d.get_temp::<u8>(zone_id)).unwrap_or(1)
204 };
205 let d = r.drag_delta();
208 match zone {
209 0 => {
210 rect.min.x = (rect.min.x + d.x)
211 .max(screen.min.x)
212 .min(rect.max.x - min_size.x);
213 }
214 2 => {
215 rect.max.x = (rect.max.x + d.x)
216 .min(screen.max.x)
217 .max(rect.min.x + min_size.x);
218 }
219 _ => {}
220 }
221 rect.max.y = (rect.max.y + d.y)
222 .min(screen.max.y)
223 .max(rect.min.y + min_size.y);
224 }
225
226 let max = (screen.max - rect.size()).max(screen.min);
229 rect = rect.translate(rect.min.clamp(screen.min, max) - rect.min);
230 ctx.data_mut(|d| d.insert_temp(rect_slot(id), rect));
231
232 let layer = LayerId::new(Order::Middle, id);
237 let pressed_here = ctx
238 .input(|i| i.pointer.interact_pos().filter(|_| i.pointer.any_pressed()))
239 .is_some_and(|p| ctx.layer_id_at(p) == Some(layer));
240 ctx.data_mut(|d| {
241 if pressed_here || d.get_temp::<Id>(key_slot()).is_none() {
242 d.insert_temp(key_slot(), id);
243 }
244 });
245 let key = is_key(ctx, id);
246
247 let mut close_clicked = false;
248 let shown = egui::Window::new(title.as_str())
249 .id(id)
250 .title_bar(false)
251 .frame(Frame::none())
252 .movable(false)
253 .constrain(false)
254 .resizable(false)
255 .collapsible(false)
256 .fade_in(false)
257 .fade_out(false)
258 .current_pos(rect.min)
259 .fixed_size(rect.size())
260 .show(ctx, |ui| {
261 let outer = ui.max_rect();
262 let l = layout(outer, closable, resizable);
263 let p = ui.painter().clone();
264 let (fill, text) = title_colors(key);
265
266 p.rect_filled(l.body, 0.0, LIGHT_GRAY);
267 p.rect_filled(l.title, 0.0, fill);
268 let title_painter =
269 p.with_clip_rect(l.title.shrink2(vec2(CLOSE_INSET * 2.0 + CLOSE_SIZE, 0.0)));
270 title_painter.text(
271 l.title.center(),
272 Align2::CENTER_CENTER,
273 &title,
274 FontId::new(12.0, FontFamily::Name(crate::theme::BOLD.into())),
275 text,
276 );
277 let _ = ui.interact(l.title, id.with("title"), Sense::drag());
279
280 if let Some(close) = l.close {
282 bevel(&p, close);
283 let x = close.shrink(4.0);
284 let stroke = Stroke::new(1.0_f32, BLACK);
285 p.line_segment([x.left_top(), x.right_bottom()], stroke);
286 p.line_segment([x.right_top(), x.left_bottom()], stroke);
287 close_clicked = ui
288 .interact(close, id.with("close"), Sense::click())
289 .clicked();
290 }
291
292 if let Some(bar) = l.resize {
293 bevel(&p, bar);
294 for f in [1.0 / 3.0, 2.0 / 3.0] {
295 let x = (bar.left() + bar.width() * f).round();
296 p.rect_filled(
297 Rect::from_x_y_ranges(x..=x + 1.0, bar.y_range()),
298 0.0,
299 BLACK,
300 );
301 p.rect_filled(
302 Rect::from_x_y_ranges(x + 1.0..=x + 2.0, bar.y_range()),
303 0.0,
304 WHITE,
305 );
306 }
307 let _ = ui.interact(bar, id.with("nx_resize"), Sense::drag());
308 }
309
310 let mut body = ui.new_child(UiBuilder::new().max_rect(l.body));
311 body.set_clip_rect(l.body.intersect(ui.clip_rect()));
312 let inner = add_contents(&mut body);
313
314 p.rect_stroke(outer.shrink(BORDER / 2.0), 0.0, Stroke::new(BORDER, BLACK));
315 ui.advance_cursor_after_rect(outer);
317 inner
318 })?;
319
320 if let (true, Some(open)) = (close_clicked, open) {
321 *open = false;
322 if is_key(ctx, id) {
324 ctx.data_mut(|d| d.remove::<Id>(key_slot()));
325 }
326 }
327 Some(InnerResponse {
328 inner: shown.inner?,
329 response: shown.response,
330 })
331 }
332}
333
334struct Layout {
336 title: Rect,
337 close: Option<Rect>,
338 body: Rect,
339 resize: Option<Rect>,
340}
341
342fn layout(outer: Rect, closable: bool, resizable: bool) -> Layout {
343 let inner = outer.shrink(BORDER);
344 let title = Rect::from_min_size(inner.min, vec2(inner.width(), TITLE_HEIGHT));
345 let close = closable.then(|| {
346 Rect::from_min_size(
347 pos2(
348 title.right() - CLOSE_INSET - CLOSE_SIZE,
349 title.center().y - CLOSE_SIZE / 2.0,
350 ),
351 Vec2::splat(CLOSE_SIZE),
352 )
353 });
354 let resize = resizable.then(|| {
355 Rect::from_min_max(
356 pos2(inner.left(), inner.bottom() - RESIZE_HEIGHT),
357 inner.max,
358 )
359 });
360 let body = Rect::from_min_max(
361 pos2(inner.left(), title.bottom()),
362 pos2(inner.right(), resize.map_or(inner.bottom(), |r| r.top())),
363 );
364 Layout {
365 title,
366 close,
367 body,
368 resize,
369 }
370}
371
372fn bevel(p: &Painter, r: Rect) {
375 let edge = |min: Pos2, max: Pos2, c: Color32| {
376 p.rect_filled(Rect::from_min_max(min, max), 0.0, c);
377 };
378 p.rect_filled(r, 0.0, LIGHT_GRAY);
379 edge(r.min, pos2(r.max.x, r.min.y + 1.0), WHITE);
380 edge(r.min, pos2(r.min.x + 1.0, r.max.y), WHITE);
381 edge(
382 pos2(r.min.x + 1.0, r.max.y - 2.0),
383 pos2(r.max.x - 1.0, r.max.y - 1.0),
384 DARK_GRAY,
385 );
386 edge(
387 pos2(r.max.x - 2.0, r.min.y + 1.0),
388 pos2(r.max.x - 1.0, r.max.y - 1.0),
389 DARK_GRAY,
390 );
391 edge(pos2(r.min.x, r.max.y - 1.0), r.max, BLACK);
392 edge(pos2(r.max.x - 1.0, r.min.y), r.max, BLACK);
393}
394
395#[cfg(test)]
396#[cfg_attr(coverage_nightly, coverage(off))]
397mod tests {
398 use super::*;
399 use egui::epaint::ClippedShape;
400 use egui::{Event, Modifiers, PointerButton, RawInput, Shape};
401
402 const SCREEN: Rect = Rect {
403 min: Pos2::ZERO,
404 max: pos2(800.0, 600.0),
405 };
406
407 fn wid(id_source: &str) -> Id {
409 NxWindow::new(id_source, "").id()
410 }
411
412 struct Harness {
414 ctx: Context,
415 time: f64,
416 }
417
418 impl Harness {
419 fn new() -> Self {
420 let ctx = Context::default();
421 crate::theme::install_fonts(&ctx);
422 Self { ctx, time: 0.0 }
423 }
424
425 fn frame(&mut self, events: Vec<Event>, ui: impl FnMut(&Context)) -> Vec<ClippedShape> {
426 self.time += 1.0 / 60.0;
427 let raw = RawInput {
428 time: Some(self.time),
429 screen_rect: Some(SCREEN),
430 events,
431 ..Default::default()
432 };
433 self.ctx.run(raw, ui).shapes
434 }
435 }
436
437 fn button(pos: Pos2, pressed: bool) -> Event {
438 Event::PointerButton {
439 pos,
440 button: PointerButton::Primary,
441 pressed,
442 modifiers: Modifiers::NONE,
443 }
444 }
445
446 fn click(at: Pos2) -> Vec<Event> {
447 vec![Event::PointerMoved(at), button(at, true), button(at, false)]
448 }
449
450 fn drag(h: &mut Harness, from: Pos2, to: Pos2, mut ui: impl FnMut(&Context)) {
452 h.frame(vec![Event::PointerMoved(from)], &mut ui);
453 h.frame(vec![button(from, true)], &mut ui);
454 h.frame(vec![Event::PointerMoved(to)], &mut ui);
455 h.frame(vec![button(to, false)], &mut ui);
456 h.frame(Vec::new(), &mut ui);
457 }
458
459 fn fill_at(shapes: &[ClippedShape], rect: Rect) -> Option<Color32> {
461 shapes.iter().find_map(|c| match &c.shape {
462 Shape::Rect(r) if r.rect == rect => Some(r.fill),
463 _ => None,
464 })
465 }
466
467 fn text_color(shapes: &[ClippedShape], text: &str) -> Option<Color32> {
469 shapes.iter().find_map(|c| match &c.shape {
470 Shape::Text(t) if t.galley.text() == text => {
471 Some(t.galley.job.sections[0].format.color)
472 }
473 _ => None,
474 })
475 }
476
477 const R: Rect = Rect {
478 min: pos2(100.0, 100.0),
479 max: pos2(400.0, 300.0),
480 };
481
482 #[test]
484 fn title_bar_is_23px_and_body_sits_below() {
485 let mut h = Harness::new();
486 let (mut outer, mut body) = (Rect::NOTHING, Rect::NOTHING);
487 let mut shapes = Vec::new();
488 for _ in 0..3 {
489 shapes = h.frame(Vec::new(), |ctx| {
490 let ir = NxWindow::new("a", "Alpha")
491 .default_rect(R)
492 .show(ctx, |ui| ui.max_rect())
493 .unwrap();
494 (outer, body) = (ir.response.rect, ir.inner);
495 });
496 }
497 assert_eq!(outer, R);
498 let title = shapes
500 .iter()
501 .find_map(|c| match &c.shape {
502 Shape::Rect(r) if r.fill == BLACK && r.rect.min == R.min + vec2(1.0, 1.0) => {
503 Some(r.rect)
504 }
505 _ => None,
506 })
507 .expect("a black title bar inside the border");
508 assert_eq!(title.width(), R.width() - 2.0);
509 assert_eq!(title.height(), 23.0);
510 assert_eq!(body.top(), title.bottom());
511 assert_eq!((body.left(), body.right()), (title.left(), title.right()));
512 assert_eq!(
513 body.bottom(),
514 R.bottom() - 1.0 - 9.0,
515 "resize bar below the body"
516 );
517 }
518
519 #[test]
521 fn close_box_clears_open() {
522 let mut h = Harness::new();
523 let mut open = true;
524 let mut shown = false;
525 let mut ui = |ctx: &Context| {
526 shown = NxWindow::new("a", "Alpha")
527 .default_rect(R)
528 .closable(&mut open)
529 .show(ctx, |_| ())
530 .is_some();
531 };
532 for _ in 0..3 {
533 h.frame(Vec::new(), &mut ui);
534 }
535 let close = layout(R, true, true).close.unwrap();
536 assert_eq!(close.size(), vec2(15.0, 15.0));
537 assert_eq!(close.right(), R.right() - 1.0 - 4.0);
538 h.frame(click(close.center()), &mut ui);
539 h.frame(Vec::new(), &mut ui);
540 assert!(!shown);
541 assert!(!open);
542 }
543
544 #[test]
546 fn title_drag_moves_window() {
547 let mut h = Harness::new();
548 let mut rect = Rect::NOTHING;
549 let mut ui = |ctx: &Context| {
550 rect = NxWindow::new("a", "Alpha")
551 .default_rect(R)
552 .show(ctx, |_| ())
553 .unwrap()
554 .response
555 .rect;
556 };
557 for _ in 0..3 {
558 h.frame(Vec::new(), &mut ui);
559 }
560 let from = pos2(200.0, 112.0);
561 drag(&mut h, from, from + vec2(60.0, 40.0), &mut ui);
562 assert_eq!(rect, R.translate(vec2(60.0, 40.0)));
563 }
564
565 fn two_windows(a: Rect, b: Rect) -> impl FnMut(&Context) {
567 move |ctx| {
568 let _ = NxWindow::new("a", "Alpha")
569 .default_rect(a)
570 .show(ctx, |_| ());
571 let _ = NxWindow::new("b", "Bravo")
572 .default_rect(b)
573 .show(ctx, |_| ());
574 }
575 }
576
577 fn title(r: Rect) -> Rect {
578 layout(r, false, true).title
579 }
580
581 #[test]
584 fn click_makes_window_key() {
585 let mut h = Harness::new();
586 let a = Rect::from_min_size(pos2(50.0, 50.0), vec2(200.0, 150.0));
587 let b = a.translate(vec2(350.0, 0.0));
588 let mut ui = two_windows(a, b);
589
590 let mut shapes = Vec::new();
591 for _ in 0..3 {
592 shapes = h.frame(Vec::new(), &mut ui);
593 }
594 assert!(is_key(&h.ctx, wid("a")), "first window shown is key");
595 assert_eq!(fill_at(&shapes, title(a)), Some(BLACK));
596 assert_eq!(fill_at(&shapes, title(b)), Some(DARK_GRAY));
597
598 h.frame(click(b.center()), &mut ui);
599 shapes = h.frame(Vec::new(), &mut ui);
600 assert!(is_key(&h.ctx, wid("b")));
601 assert!(!is_key(&h.ctx, wid("a")));
602 assert_eq!(fill_at(&shapes, title(b)), Some(BLACK));
603 assert_eq!(fill_at(&shapes, title(a)), Some(DARK_GRAY));
604 assert_eq!(text_color(&shapes, "Bravo"), Some(WHITE));
605 assert_eq!(text_color(&shapes, "Alpha"), Some(LIGHT_GRAY));
606 }
607
608 #[test]
611 fn overlap_click_keys_the_top_window() {
612 let mut h = Harness::new();
613 let a = R;
614 let b = R.translate(vec2(150.0, 100.0));
615 let mut ui = two_windows(a, b);
616 for _ in 0..3 {
617 h.frame(Vec::new(), &mut ui);
618 }
619 let overlap = a.intersect(b).center();
620
621 h.frame(click(overlap), &mut ui);
622 assert!(is_key(&h.ctx, wid("b")), "B was shown last, so is on top");
623 assert!(!is_key(&h.ctx, wid("a")));
624
625 h.frame(click(a.min + vec2(20.0, 50.0)), &mut ui);
626 assert!(is_key(&h.ctx, wid("a")));
627 assert!(!is_key(&h.ctx, wid("b")));
628
629 h.frame(click(overlap), &mut ui);
631 assert!(is_key(&h.ctx, wid("a")));
632 }
633
634 #[test]
637 fn click_frame_paints_both_titles_black() {
638 let mut h = Harness::new();
639 let a = Rect::from_min_size(pos2(50.0, 50.0), vec2(200.0, 150.0));
640 let b = a.translate(vec2(350.0, 0.0));
641 let mut ui = two_windows(a, b);
642 for _ in 0..3 {
643 h.frame(Vec::new(), &mut ui);
644 }
645 let shapes = h.frame(click(b.center()), &mut ui);
646 assert_eq!(fill_at(&shapes, title(a)), Some(BLACK));
647 assert_eq!(fill_at(&shapes, title(b)), Some(BLACK));
648 }
649
650 #[test]
652 fn closing_key_window_hands_key_on() {
653 let mut h = Harness::new();
654 let mut open = true;
655 let b = R.translate(vec2(350.0, 0.0));
656 let mut ui = |ctx: &Context| {
657 let _ = NxWindow::new("a", "Alpha")
658 .default_rect(R)
659 .closable(&mut open)
660 .show(ctx, |_| ());
661 if !open {
662 let _ = NxWindow::new("b", "Bravo")
663 .default_rect(b)
664 .show(ctx, |_| ());
665 }
666 };
667 for _ in 0..3 {
668 h.frame(Vec::new(), &mut ui);
669 }
670 assert!(is_key(&h.ctx, wid("a")));
671 h.frame(
672 click(layout(R, true, true).close.unwrap().center()),
673 &mut ui,
674 );
675 h.frame(Vec::new(), &mut ui);
676 assert!(!open);
677 assert!(is_key(&h.ctx, wid("b")));
678 }
679
680 #[test]
682 fn off_screen_window_is_clamped_on_screen() {
683 let mut h = Harness::new();
684 let mut rect = Rect::NOTHING;
685 for _ in 0..3 {
686 h.frame(Vec::new(), |ctx| {
687 rect = NxWindow::new("a", "Alpha")
688 .default_rect(R.translate(vec2(1000.0, 1000.0)))
689 .show(ctx, |_| ())
690 .unwrap()
691 .response
692 .rect;
693 });
694 }
695 assert_eq!(rect.max, SCREEN.max);
696 assert_eq!(rect.size(), R.size());
697 }
698
699 fn drag_resize_bar(resizable: bool, x: f32, delta: Vec2) -> Rect {
701 let mut h = Harness::new();
702 let mut rect = Rect::NOTHING;
703 let mut ui = |ctx: &Context| {
704 rect = NxWindow::new("a", "Alpha")
705 .default_rect(R)
706 .resizable(resizable)
707 .show(ctx, |_| ())
708 .unwrap()
709 .response
710 .rect;
711 };
712 for _ in 0..3 {
713 h.frame(Vec::new(), &mut ui);
714 }
715 let from = pos2(x, R.bottom() - 1.0 - 4.5);
716 drag(&mut h, from, from + delta, &mut ui);
717 rect
718 }
719
720 #[test]
723 fn resize_bar_resizes_only_when_resizable() {
724 let d = vec2(40.0, 30.0);
725 let right = drag_resize_bar(true, R.right() - 20.0, d);
726 assert_eq!(right, Rect::from_min_max(R.min, R.max + d));
727 let left = drag_resize_bar(true, R.left() + 20.0, d);
728 assert_eq!(
729 left,
730 Rect::from_min_max(R.min + vec2(40.0, 0.0), R.max + vec2(0.0, 30.0))
731 );
732 assert_eq!(drag_resize_bar(false, R.right() - 20.0, d), R);
733 }
734
735 #[test]
737 fn middle_third_resizes_height_only() {
738 let mid = drag_resize_bar(true, R.center().x, vec2(40.0, 30.0));
739 assert_eq!(mid, Rect::from_min_max(R.min, R.max + vec2(0.0, 30.0)));
740 }
741
742 #[test]
745 fn resize_past_screen_bottom_clamps_the_edge() {
746 let r = drag_resize_bar(true, R.center().x, vec2(0.0, 400.0));
747 assert_eq!(r.min.y, R.min.y);
748 assert_eq!(r.max.y, SCREEN.max.y);
749 }
750
751 fn show_a(ctx: &Context, f: impl FnOnce(NxWindow<'_>) -> NxWindow<'_>) -> Rect {
753 f(NxWindow::new("a", "Alpha").default_rect(R))
754 .show(ctx, |_| ())
755 .unwrap()
756 .response
757 .rect
758 }
759
760 #[test]
763 fn left_resize_stops_at_min_size() {
764 let min = vec2(240.0, 180.0);
765 let mut h = Harness::new();
766 let mut ui = |ctx: &Context| {
767 show_a(ctx, |w| w.min_size(min));
768 };
769 for _ in 0..3 {
770 h.frame(Vec::new(), &mut ui);
771 }
772 let from = pos2(R.left() + 20.0, R.bottom() - 1.0 - 4.5);
773 drag(&mut h, from, from + vec2(400.0, 0.0), &mut ui);
774 let rect = stored_rect(&h.ctx, wid("a")).unwrap();
775 assert_eq!(rect.max.x, R.max.x);
776 assert_eq!(rect.width(), min.x);
777 }
778
779 #[test]
781 fn bounds_clamp_a_dragged_window() {
782 let bounds = Rect::from_min_max(pos2(50.0, 60.0), pos2(600.0, 500.0));
783 let mut h = Harness::new();
784 let mut ui = |ctx: &Context| {
785 show_a(ctx, |w| w.bounds(bounds));
786 };
787 for _ in 0..3 {
788 h.frame(Vec::new(), &mut ui);
789 }
790 let from = title(R).center();
791 drag(&mut h, from, from + vec2(500.0, 500.0), &mut ui);
792 let rect = stored_rect(&h.ctx, wid("a")).unwrap();
793 assert_eq!(rect.max, bounds.max);
794 assert_eq!(rect.size(), R.size());
795 }
796
797 #[test]
800 fn owned_rect_overrides_and_reads_back() {
801 let mut h = Harness::new();
802 for _ in 0..3 {
803 h.frame(Vec::new(), |ctx| {
804 show_a(ctx, |w| w);
805 });
806 }
807 assert_eq!(stored_rect(&h.ctx, wid("a")), Some(R));
808 let start = R.translate(vec2(200.0, 150.0));
809 let mut shown = Rect::NOTHING;
810 h.frame(Vec::new(), |ctx| shown = show_a(ctx, |w| w.rect(start)));
811 assert_eq!(shown, start, "rect() beats the stored R");
812 let mut owned = start;
813 let mut ui = |ctx: &Context| {
814 shown = show_a(ctx, |w| w.rect(owned));
815 owned = stored_rect(ctx, wid("a")).unwrap();
816 };
817 let from = title(start).center();
818 let d = vec2(-60.0, -40.0);
819 drag(&mut h, from, from + d, &mut ui);
820 assert_eq!(owned, start.translate(d));
821 assert_eq!(shown, owned);
822 }
823
824 #[test]
826 fn undersized_rect_is_enlarged() {
827 let min = vec2(240.0, 180.0);
828 let small = Rect::from_min_size(pos2(100.0, 100.0), vec2(50.0, 40.0));
829 let mut h = Harness::new();
830 let mut rect = Rect::NOTHING;
831 for _ in 0..3 {
832 h.frame(Vec::new(), |ctx| {
833 rect = show_a(ctx, |w| w.rect(small).min_size(min));
834 });
835 }
836 assert_eq!(rect, Rect::from_min_size(small.min, min));
837 assert_eq!(stored_rect(&h.ctx, wid("a")), Some(rect));
838 }
839}