1use crate::TestSupportExt as _;
4use std::{rc::Rc, sync::Arc};
5
6use gpui::{
7 AnyElement, App, Bounds, Context, Div, Empty, EntityId, EventEmitter, FocusHandle, Focusable,
8 InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Point, Render, Size,
9 Stateful, Styled as _, WeakEntity, Window, div, prelude::FluentBuilder as _, px,
10};
11
12use crate::UndoHistory;
13
14use super::{
15 drag::AnyDrag,
16 layout::{NodeId, PanelId},
17 panel::PanelView,
18 tiles_geometry::{
19 MINIMUM_SIZE, ResizeDrag, ResizeSide, TileChange, apply_boundary_constraints,
20 compute_resized_bounds, content_size, magnetic_snap,
21 },
22};
23
24#[non_exhaustive]
31pub enum TilesEvent {
32 BoundsChanged {
34 panel: PanelId,
35 bounds: Bounds<Pixels>,
36 },
37 BringToFront { panel: PanelId },
39 ClosePanel { panel: PanelId },
41 DragDrop { item: AnyDrag },
44 ZoomIn { panel: PanelId },
48 ZoomOut,
50}
51
52#[derive(Clone)]
54struct Tile {
55 panel: Arc<dyn PanelView>,
56 id: PanelId,
57 bounds: Bounds<Pixels>,
58 z_index: usize,
59}
60
61#[derive(Clone, Copy)]
64struct TileMove {
65 panel: PanelId,
66 initial_pointer: Point<Pixels>,
67 initial_bounds: Bounds<Pixels>,
68}
69
70#[derive(Clone, Copy)]
72struct TileResize {
73 panel: PanelId,
74 initial_bounds: Bounds<Pixels>,
75 drag: ResizeDrag,
76}
77
78pub struct TilesState {
84 node: NodeId,
85 this: WeakEntity<Self>,
88 tiles: Vec<Tile>,
89 focus_handle: FocusHandle,
90 zoomed: Option<PanelId>,
94 moving: Option<TileMove>,
95 resizing: Option<TileResize>,
96 history: UndoHistory<TileChange>,
97 renderer: Rc<dyn TilesRenderer>,
98}
99
100impl TilesState {
101 pub(crate) fn new(node: NodeId, _window: &mut Window, cx: &mut Context<Self>) -> Self {
104 Self {
105 node,
106 this: cx.weak_entity(),
107 tiles: Vec::new(),
108 focus_handle: cx.focus_handle(),
109 zoomed: None,
110 moving: None,
111 resizing: None,
112 history: UndoHistory::new().group_interval(std::time::Duration::from_millis(100)),
113 renderer: Rc::new(BareTiles),
114 }
115 }
116
117 pub fn with_renderer(mut self, renderer: Rc<dyn TilesRenderer>) -> Self {
118 self.renderer = renderer;
119 self
120 }
121
122 pub fn node(&self) -> NodeId {
124 self.node
125 }
126
127 pub fn tiles(&self, cx: &App) -> Vec<TileContext> {
129 let mut order: Vec<usize> = (0..self.tiles.len()).collect();
130 order.sort_by_key(|ix| (self.tiles[*ix].z_index, *ix));
131 order
132 .into_iter()
133 .map(|ix| self.tile_context(ix, cx))
134 .collect()
135 }
136
137 pub(crate) fn sync_from_tree(
139 &mut self,
140 tiles: Vec<(Arc<dyn PanelView>, Bounds<Pixels>, usize)>,
141 cx: &mut Context<Self>,
142 ) {
143 self.tiles = tiles
144 .into_iter()
145 .map(|(panel, bounds, z_index)| Tile {
146 id: panel.panel_id(cx),
147 panel,
148 bounds,
149 z_index,
150 })
151 .collect();
152 if self
155 .moving
156 .is_some_and(|drag| self.index_of(drag.panel).is_none())
157 {
158 self.moving = None;
159 }
160 if self
161 .resizing
162 .is_some_and(|drag| self.index_of(drag.panel).is_none())
163 {
164 self.resizing = None;
165 }
166 cx.notify();
167 }
168
169 pub fn zoomed_tile(&self) -> Option<PanelId> {
171 self.zoomed
172 }
173
174 pub fn toggle_zoom(&mut self, panel: PanelId, window: &mut Window, cx: &mut Context<Self>) {
176 let zoomed = (self.zoomed != Some(panel)).then_some(panel);
177 self.set_zoomed(zoomed, window, cx);
178 }
179
180 pub(crate) fn set_zoomed(
192 &mut self,
193 zoomed: Option<PanelId>,
194 window: &mut Window,
195 cx: &mut Context<Self>,
196 ) {
197 if self.zoomed == zoomed {
198 return;
199 }
200 let outgoing = self.zoomed.and_then(|panel| self.panel_view(panel));
204 let incoming = zoomed.and_then(|panel| self.panel_view(panel));
205 if zoomed.is_some() && !incoming.as_ref().is_some_and(|panel| panel.zoomable(cx)) {
206 return;
207 }
208
209 self.zoomed = zoomed;
210 cx.emit(match zoomed {
211 Some(panel) => TilesEvent::ZoomIn { panel },
212 None => TilesEvent::ZoomOut,
213 });
214
215 cx.spawn_in(window, async move |_, cx| {
218 _ = cx.update(|window, cx| {
219 if let Some(panel) = outgoing {
220 panel.set_zoomed(false, window, cx);
221 }
222 if let Some(panel) = incoming {
223 panel.set_zoomed(true, window, cx);
224 }
225 });
226 })
227 .detach();
228 cx.notify();
229 }
230
231 pub fn undo(&mut self, cx: &mut Context<Self>) {
233 let Some(changes) = self.history.undo() else {
234 return;
235 };
236 for change in changes {
237 if let (Some(panel), Some(bounds)) =
238 (self.panel_of(change.tile_id()), change.old_bounds())
239 {
240 cx.emit(TilesEvent::BoundsChanged { panel, bounds });
241 }
242 }
243 cx.notify();
244 }
245
246 pub fn redo(&mut self, cx: &mut Context<Self>) {
248 let Some(changes) = self.history.redo() else {
249 return;
250 };
251 for change in changes {
252 if let (Some(panel), Some(bounds)) =
253 (self.panel_of(change.tile_id()), change.new_bounds())
254 {
255 cx.emit(TilesEvent::BoundsChanged { panel, bounds });
256 }
257 }
258 cx.notify();
259 }
260}
261
262impl TilesState {
263 fn index_of(&self, panel: PanelId) -> Option<usize> {
264 self.tiles.iter().position(|tile| tile.id == panel)
265 }
266
267 fn bounds_of(&self, panel: PanelId) -> Option<Bounds<Pixels>> {
268 self.index_of(panel).map(|ix| self.tiles[ix].bounds)
269 }
270
271 fn panel_view(&self, panel: PanelId) -> Option<Arc<dyn PanelView>> {
272 self.index_of(panel).map(|ix| self.tiles[ix].panel.clone())
273 }
274
275 fn panel_of(&self, entity: EntityId) -> Option<PanelId> {
277 self.tiles
278 .iter()
279 .find(|tile| tile.panel.view().entity_id() == entity)
280 .map(|tile| tile.id)
281 }
282
283 fn other_bounds(&self, panel: PanelId) -> Vec<Bounds<Pixels>> {
286 self.tiles
287 .iter()
288 .filter(|tile| tile.id != panel)
289 .map(|tile| tile.bounds)
290 .collect()
291 }
292
293 fn grid_size(&self, cx: &App) -> Pixels {
294 self.renderer.grid_size(cx)
295 }
296
297 fn begin_move(&mut self, panel: PanelId, pointer: Point<Pixels>, cx: &mut Context<Self>) {
298 if self.zoomed.is_some() {
302 return;
303 }
304 let Some(initial_bounds) = self.bounds_of(panel) else {
305 return;
306 };
307 self.moving = Some(TileMove {
308 panel,
309 initial_pointer: pointer,
310 initial_bounds,
311 });
312 cx.emit(TilesEvent::BringToFront { panel });
313 cx.notify();
314 }
315
316 fn move_to(&mut self, pointer: Point<Pixels>, cx: &mut Context<Self>) {
317 let Some(drag) = self.moving else {
318 return;
319 };
320 let delta = pointer - drag.initial_pointer;
321 let candidate = Bounds {
322 origin: apply_boundary_constraints(
323 drag.initial_bounds.origin + delta,
324 drag.initial_bounds.size.width,
325 ),
326 size: drag.initial_bounds.size,
327 };
328 let origin = magnetic_snap(
329 candidate,
330 &self.other_bounds(drag.panel),
331 self.grid_size(cx),
332 );
333
334 self.apply_bounds(
335 drag.panel,
336 Bounds {
337 origin,
338 size: drag.initial_bounds.size,
339 },
340 cx,
341 );
342 }
343
344 fn end_move(&mut self, cx: &mut Context<Self>) {
345 let Some(drag) = self.moving.take() else {
346 return;
347 };
348 self.record(drag.panel, drag.initial_bounds, cx);
349 }
350
351 fn begin_resize(
352 &mut self,
353 panel: PanelId,
354 side: ResizeSide,
355 pointer: Point<Pixels>,
356 cx: &mut Context<Self>,
357 ) {
358 if self.zoomed.is_some() {
359 return;
360 }
361 let Some(initial_bounds) = self.bounds_of(panel) else {
362 return;
363 };
364 self.resizing = Some(TileResize {
365 panel,
366 initial_bounds,
367 drag: ResizeDrag::new(side, pointer, initial_bounds),
368 });
369 cx.emit(TilesEvent::BringToFront { panel });
370 cx.notify();
371 }
372
373 fn resize_to(&mut self, pointer: Point<Pixels>, cx: &mut Context<Self>) {
374 let Some(resize) = self.resizing else {
375 return;
376 };
377 let previous = resize.drag.last_bounds();
378 let initial = resize.initial_bounds;
383 let delta = pointer - resize.drag.start_position();
384 let (new_x, new_y, new_width, new_height) = match resize.drag.side() {
385 ResizeSide::Left => (Some(initial.origin.x + delta.x), None, None, None),
386 ResizeSide::Right => (
387 None,
388 None,
389 Some((initial.size.width + delta.x).max(MINIMUM_SIZE.width)),
390 None,
391 ),
392 ResizeSide::Top => (None, Some(initial.origin.y + delta.y), None, None),
393 ResizeSide::Bottom => (
394 None,
395 None,
396 None,
397 Some((initial.size.height + delta.y).max(MINIMUM_SIZE.height)),
398 ),
399 ResizeSide::BottomRight => (
400 None,
401 None,
402 Some((initial.size.width + delta.x).max(MINIMUM_SIZE.width)),
403 Some((initial.size.height + delta.y).max(MINIMUM_SIZE.height)),
404 ),
405 };
406
407 let bounds = compute_resized_bounds(
408 previous,
409 new_x,
410 new_y,
411 new_width,
412 new_height,
413 &self.other_bounds(resize.panel),
414 self.grid_size(cx),
415 );
416
417 self.resizing = Some(TileResize {
418 drag: resize.drag.with_last_bounds(bounds),
419 ..resize
420 });
421 self.apply_bounds(resize.panel, bounds, cx);
422 }
423
424 fn end_resize(&mut self, cx: &mut Context<Self>) {
425 let Some(resize) = self.resizing.take() else {
426 return;
427 };
428 self.record(resize.panel, resize.initial_bounds, cx);
429 }
430
431 fn apply_bounds(&mut self, panel: PanelId, bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
434 let Some(ix) = self.index_of(panel) else {
435 return;
436 };
437 if self.tiles[ix].bounds == bounds {
438 return;
439 }
440 self.tiles[ix].bounds = bounds;
441 cx.emit(TilesEvent::BoundsChanged { panel, bounds });
442 cx.notify();
443 }
444
445 fn record(&mut self, panel: PanelId, old_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
447 let Some(ix) = self.index_of(panel) else {
448 return;
449 };
450 let tile = &self.tiles[ix];
451 if tile.bounds == old_bounds {
452 return;
453 }
454 self.history.push(TileChange::bounds_change(
455 tile.panel.view().entity_id(),
456 old_bounds,
457 tile.bounds,
458 ));
459 cx.notify();
460 }
461
462 fn close_tile(&mut self, panel: PanelId, cx: &mut Context<Self>) {
465 let closable = self
466 .tiles
467 .iter()
468 .any(|tile| tile.id == panel && tile.panel.closable(cx));
469 if !closable {
470 return;
471 }
472 cx.emit(TilesEvent::ClosePanel { panel });
473 cx.notify();
474 }
475
476 fn tile_context(&self, ix: usize, cx: &App) -> TileContext {
477 let tile = &self.tiles[ix];
478 let panel = tile.id;
479 let canvas = self.this.clone();
480
481 TileContext {
482 node: self.node,
483 panel: tile.panel.clone(),
484 id: panel,
485 bounds: tile.bounds,
486 z_index: tile.z_index,
487 moving: self.moving.is_some_and(|drag| drag.panel == panel),
488 resizing: self.resizing.is_some_and(|drag| drag.panel == panel),
489 closable: tile.panel.closable(cx),
490 zoomed: self.zoomed == Some(panel),
491 zoomable: tile.panel.zoomable(cx),
492 on_begin_move: {
493 let canvas = canvas.clone();
494 Rc::new(move |pointer, _, cx| {
495 _ = canvas.update(cx, |canvas, cx| canvas.begin_move(panel, pointer, cx));
496 })
497 },
498 on_move_to: {
499 let canvas = canvas.clone();
500 Rc::new(move |pointer, _, cx| {
501 _ = canvas.update(cx, |canvas, cx| canvas.move_to(pointer, cx));
502 })
503 },
504 on_end_move: {
505 let canvas = canvas.clone();
506 Rc::new(move |_, cx| {
507 _ = canvas.update(cx, |canvas, cx| canvas.end_move(cx));
508 })
509 },
510 on_begin_resize: {
511 let canvas = canvas.clone();
512 Rc::new(move |side, pointer, _, cx| {
513 _ = canvas.update(cx, |canvas, cx| {
514 canvas.begin_resize(panel, side, pointer, cx)
515 });
516 })
517 },
518 on_resize_to: {
519 let canvas = canvas.clone();
520 Rc::new(move |pointer, _, cx| {
521 _ = canvas.update(cx, |canvas, cx| canvas.resize_to(pointer, cx));
522 })
523 },
524 on_end_resize: {
525 let canvas = canvas.clone();
526 Rc::new(move |_, cx| {
527 _ = canvas.update(cx, |canvas, cx| canvas.end_resize(cx));
528 })
529 },
530 on_bring_to_front: {
531 let canvas = canvas.clone();
532 Rc::new(move |_, cx| {
533 _ = canvas.update(cx, |_, cx| {
534 cx.emit(TilesEvent::BringToFront { panel });
535 });
536 })
537 },
538 on_toggle_zoom: {
539 let canvas = canvas.clone();
540 Rc::new(move |window, cx| {
541 _ = canvas.update(cx, |canvas, cx| canvas.toggle_zoom(panel, window, cx));
542 })
543 },
544 on_close: Rc::new(move |_, cx| {
545 _ = canvas.update(cx, |canvas, cx| canvas.close_tile(panel, cx));
546 }),
547 }
548 }
549}
550
551impl EventEmitter<TilesEvent> for TilesState {}
552
553impl Focusable for TilesState {
554 fn focus_handle(&self, _: &App) -> FocusHandle {
555 self.focus_handle.clone()
556 }
557}
558
559impl Render for TilesState {
560 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
561 let renderer = self.renderer.clone();
562 let focus_handle = self.focus_handle.clone();
563 let zoomed = self.zoomed.filter(|panel| self.index_of(*panel).is_some());
568 let tiles: Vec<TileContext> = self
569 .tiles(cx)
570 .into_iter()
571 .filter(|tile| zoomed.is_none_or(|panel| tile.id == panel))
572 .collect();
573 let content = content_size(
576 &self
577 .tiles
578 .iter()
579 .map(|tile| tile.bounds)
580 .collect::<Vec<_>>(),
581 );
582
583 renderer
584 .frame(window, cx)
585 .test_support()
586 .track_focus(&focus_handle)
587 .on_drop(cx.listener(|_, item: &AnyDrag, _, cx| {
588 cx.emit(TilesEvent::DragDrop { item: item.clone() });
589 }))
590 .children(
591 tiles
592 .into_iter()
593 .map(|tile| {
594 renderer
595 .tile_frame(&tile, window, cx)
596 .when(!tile.zoomed, |this| {
605 this.absolute()
606 .left(tile.bounds.origin.x)
607 .top(tile.bounds.origin.y)
608 .w(tile.bounds.size.width)
609 .h(tile.bounds.size.height)
610 })
611 .child(renderer.render_drag_bar(&tile, window, cx))
612 .child(
613 renderer
614 .panel_frame(&tile, window, cx)
615 .child(tile.panel.view()),
616 )
617 .when(!tile.zoomed, |this| {
620 this.child(renderer.render_resize_handles(&tile, window, cx))
621 })
622 })
623 .collect::<Vec<_>>(),
624 )
625 .when(zoomed.is_none(), |this| {
629 this.children(renderer.render_overlay(content, window, cx))
630 })
631 }
632}
633
634type MovePointerHandler = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
635type ResizeStartHandler = Rc<dyn Fn(ResizeSide, Point<Pixels>, &mut Window, &mut App)>;
636type GestureEndHandler = Rc<dyn Fn(&mut Window, &mut App)>;
637
638#[derive(Clone)]
641pub struct TileContext {
642 node: NodeId,
643 panel: Arc<dyn PanelView>,
644 id: PanelId,
645 bounds: Bounds<Pixels>,
646 z_index: usize,
647 moving: bool,
648 resizing: bool,
649 closable: bool,
650 zoomed: bool,
651 zoomable: bool,
652 on_begin_move: MovePointerHandler,
653 on_move_to: MovePointerHandler,
654 on_end_move: GestureEndHandler,
655 on_begin_resize: ResizeStartHandler,
656 on_resize_to: MovePointerHandler,
657 on_end_resize: GestureEndHandler,
658 on_bring_to_front: GestureEndHandler,
659 on_toggle_zoom: GestureEndHandler,
660 on_close: GestureEndHandler,
661}
662
663impl TileContext {
664 pub fn node(&self) -> NodeId {
667 self.node
668 }
669
670 pub fn panel(&self) -> &Arc<dyn PanelView> {
671 &self.panel
672 }
673
674 pub fn panel_id(&self) -> PanelId {
675 self.id
676 }
677
678 pub fn bounds(&self) -> Bounds<Pixels> {
679 self.bounds
680 }
681
682 pub fn z_index(&self) -> usize {
683 self.z_index
684 }
685
686 pub fn is_moving(&self) -> bool {
687 self.moving
688 }
689
690 pub fn is_resizing(&self) -> bool {
691 self.resizing
692 }
693
694 pub fn is_closable(&self) -> bool {
695 self.closable
696 }
697
698 pub fn is_zoomed(&self) -> bool {
704 self.zoomed
705 }
706
707 pub fn is_zoomable(&self) -> bool {
711 self.zoomable
712 }
713
714 pub fn begin_move(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
718 (self.on_begin_move)(pointer, window, cx);
719 }
720
721 pub fn move_to(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
722 (self.on_move_to)(pointer, window, cx);
723 }
724
725 pub fn end_move(&self, window: &mut Window, cx: &mut App) {
726 (self.on_end_move)(window, cx);
727 }
728
729 pub fn begin_resize(
730 &self,
731 side: ResizeSide,
732 pointer: Point<Pixels>,
733 window: &mut Window,
734 cx: &mut App,
735 ) {
736 (self.on_begin_resize)(side, pointer, window, cx);
737 }
738
739 pub fn resize_to(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
740 (self.on_resize_to)(pointer, window, cx);
741 }
742
743 pub fn end_resize(&self, window: &mut Window, cx: &mut App) {
744 (self.on_end_resize)(window, cx);
745 }
746
747 pub fn bring_to_front(&self, window: &mut Window, cx: &mut App) {
748 (self.on_bring_to_front)(window, cx);
749 }
750
751 pub fn toggle_zoom(&self, window: &mut Window, cx: &mut App) {
758 (self.on_toggle_zoom)(window, cx);
759 }
760
761 pub fn close(&self, window: &mut Window, cx: &mut App) {
764 (self.on_close)(window, cx);
765 }
766}
767
768#[allow(unused_variables)]
777pub trait TilesRenderer: 'static {
778 fn frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
780 div().id("tiles")
781 }
782
783 fn tile_frame(&self, tile: &TileContext, window: &mut Window, cx: &mut App) -> Stateful<Div> {
785 div().id("tile")
786 }
787
788 fn render_drag_bar(&self, tile: &TileContext, window: &mut Window, cx: &mut App) -> AnyElement;
792
793 fn render_resize_handles(
796 &self,
797 tile: &TileContext,
798 window: &mut Window,
799 cx: &mut App,
800 ) -> AnyElement {
801 Empty.into_any_element()
802 }
803
804 fn panel_frame(&self, tile: &TileContext, window: &mut Window, cx: &mut App) -> Stateful<Div> {
815 div().id(("tile-panel", tile.panel_id().as_u64()))
816 }
817
818 fn render_overlay(
830 &self,
831 content: Size<Pixels>,
832 window: &mut Window,
833 cx: &mut App,
834 ) -> Option<AnyElement> {
835 None
836 }
837
838 fn grid_size(&self, cx: &App) -> Pixels {
843 px(10.)
844 }
845}
846
847pub(crate) struct BareTiles;
849
850impl TilesRenderer for BareTiles {
851 fn render_drag_bar(&self, _: &TileContext, _: &mut Window, _: &mut App) -> AnyElement {
852 Empty.into_any_element()
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use std::{cell::RefCell, rc::Rc};
859
860 use gpui::{Bounds, Entity, TestAppContext, VisualTestContext, point, size};
861
862 use super::*;
863 use crate::ElementExt as _;
864 use crate::dock::{
865 DockArea, DockAreaRenderer, DockLayout, TabGroupRenderer, test_support::TestPanel,
866 };
867
868 #[derive(Default)]
875 struct DrawOrder {
876 painted: Vec<&'static str>,
877 content: Option<Size<Pixels>>,
878 tiles: Vec<TileContext>,
881 }
882
883 struct OrderRecorder {
884 order: Rc<RefCell<DrawOrder>>,
885 }
886
887 impl TilesRenderer for OrderRecorder {
888 fn render_drag_bar(&self, tile: &TileContext, _: &mut Window, _: &mut App) -> AnyElement {
889 self.order.borrow_mut().tiles.push(tile.clone());
890 Empty.into_any_element()
891 }
892
893 fn panel_frame(&self, tile: &TileContext, _: &mut Window, _: &mut App) -> Stateful<Div> {
894 let order = self.order.clone();
895 div()
896 .id(("tile", tile.panel_id().as_u64()))
897 .on_prepaint(move |_, _, _| order.borrow_mut().painted.push("tile"))
898 }
899
900 fn render_overlay(
901 &self,
902 content: Size<Pixels>,
903 _: &mut Window,
904 _: &mut App,
905 ) -> Option<AnyElement> {
906 let order = self.order.clone();
907 Some(
908 div()
909 .on_prepaint(move |_, _, _| {
910 let mut order = order.borrow_mut();
911 order.painted.push("overlay");
912 order.content = Some(content);
913 })
914 .into_any_element(),
915 )
916 }
917 }
918
919 impl TabGroupRenderer for OrderRecorder {
920 fn render_tab_bar(
921 &self,
922 _: &super::super::TabGroupContext,
923 _: &mut Window,
924 _: &mut App,
925 ) -> AnyElement {
926 Empty.into_any_element()
927 }
928 }
929
930 impl DockAreaRenderer for OrderRecorder {
931 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
932 Rc::new(OrderRecorder {
933 order: self.order.clone(),
934 })
935 }
936
937 fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
938 Rc::new(OrderRecorder {
939 order: self.order.clone(),
940 })
941 }
942 }
943
944 fn setup_order(
945 cx: &mut TestAppContext,
946 ) -> (
947 Entity<DockArea>,
948 Rc<RefCell<DrawOrder>>,
949 &mut VisualTestContext,
950 ) {
951 cx.update(|cx| {
952 let _ = crate::Theme::global_mut(cx);
953 });
954 let order: Rc<RefCell<DrawOrder>> = Rc::default();
955 let renderer = Rc::new(OrderRecorder {
956 order: order.clone(),
957 });
958 let (area, cx) = cx.add_window_view(|window, cx| {
959 DockArea::new("tiles-order", None, window, cx).with_renderer(renderer)
960 });
961 (area, order, cx)
962 }
963
964 #[gpui::test]
973 fn the_canvas_overlay_is_drawn_after_every_tile(cx: &mut TestAppContext) {
974 let (area, order, cx) = setup_order(cx);
975
976 cx.update(|window, cx| {
977 let first = TestPanel::new("First", cx);
978 let second = TestPanel::new("Second", cx);
979 let layout = DockLayout::tiles()
980 .tile(
981 first,
982 Bounds {
983 origin: point(px(20.), px(20.)),
984 size: size(px(100.), px(80.)),
985 },
986 )
987 .tile(
988 second,
989 Bounds {
990 origin: point(px(140.), px(20.)),
991 size: size(px(100.), px(80.)),
992 },
993 );
994 area.update(cx, |area, cx| area.set_center(layout, window, cx));
995 });
996 cx.run_until_parked();
997 order.borrow_mut().painted.clear();
998 cx.update(|window, cx| window.draw(cx).clear(cx));
999
1000 assert_eq!(
1001 order.borrow().painted,
1002 vec!["tile", "tile", "overlay"],
1003 "the overlay must come after every tile, or it paints beneath them"
1004 );
1005
1006 assert_eq!(
1009 order.borrow().content,
1010 Some(size(px(240.), px(100.))),
1011 "the overlay is given the canvas's scrollable extent"
1012 );
1013 }
1014
1015 #[gpui::test]
1021 fn a_resize_tracks_the_pointer_travel_not_its_window_position(cx: &mut TestAppContext) {
1022 let (area, order, cx) = setup_order(cx);
1023
1024 cx.update(|window, cx| {
1025 let panel = TestPanel::new("Only", cx);
1026 let layout = DockLayout::tiles().tile(
1027 panel,
1028 Bounds {
1029 origin: point(px(20.), px(20.)),
1030 size: size(px(100.), px(100.)),
1031 },
1032 );
1033 area.update(cx, |area, cx| area.set_center(layout, window, cx));
1034 });
1035 cx.run_until_parked();
1036 cx.update(|window, cx| window.draw(cx).clear(cx));
1037 let tile = order.borrow().tiles.last().cloned().expect("the tile drew");
1038
1039 let start = point(px(500.), px(300.));
1042 cx.update(|window, cx| tile.begin_resize(ResizeSide::Right, start, window, cx));
1043 cx.update(|window, cx| tile.resize_to(start, window, cx));
1044 cx.run_until_parked();
1045 cx.update(|window, cx| window.draw(cx).clear(cx));
1046 assert_eq!(
1047 order.borrow().tiles.last().unwrap().bounds().size.width,
1048 px(100.),
1049 "a pointer that has not moved must not resize the tile"
1050 );
1051
1052 cx.update(|window, cx| tile.resize_to(start + point(px(32.), px(0.)), window, cx));
1055 cx.update(|window, cx| tile.end_resize(window, cx));
1056 cx.run_until_parked();
1057 cx.update(|window, cx| window.draw(cx).clear(cx));
1058 assert_eq!(
1059 order.borrow().tiles.last().unwrap().bounds().size.width,
1060 px(130.),
1061 "the tile grows by the pointer's travel, grid-rounded"
1062 );
1063 }
1064
1065 #[gpui::test]
1067 fn a_zoomed_canvas_draws_no_overlay(cx: &mut TestAppContext) {
1068 let (area, order, cx) = setup_order(cx);
1069
1070 let panel = cx.update(|window, cx| {
1071 let panel = TestPanel::new("Only", cx);
1072 let layout = DockLayout::tiles().tile(
1073 panel.clone(),
1074 Bounds {
1075 origin: point(px(20.), px(20.)),
1076 size: size(px(100.), px(80.)),
1077 },
1078 );
1079 area.update(cx, |area, cx| area.set_center(layout, window, cx));
1080 panel
1081 });
1082 cx.run_until_parked();
1083
1084 cx.update(|window, cx| window.draw(cx).clear(cx));
1086 let tile = order.borrow().tiles.last().cloned().expect("the tile drew");
1087 cx.update(|window, cx| tile.toggle_zoom(window, cx));
1088 cx.run_until_parked();
1089 assert_eq!(
1090 cx.read(|cx| area.read(cx).zoomed_tile()),
1091 Some(PanelId::from(panel.entity_id())),
1092 "the tile is the one filling the dock"
1093 );
1094 order.borrow_mut().painted.clear();
1095 cx.update(|window, cx| window.draw(cx).clear(cx));
1096
1097 assert_eq!(
1098 order.borrow().painted,
1099 vec!["tile"],
1100 "a zoomed tile fills the dock, so no overlay is drawn over it"
1101 );
1102 }
1103}