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