1use super::{ItemViewHandle, SplitDirection};
2use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace};
3use collections::{HashMap, VecDeque};
4use gpui::{
5 action,
6 elements::*,
7 geometry::{rect::RectF, vector::vec2f},
8 keymap::Binding,
9 platform::CursorStyle,
10 Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle,
11};
12use postage::watch;
13use project::ProjectEntry;
14use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
15use util::ResultExt;
16
17action!(Split, SplitDirection);
18action!(ActivateItem, usize);
19action!(ActivatePrevItem);
20action!(ActivateNextItem);
21action!(CloseActiveItem);
22action!(CloseItem, usize);
23action!(GoBack);
24action!(GoForward);
25
26const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
27
28pub fn init(cx: &mut MutableAppContext) {
29 cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
30 pane.activate_item(action.0, cx);
31 });
32 cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
33 pane.activate_prev_item(cx);
34 });
35 cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
36 pane.activate_next_item(cx);
37 });
38 cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
39 pane.close_active_item(cx);
40 });
41 cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
42 pane.close_item(action.0, cx);
43 });
44 cx.add_action(|pane: &mut Pane, action: &Split, cx| {
45 pane.split(action.0, cx);
46 });
47 cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
48 Pane::go_back(workspace, cx).detach();
49 });
50 cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
51 Pane::go_forward(workspace, cx).detach();
52 });
53
54 cx.add_bindings(vec![
55 Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
56 Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
57 Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
58 Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
59 Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
60 Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
61 Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
62 Binding::new("ctrl--", GoBack, Some("Pane")),
63 Binding::new("shift-ctrl-_", GoForward, Some("Pane")),
64 ]);
65}
66
67pub enum Event {
68 Activate,
69 Remove,
70 Split(SplitDirection),
71}
72
73const MAX_TAB_TITLE_LEN: usize = 24;
74
75pub struct Pane {
76 item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
77 active_item_index: usize,
78 settings: watch::Receiver<Settings>,
79 nav_history: Rc<NavHistory>,
80}
81
82#[derive(Default)]
83pub struct NavHistory(RefCell<NavHistoryState>);
84
85#[derive(Default)]
86struct NavHistoryState {
87 mode: NavigationMode,
88 backward_stack: VecDeque<NavigationEntry>,
89 forward_stack: VecDeque<NavigationEntry>,
90 project_entries_by_item: HashMap<usize, ProjectEntry>,
91}
92
93#[derive(Copy, Clone)]
94enum NavigationMode {
95 Normal,
96 GoingBack,
97 GoingForward,
98}
99
100impl Default for NavigationMode {
101 fn default() -> Self {
102 Self::Normal
103 }
104}
105
106pub struct NavigationEntry {
107 pub item_view: Box<dyn WeakItemViewHandle>,
108 pub data: Option<Box<dyn Any>>,
109}
110
111impl Pane {
112 pub fn new(settings: watch::Receiver<Settings>) -> Self {
113 Self {
114 item_views: Vec::new(),
115 active_item_index: 0,
116 settings,
117 nav_history: Default::default(),
118 }
119 }
120
121 pub fn activate(&self, cx: &mut ViewContext<Self>) {
122 cx.emit(Event::Activate);
123 }
124
125 pub fn go_back(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
126 Self::navigate_history(
127 workspace,
128 workspace.active_pane().clone(),
129 NavigationMode::GoingBack,
130 cx,
131 )
132 }
133
134 pub fn go_forward(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
135 Self::navigate_history(
136 workspace,
137 workspace.active_pane().clone(),
138 NavigationMode::GoingForward,
139 cx,
140 )
141 }
142
143 fn navigate_history(
144 workspace: &mut Workspace,
145 pane: ViewHandle<Pane>,
146 mode: NavigationMode,
147 cx: &mut ViewContext<Workspace>,
148 ) -> Task<()> {
149 let to_load = pane.update(cx, |pane, cx| {
150 // Retrieve the weak item handle from the history.
151 let nav_entry = pane.nav_history.pop(mode)?;
152
153 // If the item is still present in this pane, then activate it.
154 if let Some(index) = nav_entry
155 .item_view
156 .upgrade(cx)
157 .and_then(|v| pane.index_for_item_view(v.as_ref()))
158 {
159 if let Some(item_view) = pane.active_item() {
160 pane.nav_history.set_mode(mode);
161 item_view.deactivated(cx);
162 pane.nav_history.set_mode(NavigationMode::Normal);
163 }
164
165 pane.active_item_index = index;
166 pane.focus_active_item(cx);
167 if let Some(data) = nav_entry.data {
168 pane.active_item()?.navigate(data, cx);
169 }
170 cx.notify();
171 None
172 }
173 // If the item is no longer present in this pane, then retrieve its
174 // project path in order to reopen it.
175 else {
176 pane.nav_history
177 .0
178 .borrow_mut()
179 .project_entries_by_item
180 .get(&nav_entry.item_view.id())
181 .cloned()
182 .map(|project_entry| (project_entry, nav_entry))
183 }
184 });
185
186 if let Some((project_entry, nav_entry)) = to_load {
187 // If the item was no longer present, then load it again from its previous path.
188 let pane = pane.downgrade();
189 let task = workspace.load_entry(project_entry, cx);
190 cx.spawn(|workspace, mut cx| async move {
191 let item = task.await;
192 if let Some(pane) = cx.read(|cx| pane.upgrade(cx)) {
193 if let Some(item) = item.log_err() {
194 workspace.update(&mut cx, |workspace, cx| {
195 pane.update(cx, |p, _| p.nav_history.set_mode(mode));
196 let item_view = workspace.open_item_in_pane(item, &pane, cx);
197 pane.update(cx, |p, _| p.nav_history.set_mode(NavigationMode::Normal));
198
199 if let Some(data) = nav_entry.data {
200 item_view.navigate(data, cx);
201 }
202 });
203 } else {
204 workspace
205 .update(&mut cx, |workspace, cx| {
206 Self::navigate_history(workspace, pane, mode, cx)
207 })
208 .await;
209 }
210 }
211 })
212 } else {
213 Task::ready(())
214 }
215 }
216
217 pub fn open_item<T>(
218 &mut self,
219 item_handle: T,
220 workspace: &Workspace,
221 cx: &mut ViewContext<Self>,
222 ) -> Box<dyn ItemViewHandle>
223 where
224 T: 'static + ItemHandle,
225 {
226 for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() {
227 if *item_id == item_handle.id() {
228 let item_view = item_view.boxed_clone();
229 self.activate_item(ix, cx);
230 return item_view;
231 }
232 }
233
234 let item_view =
235 item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx);
236 self.add_item_view(item_view.boxed_clone(), cx);
237 item_view
238 }
239
240 pub fn add_item_view(
241 &mut self,
242 mut item_view: Box<dyn ItemViewHandle>,
243 cx: &mut ViewContext<Self>,
244 ) {
245 item_view.added_to_pane(cx);
246 let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len());
247 self.item_views
248 .insert(item_idx, (item_view.item_handle(cx).id(), item_view));
249 self.activate_item(item_idx, cx);
250 cx.notify();
251 }
252
253 pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
254 let item_id = item.id();
255 self.item_views
256 .iter()
257 .any(|(existing_item_id, _)| *existing_item_id == item_id)
258 }
259
260 pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
261 self.item_views.iter().map(|(_, view)| view)
262 }
263
264 pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
265 self.item_views
266 .get(self.active_item_index)
267 .map(|(_, view)| view.clone())
268 }
269
270 pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
271 self.item_views
272 .iter()
273 .position(|(_, i)| i.id() == item_view.id())
274 }
275
276 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
277 self.item_views.iter().position(|(id, _)| *id == item.id())
278 }
279
280 pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
281 if index < self.item_views.len() {
282 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
283 if prev_active_item_ix != self.active_item_index {
284 self.item_views[prev_active_item_ix].1.deactivated(cx);
285 }
286 self.focus_active_item(cx);
287 cx.notify();
288 }
289 }
290
291 pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
292 let mut index = self.active_item_index;
293 if index > 0 {
294 index -= 1;
295 } else if self.item_views.len() > 0 {
296 index = self.item_views.len() - 1;
297 }
298 self.activate_item(index, cx);
299 }
300
301 pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
302 let mut index = self.active_item_index;
303 if index + 1 < self.item_views.len() {
304 index += 1;
305 } else {
306 index = 0;
307 }
308 self.activate_item(index, cx);
309 }
310
311 pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
312 if !self.item_views.is_empty() {
313 self.close_item(self.item_views[self.active_item_index].1.id(), cx)
314 }
315 }
316
317 pub fn close_item(&mut self, item_view_id: usize, cx: &mut ViewContext<Self>) {
318 let mut item_ix = 0;
319 self.item_views.retain(|(_, item_view)| {
320 if item_view.id() == item_view_id {
321 if item_ix == self.active_item_index {
322 item_view.deactivated(cx);
323 }
324
325 let mut nav_history = self.nav_history.0.borrow_mut();
326 if let Some(entry) = item_view.project_entry(cx) {
327 nav_history
328 .project_entries_by_item
329 .insert(item_view.id(), entry);
330 } else {
331 nav_history.project_entries_by_item.remove(&item_view.id());
332 }
333
334 item_ix += 1;
335 false
336 } else {
337 item_ix += 1;
338 true
339 }
340 });
341 self.active_item_index = cmp::min(
342 self.active_item_index,
343 self.item_views.len().saturating_sub(1),
344 );
345
346 if self.item_views.is_empty() {
347 cx.emit(Event::Remove);
348 }
349 cx.notify();
350 }
351
352 fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
353 if let Some(active_item) = self.active_item() {
354 cx.focus(active_item.to_any());
355 }
356 }
357
358 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
359 cx.emit(Event::Split(direction));
360 }
361
362 fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
363 let settings = self.settings.borrow();
364 let theme = &settings.theme;
365
366 enum Tabs {}
367 let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
368 let mut row = Flex::row();
369 for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
370 let is_active = ix == self.active_item_index;
371
372 row.add_child({
373 let mut title = item_view.title(cx);
374 if title.len() > MAX_TAB_TITLE_LEN {
375 let mut truncated_len = MAX_TAB_TITLE_LEN;
376 while !title.is_char_boundary(truncated_len) {
377 truncated_len -= 1;
378 }
379 title.truncate(truncated_len);
380 title.push('…');
381 }
382
383 let mut style = if is_active {
384 theme.workspace.active_tab.clone()
385 } else {
386 theme.workspace.tab.clone()
387 };
388 if ix == 0 {
389 style.container.border.left = false;
390 }
391
392 EventHandler::new(
393 Container::new(
394 Flex::row()
395 .with_child(
396 Align::new({
397 let diameter = 7.0;
398 let icon_color = if item_view.has_conflict(cx) {
399 Some(style.icon_conflict)
400 } else if item_view.is_dirty(cx) {
401 Some(style.icon_dirty)
402 } else {
403 None
404 };
405
406 ConstrainedBox::new(
407 Canvas::new(move |bounds, _, cx| {
408 if let Some(color) = icon_color {
409 let square = RectF::new(
410 bounds.origin(),
411 vec2f(diameter, diameter),
412 );
413 cx.scene.push_quad(Quad {
414 bounds: square,
415 background: Some(color),
416 border: Default::default(),
417 corner_radius: diameter / 2.,
418 });
419 }
420 })
421 .boxed(),
422 )
423 .with_width(diameter)
424 .with_height(diameter)
425 .boxed()
426 })
427 .boxed(),
428 )
429 .with_child(
430 Container::new(
431 Align::new(
432 Label::new(
433 title,
434 if is_active {
435 theme.workspace.active_tab.label.clone()
436 } else {
437 theme.workspace.tab.label.clone()
438 },
439 )
440 .boxed(),
441 )
442 .boxed(),
443 )
444 .with_style(ContainerStyle {
445 margin: Margin {
446 left: style.spacing,
447 right: style.spacing,
448 ..Default::default()
449 },
450 ..Default::default()
451 })
452 .boxed(),
453 )
454 .with_child(
455 Align::new(
456 ConstrainedBox::new(if mouse_state.hovered {
457 let item_id = item_view.id();
458 enum TabCloseButton {}
459 let icon = Svg::new("icons/x.svg");
460 MouseEventHandler::new::<TabCloseButton, _, _, _>(
461 item_id,
462 cx,
463 |mouse_state, _| {
464 if mouse_state.hovered {
465 icon.with_color(style.icon_close_active)
466 .boxed()
467 } else {
468 icon.with_color(style.icon_close).boxed()
469 }
470 },
471 )
472 .with_padding(Padding::uniform(4.))
473 .with_cursor_style(CursorStyle::PointingHand)
474 .on_click(move |cx| {
475 cx.dispatch_action(CloseItem(item_id))
476 })
477 .named("close-tab-icon")
478 } else {
479 Empty::new().boxed()
480 })
481 .with_width(style.icon_width)
482 .boxed(),
483 )
484 .boxed(),
485 )
486 .boxed(),
487 )
488 .with_style(style.container)
489 .boxed(),
490 )
491 .on_mouse_down(move |cx| {
492 cx.dispatch_action(ActivateItem(ix));
493 true
494 })
495 .boxed()
496 })
497 }
498
499 row.add_child(
500 Empty::new()
501 .contained()
502 .with_border(theme.workspace.tab.container.border)
503 .flexible(0., true)
504 .named("filler"),
505 );
506
507 row.boxed()
508 });
509
510 ConstrainedBox::new(tabs.boxed())
511 .with_height(theme.workspace.tab.height)
512 .named("tabs")
513 }
514}
515
516impl Entity for Pane {
517 type Event = Event;
518}
519
520impl View for Pane {
521 fn ui_name() -> &'static str {
522 "Pane"
523 }
524
525 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
526 if let Some(active_item) = self.active_item() {
527 Flex::column()
528 .with_child(self.render_tabs(cx))
529 .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
530 .named("pane")
531 } else {
532 Empty::new().named("pane")
533 }
534 }
535
536 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
537 self.focus_active_item(cx);
538 }
539}
540
541impl NavHistory {
542 pub fn pop_backward(&self) -> Option<NavigationEntry> {
543 self.0.borrow_mut().backward_stack.pop_back()
544 }
545
546 pub fn pop_forward(&self) -> Option<NavigationEntry> {
547 self.0.borrow_mut().forward_stack.pop_back()
548 }
549
550 fn pop(&self, mode: NavigationMode) -> Option<NavigationEntry> {
551 match mode {
552 NavigationMode::Normal => None,
553 NavigationMode::GoingBack => self.pop_backward(),
554 NavigationMode::GoingForward => self.pop_forward(),
555 }
556 }
557
558 fn set_mode(&self, mode: NavigationMode) {
559 self.0.borrow_mut().mode = mode;
560 }
561
562 pub fn push<D: 'static + Any, T: ItemView>(&self, data: Option<D>, cx: &mut ViewContext<T>) {
563 let mut state = self.0.borrow_mut();
564 match state.mode {
565 NavigationMode::Normal => {
566 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
567 state.backward_stack.pop_front();
568 }
569 state.backward_stack.push_back(NavigationEntry {
570 item_view: Box::new(cx.weak_handle()),
571 data: data.map(|data| Box::new(data) as Box<dyn Any>),
572 });
573 state.forward_stack.clear();
574 }
575 NavigationMode::GoingBack => {
576 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
577 state.forward_stack.pop_front();
578 }
579 state.forward_stack.push_back(NavigationEntry {
580 item_view: Box::new(cx.weak_handle()),
581 data: data.map(|data| Box::new(data) as Box<dyn Any>),
582 });
583 }
584 NavigationMode::GoingForward => {
585 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
586 state.backward_stack.pop_front();
587 }
588 state.backward_stack.push_back(NavigationEntry {
589 item_view: Box::new(cx.weak_handle()),
590 data: data.map(|data| Box::new(data) as Box<dyn Any>),
591 });
592 }
593 }
594 }
595}