1use collections::HashMap;
2use gpui::{
3 actions,
4 elements::{ChildView, Container, Empty, MouseEventHandler, Side, Svg},
5 impl_internal_actions, Border, CursorStyle, Element, ElementBox, Entity, MouseButton,
6 MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
7};
8use serde::Deserialize;
9use settings::{DockAnchor, Settings};
10use theme::Theme;
11
12use crate::{sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace};
13
14#[derive(PartialEq, Clone, Deserialize)]
15pub struct MoveDock(pub DockAnchor);
16
17#[derive(PartialEq, Clone)]
18pub struct AddDefaultItemToDock;
19
20actions!(
21 dock,
22 [
23 FocusDock,
24 HideDock,
25 AnchorDockRight,
26 AnchorDockBottom,
27 ExpandDock
28 ]
29);
30impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
31
32pub fn init(cx: &mut MutableAppContext) {
33 cx.add_action(Dock::focus_dock);
34 cx.add_action(Dock::hide_dock);
35 cx.add_action(Dock::move_dock);
36 cx.add_action(
37 |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
38 settings::settings_file::write_setting("default_dock_anchor", "right".to_string(), cx);
39 Dock::move_dock(workspace, &MoveDock(DockAnchor::Right), cx)
40 },
41 );
42 cx.add_action(
43 |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
44 settings::settings_file::write_setting("default_dock_anchor", "bottom".to_string(), cx);
45 Dock::move_dock(workspace, &MoveDock(DockAnchor::Bottom), cx)
46 },
47 );
48 cx.add_action(
49 |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
50 settings::settings_file::write_setting(
51 "default_dock_anchor",
52 "expanded".to_string(),
53 cx,
54 );
55 Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
56 },
57 );
58}
59
60#[derive(Copy, Clone, PartialEq, Eq, Debug)]
61pub enum DockPosition {
62 Shown(DockAnchor),
63 Hidden(DockAnchor),
64}
65
66impl Default for DockPosition {
67 fn default() -> Self {
68 DockPosition::Hidden(Default::default())
69 }
70}
71
72pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
73 match anchor {
74 DockAnchor::Right => "icons/dock_right_12.svg",
75 DockAnchor::Bottom => "icons/dock_bottom_12.svg",
76 DockAnchor::Expanded => "icons/dock_modal_12.svg",
77 }
78}
79
80impl DockPosition {
81 fn is_visible(&self) -> bool {
82 match self {
83 DockPosition::Shown(_) => true,
84 DockPosition::Hidden(_) => false,
85 }
86 }
87
88 fn anchor(&self) -> DockAnchor {
89 match self {
90 DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
91 }
92 }
93
94 fn hide(self) -> Self {
95 match self {
96 DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
97 DockPosition::Hidden(_) => self,
98 }
99 }
100
101 fn show(self) -> Self {
102 match self {
103 DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
104 DockPosition::Shown(_) => self,
105 }
106 }
107}
108
109pub type DefaultItemFactory =
110 fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
111
112pub struct Dock {
113 position: DockPosition,
114 panel_sizes: HashMap<DockAnchor, f32>,
115 pane: ViewHandle<Pane>,
116 default_item_factory: DefaultItemFactory,
117}
118
119impl Dock {
120 pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
121 let anchor = cx.global::<Settings>().default_dock_anchor;
122 let pane = cx.add_view(|cx| Pane::new(Some(anchor), cx));
123 pane.update(cx, |pane, cx| {
124 pane.set_active(false, cx);
125 });
126 let pane_id = pane.id();
127 cx.subscribe(&pane, move |workspace, _, event, cx| {
128 workspace.handle_pane_event(pane_id, event, cx);
129 })
130 .detach();
131
132 Self {
133 pane,
134 panel_sizes: Default::default(),
135 position: DockPosition::Hidden(anchor),
136 default_item_factory,
137 }
138 }
139
140 pub fn pane(&self) -> &ViewHandle<Pane> {
141 &self.pane
142 }
143
144 pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
145 self.position.is_visible().then(|| self.pane())
146 }
147
148 pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool {
149 self.position.is_visible() && self.position.anchor() == anchor
150 }
151
152 fn set_dock_position(
153 workspace: &mut Workspace,
154 new_position: DockPosition,
155 cx: &mut ViewContext<Workspace>,
156 ) {
157 workspace.dock.position = new_position;
158 // Tell the pane about the new anchor position
159 workspace.dock.pane.update(cx, |pane, cx| {
160 pane.set_docked(Some(new_position.anchor()), cx)
161 });
162
163 if workspace.dock.position.is_visible() {
164 // Close the right sidebar if the dock is on the right side and the right sidebar is open
165 if workspace.dock.position.anchor() == DockAnchor::Right {
166 if workspace.right_sidebar().read(cx).is_open() {
167 workspace.toggle_sidebar(SidebarSide::Right, cx);
168 }
169 }
170
171 // Ensure that the pane has at least one item or construct a default item to put in it
172 let pane = workspace.dock.pane.clone();
173 if pane.read(cx).items().next().is_none() {
174 let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
175 // Adding the item focuses the pane by default
176 Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
177 } else {
178 cx.focus(pane);
179 }
180 } else if let Some(last_active_center_pane) = workspace.last_active_center_pane.clone() {
181 cx.focus(last_active_center_pane);
182 }
183 cx.emit(crate::Event::DockAnchorChanged);
184 cx.notify();
185 }
186
187 pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
188 Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
189 }
190
191 pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
192 Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
193 }
194
195 pub fn hide_on_sidebar_shown(
196 workspace: &mut Workspace,
197 sidebar_side: SidebarSide,
198 cx: &mut ViewContext<Workspace>,
199 ) {
200 if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right))
201 || workspace.dock.is_anchored_at(DockAnchor::Expanded)
202 {
203 Self::hide(workspace, cx);
204 }
205 }
206
207 fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
208 Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
209 }
210
211 fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
212 Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
213 }
214
215 fn move_dock(
216 workspace: &mut Workspace,
217 &MoveDock(new_anchor): &MoveDock,
218 cx: &mut ViewContext<Workspace>,
219 ) {
220 Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
221 }
222
223 pub fn render(
224 &self,
225 theme: &Theme,
226 anchor: DockAnchor,
227 cx: &mut RenderContext<Workspace>,
228 ) -> Option<ElementBox> {
229 let style = &theme.workspace.dock;
230
231 self.position
232 .is_visible()
233 .then(|| self.position.anchor())
234 .filter(|current_anchor| *current_anchor == anchor)
235 .map(|anchor| match anchor {
236 DockAnchor::Bottom | DockAnchor::Right => {
237 let mut panel_style = style.panel.clone();
238 let (resize_side, initial_size) = if anchor == DockAnchor::Bottom {
239 panel_style.border = Border {
240 top: true,
241 bottom: false,
242 left: false,
243 right: false,
244 ..panel_style.border
245 };
246
247 (Side::Top, style.initial_size_bottom)
248 } else {
249 panel_style.border = Border {
250 top: false,
251 bottom: false,
252 left: true,
253 right: false,
254 ..panel_style.border
255 };
256 (Side::Left, style.initial_size_right)
257 };
258
259 enum DockResizeHandle {}
260
261 let resizable = Container::new(ChildView::new(self.pane.clone()).boxed())
262 .with_style(panel_style)
263 .with_resize_handle::<DockResizeHandle, _>(
264 resize_side as usize,
265 resize_side,
266 4.,
267 self.panel_sizes
268 .get(&anchor)
269 .copied()
270 .unwrap_or(initial_size),
271 cx,
272 );
273
274 let size = resizable.current_size();
275 let workspace = cx.handle();
276 cx.defer(move |cx| {
277 if let Some(workspace) = workspace.upgrade(cx) {
278 workspace.update(cx, |workspace, _| {
279 workspace.dock.panel_sizes.insert(anchor, size);
280 })
281 }
282 });
283
284 resizable.flex(5., false).boxed()
285 }
286 DockAnchor::Expanded => {
287 enum ExpandedDockWash {}
288 enum ExpandedDockPane {}
289 Container::new(
290 MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_state, cx| {
291 MouseEventHandler::<ExpandedDockPane>::new(0, cx, |_state, _cx| {
292 ChildView::new(self.pane.clone()).boxed()
293 })
294 .capture_all()
295 .contained()
296 .with_style(style.maximized)
297 .boxed()
298 })
299 .capture_all()
300 .on_down(MouseButton::Left, |_, cx| {
301 cx.dispatch_action(HideDock);
302 })
303 .with_cursor_style(CursorStyle::Arrow)
304 .boxed(),
305 )
306 .with_background_color(style.wash_color)
307 .boxed()
308 }
309 })
310 }
311}
312
313pub struct ToggleDockButton {
314 workspace: WeakViewHandle<Workspace>,
315}
316
317impl ToggleDockButton {
318 pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
319 // When dock moves, redraw so that the icon and toggle status matches.
320 cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
321
322 Self {
323 workspace: workspace.downgrade(),
324 }
325 }
326}
327
328impl Entity for ToggleDockButton {
329 type Event = ();
330}
331
332impl View for ToggleDockButton {
333 fn ui_name() -> &'static str {
334 "Dock Toggle"
335 }
336
337 fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
338 let workspace = self.workspace.upgrade(cx);
339
340 if workspace.is_none() {
341 return Empty::new().boxed();
342 }
343
344 let dock_position = workspace.unwrap().read(cx).dock.position;
345
346 let theme = cx.global::<Settings>().theme.clone();
347 let button = MouseEventHandler::<Self>::new(0, cx, {
348 let theme = theme.clone();
349 move |state, _| {
350 let style = theme
351 .workspace
352 .status_bar
353 .sidebar_buttons
354 .item
355 .style_for(state, dock_position.is_visible());
356
357 Svg::new(icon_for_dock_anchor(dock_position.anchor()))
358 .with_color(style.icon_color)
359 .constrained()
360 .with_width(style.icon_size)
361 .with_height(style.icon_size)
362 .contained()
363 .with_style(style.container)
364 .boxed()
365 }
366 })
367 .with_cursor_style(CursorStyle::PointingHand);
368
369 if dock_position.is_visible() {
370 button
371 .on_click(MouseButton::Left, |_, cx| {
372 cx.dispatch_action(HideDock);
373 })
374 .with_tooltip::<Self, _>(
375 0,
376 "Hide Dock".into(),
377 Some(Box::new(HideDock)),
378 theme.tooltip.clone(),
379 cx,
380 )
381 } else {
382 button
383 .on_click(MouseButton::Left, |_, cx| {
384 cx.dispatch_action(FocusDock);
385 })
386 .with_tooltip::<Self, _>(
387 0,
388 "Focus Dock".into(),
389 Some(Box::new(FocusDock)),
390 theme.tooltip.clone(),
391 cx,
392 )
393 }
394 .boxed()
395 }
396}
397
398impl StatusItemView for ToggleDockButton {
399 fn set_active_pane_item(
400 &mut self,
401 _active_pane_item: Option<&dyn crate::ItemHandle>,
402 _cx: &mut ViewContext<Self>,
403 ) {
404 //Not applicable
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use std::ops::{Deref, DerefMut};
411
412 use gpui::{AppContext, TestAppContext, UpdateView, ViewContext};
413 use project::{FakeFs, Project};
414 use settings::Settings;
415
416 use super::*;
417 use crate::{sidebar::Sidebar, tests::TestItem, ItemHandle, Workspace};
418
419 pub fn default_item_factory(
420 _workspace: &mut Workspace,
421 cx: &mut ViewContext<Workspace>,
422 ) -> Box<dyn ItemHandle> {
423 Box::new(cx.add_view(|_| TestItem::new()))
424 }
425
426 #[gpui::test]
427 async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
428 let mut cx = DockTestContext::new(cx).await;
429
430 // Closing the last item in the dock hides the dock
431 cx.move_dock(DockAnchor::Right);
432 let old_items = cx.dock_items();
433 assert!(!old_items.is_empty());
434 cx.close_dock_items().await;
435 cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
436
437 // Reopening the dock adds a new item
438 cx.move_dock(DockAnchor::Right);
439 let new_items = cx.dock_items();
440 assert!(!new_items.is_empty());
441 assert!(new_items
442 .into_iter()
443 .all(|new_item| !old_items.contains(&new_item)));
444 }
445
446 #[gpui::test]
447 async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
448 let mut cx = DockTestContext::new(cx).await;
449
450 // Dock closes when expanded for either panel
451 cx.move_dock(DockAnchor::Expanded);
452 cx.open_sidebar(SidebarSide::Left);
453 cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
454 cx.close_sidebar(SidebarSide::Left);
455 cx.move_dock(DockAnchor::Expanded);
456 cx.open_sidebar(SidebarSide::Right);
457 cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
458
459 // Dock closes in the right position if the right sidebar is opened
460 cx.move_dock(DockAnchor::Right);
461 cx.open_sidebar(SidebarSide::Left);
462 cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
463 cx.open_sidebar(SidebarSide::Right);
464 cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
465 cx.close_sidebar(SidebarSide::Right);
466
467 // Dock in bottom position ignores sidebars
468 cx.move_dock(DockAnchor::Bottom);
469 cx.open_sidebar(SidebarSide::Left);
470 cx.open_sidebar(SidebarSide::Right);
471 cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom));
472
473 // Opening the dock in the right position closes the right sidebar
474 cx.move_dock(DockAnchor::Right);
475 cx.assert_sidebar_closed(SidebarSide::Right);
476 }
477
478 #[gpui::test]
479 async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
480 let mut cx = DockTestContext::new(cx).await;
481
482 // Focusing an item not in the dock when expanded hides the dock
483 let center_item = cx.add_item_to_center_pane();
484 cx.move_dock(DockAnchor::Expanded);
485 let dock_item = cx
486 .dock_items()
487 .get(0)
488 .cloned()
489 .expect("Dock should have an item at this point");
490 center_item.update(&mut cx, |_, cx| cx.focus_self());
491 cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
492
493 // Focusing an item not in the dock when not expanded, leaves the dock open but inactive
494 cx.move_dock(DockAnchor::Right);
495 center_item.update(&mut cx, |_, cx| cx.focus_self());
496 cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
497 cx.assert_dock_pane_inactive();
498 cx.assert_workspace_pane_active();
499
500 // Focusing an item in the dock activates it's pane
501 dock_item.update(&mut cx, |_, cx| cx.focus_self());
502 cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
503 cx.assert_dock_pane_active();
504 cx.assert_workspace_pane_inactive();
505 }
506
507 #[gpui::test]
508 async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
509 let cx = DockTestContext::new(cx).await;
510
511 cx.move_dock(DockAnchor::Right);
512 cx.assert_dock_pane_active();
513 cx.hide_dock();
514 cx.move_dock(DockAnchor::Right);
515 cx.assert_dock_pane_active();
516 }
517
518 struct DockTestContext<'a> {
519 pub cx: &'a mut TestAppContext,
520 pub window_id: usize,
521 pub workspace: ViewHandle<Workspace>,
522 }
523
524 impl<'a> DockTestContext<'a> {
525 pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
526 Settings::test_async(cx);
527 let fs = FakeFs::new(cx.background());
528
529 cx.update(|cx| init(cx));
530 let project = Project::test(fs, [], cx).await;
531 let (window_id, workspace) =
532 cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
533
534 workspace.update(cx, |workspace, cx| {
535 let left_panel = cx.add_view(|_| TestItem::new());
536 workspace.left_sidebar().update(cx, |sidebar, cx| {
537 sidebar.add_item(
538 "icons/folder_tree_16.svg",
539 "Left Test Panel".to_string(),
540 left_panel.clone(),
541 cx,
542 );
543 });
544
545 let right_panel = cx.add_view(|_| TestItem::new());
546 workspace.right_sidebar().update(cx, |sidebar, cx| {
547 sidebar.add_item(
548 "icons/folder_tree_16.svg",
549 "Right Test Panel".to_string(),
550 right_panel.clone(),
551 cx,
552 );
553 });
554 });
555
556 Self {
557 cx,
558 window_id,
559 workspace,
560 }
561 }
562
563 pub fn workspace<F, T>(&self, read: F) -> T
564 where
565 F: FnOnce(&Workspace, &AppContext) -> T,
566 {
567 self.workspace.read_with(self.cx, read)
568 }
569
570 pub fn update_workspace<F, T>(&mut self, update: F) -> T
571 where
572 F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
573 {
574 self.workspace.update(self.cx, update)
575 }
576
577 pub fn sidebar<F, T>(&self, sidebar_side: SidebarSide, read: F) -> T
578 where
579 F: FnOnce(&Sidebar, &AppContext) -> T,
580 {
581 self.workspace(|workspace, cx| {
582 let sidebar = match sidebar_side {
583 SidebarSide::Left => workspace.left_sidebar(),
584 SidebarSide::Right => workspace.right_sidebar(),
585 }
586 .read(cx);
587
588 read(sidebar, cx)
589 })
590 }
591
592 pub fn center_pane_handle(&self) -> ViewHandle<Pane> {
593 self.workspace(|workspace, _| {
594 workspace
595 .last_active_center_pane
596 .clone()
597 .unwrap_or_else(|| workspace.center.panes()[0].clone())
598 })
599 }
600
601 pub fn add_item_to_center_pane(&mut self) -> ViewHandle<TestItem> {
602 self.update_workspace(|workspace, cx| {
603 let item = cx.add_view(|_| TestItem::new());
604 let pane = workspace
605 .last_active_center_pane
606 .clone()
607 .unwrap_or_else(|| workspace.center.panes()[0].clone());
608 Pane::add_item(
609 workspace,
610 &pane,
611 Box::new(item.clone()),
612 true,
613 true,
614 None,
615 cx,
616 );
617 item
618 })
619 }
620
621 pub fn dock_pane<F, T>(&self, read: F) -> T
622 where
623 F: FnOnce(&Pane, &AppContext) -> T,
624 {
625 self.workspace(|workspace, cx| {
626 let dock_pane = workspace.dock_pane().read(cx);
627 read(dock_pane, cx)
628 })
629 }
630
631 pub fn move_dock(&self, anchor: DockAnchor) {
632 self.cx.dispatch_action(self.window_id, MoveDock(anchor));
633 }
634
635 pub fn hide_dock(&self) {
636 self.cx.dispatch_action(self.window_id, HideDock);
637 }
638
639 pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) {
640 if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
641 self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
642 }
643 }
644
645 pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) {
646 if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
647 self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
648 }
649 }
650
651 pub fn dock_items(&self) -> Vec<ViewHandle<TestItem>> {
652 self.dock_pane(|pane, cx| {
653 pane.items()
654 .map(|item| {
655 item.act_as::<TestItem>(cx)
656 .expect("Dock Test Context uses TestItems in the dock")
657 })
658 .collect()
659 })
660 }
661
662 pub async fn close_dock_items(&mut self) {
663 self.update_workspace(|workspace, cx| {
664 Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true)
665 })
666 .await
667 .expect("Could not close dock items")
668 }
669
670 pub fn assert_dock_position(&self, expected_position: DockPosition) {
671 self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position));
672 }
673
674 pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) {
675 assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()));
676 }
677
678 pub fn assert_workspace_pane_active(&self) {
679 assert!(self
680 .center_pane_handle()
681 .read_with(self.cx, |pane, _| pane.is_active()));
682 }
683
684 pub fn assert_workspace_pane_inactive(&self) {
685 assert!(!self
686 .center_pane_handle()
687 .read_with(self.cx, |pane, _| pane.is_active()));
688 }
689
690 pub fn assert_dock_pane_active(&self) {
691 assert!(self.dock_pane(|pane, _| pane.is_active()))
692 }
693
694 pub fn assert_dock_pane_inactive(&self) {
695 assert!(!self.dock_pane(|pane, _| pane.is_active()))
696 }
697 }
698
699 impl<'a> Deref for DockTestContext<'a> {
700 type Target = gpui::TestAppContext;
701
702 fn deref(&self) -> &Self::Target {
703 self.cx
704 }
705 }
706
707 impl<'a> DerefMut for DockTestContext<'a> {
708 fn deref_mut(&mut self) -> &mut Self::Target {
709 &mut self.cx
710 }
711 }
712
713 impl<'a> UpdateView for DockTestContext<'a> {
714 fn update_view<T, S>(
715 &mut self,
716 handle: &ViewHandle<T>,
717 update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
718 ) -> S
719 where
720 T: View,
721 {
722 handle.update(self.cx, update)
723 }
724 }
725}