1use super::{ItemHandle, SplitDirection};
2use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
3use anyhow::Result;
4use collections::{HashMap, HashSet, VecDeque};
5use futures::StreamExt;
6use gpui::{
7 actions,
8 elements::*,
9 geometry::{rect::RectF, vector::vec2f},
10 impl_actions, impl_internal_actions,
11 platform::{CursorStyle, NavigationDirection},
12 AppContext, AsyncAppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad,
13 RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
14};
15use project::{Project, ProjectEntryId, ProjectPath};
16use serde::Deserialize;
17use settings::Settings;
18use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
19use util::ResultExt;
20
21actions!(
22 pane,
23 [
24 ActivatePrevItem,
25 ActivateNextItem,
26 CloseActiveItem,
27 CloseInactiveItems,
28 SplitLeft,
29 SplitUp,
30 SplitRight,
31 SplitDown,
32 ]
33);
34
35#[derive(Clone, PartialEq)]
36pub struct CloseItem {
37 pub item_id: usize,
38 pub pane: WeakViewHandle<Pane>,
39}
40
41#[derive(Clone, Deserialize, PartialEq)]
42pub struct ActivateItem(pub usize);
43
44#[derive(Clone, Deserialize, PartialEq)]
45pub struct GoBack {
46 #[serde(skip_deserializing)]
47 pub pane: Option<WeakViewHandle<Pane>>,
48}
49
50#[derive(Clone, Deserialize, PartialEq)]
51pub struct GoForward {
52 #[serde(skip_deserializing)]
53 pub pane: Option<WeakViewHandle<Pane>>,
54}
55
56impl_actions!(pane, [GoBack, GoForward]);
57impl_internal_actions!(pane, [CloseItem, ActivateItem]);
58
59const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
60
61pub fn init(cx: &mut MutableAppContext) {
62 cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
63 pane.activate_item(action.0, true, true, cx);
64 });
65 cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
66 pane.activate_prev_item(cx);
67 });
68 cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
69 pane.activate_next_item(cx);
70 });
71 cx.add_async_action(Pane::close_active_item);
72 cx.add_async_action(Pane::close_inactive_items);
73 cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
74 let pane = action.pane.upgrade(cx)?;
75 let task = Pane::close_item(workspace, pane, action.item_id, cx);
76 Some(cx.foreground().spawn(async move {
77 task.await?;
78 Ok(())
79 }))
80 });
81 cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
82 cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
83 cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
84 cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
85 cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
86 Pane::go_back(
87 workspace,
88 action
89 .pane
90 .as_ref()
91 .and_then(|weak_handle| weak_handle.upgrade(cx)),
92 cx,
93 )
94 .detach();
95 });
96 cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
97 Pane::go_forward(
98 workspace,
99 action
100 .pane
101 .as_ref()
102 .and_then(|weak_handle| weak_handle.upgrade(cx)),
103 cx,
104 )
105 .detach();
106 });
107}
108
109pub enum Event {
110 Activate,
111 ActivateItem { local: bool },
112 Remove,
113 Split(SplitDirection),
114 ChangeItemTitle,
115}
116
117pub struct Pane {
118 items: Vec<Box<dyn ItemHandle>>,
119 active_item_index: usize,
120 autoscroll: bool,
121 nav_history: Rc<RefCell<NavHistory>>,
122 toolbar: ViewHandle<Toolbar>,
123}
124
125pub struct ItemNavHistory {
126 history: Rc<RefCell<NavHistory>>,
127 item: Rc<dyn WeakItemHandle>,
128}
129
130#[derive(Default)]
131pub struct NavHistory {
132 mode: NavigationMode,
133 backward_stack: VecDeque<NavigationEntry>,
134 forward_stack: VecDeque<NavigationEntry>,
135 paths_by_item: HashMap<usize, ProjectPath>,
136}
137
138#[derive(Copy, Clone)]
139enum NavigationMode {
140 Normal,
141 GoingBack,
142 GoingForward,
143 Disabled,
144}
145
146impl Default for NavigationMode {
147 fn default() -> Self {
148 Self::Normal
149 }
150}
151
152pub struct NavigationEntry {
153 pub item: Rc<dyn WeakItemHandle>,
154 pub data: Option<Box<dyn Any>>,
155}
156
157impl Pane {
158 pub fn new(cx: &mut ViewContext<Self>) -> Self {
159 Self {
160 items: Vec::new(),
161 active_item_index: 0,
162 autoscroll: false,
163 nav_history: Default::default(),
164 toolbar: cx.add_view(|_| Toolbar::new()),
165 }
166 }
167
168 pub fn nav_history(&self) -> &Rc<RefCell<NavHistory>> {
169 &self.nav_history
170 }
171
172 pub fn activate(&self, cx: &mut ViewContext<Self>) {
173 cx.emit(Event::Activate);
174 }
175
176 pub fn go_back(
177 workspace: &mut Workspace,
178 pane: Option<ViewHandle<Pane>>,
179 cx: &mut ViewContext<Workspace>,
180 ) -> Task<()> {
181 Self::navigate_history(
182 workspace,
183 pane.unwrap_or_else(|| workspace.active_pane().clone()),
184 NavigationMode::GoingBack,
185 cx,
186 )
187 }
188
189 pub fn go_forward(
190 workspace: &mut Workspace,
191 pane: Option<ViewHandle<Pane>>,
192 cx: &mut ViewContext<Workspace>,
193 ) -> Task<()> {
194 Self::navigate_history(
195 workspace,
196 pane.unwrap_or_else(|| workspace.active_pane().clone()),
197 NavigationMode::GoingForward,
198 cx,
199 )
200 }
201
202 fn navigate_history(
203 workspace: &mut Workspace,
204 pane: ViewHandle<Pane>,
205 mode: NavigationMode,
206 cx: &mut ViewContext<Workspace>,
207 ) -> Task<()> {
208 workspace.activate_pane(pane.clone(), cx);
209
210 let to_load = pane.update(cx, |pane, cx| {
211 loop {
212 // Retrieve the weak item handle from the history.
213 let entry = pane.nav_history.borrow_mut().pop(mode)?;
214
215 // If the item is still present in this pane, then activate it.
216 if let Some(index) = entry
217 .item
218 .upgrade(cx)
219 .and_then(|v| pane.index_for_item(v.as_ref()))
220 {
221 let prev_active_item_index = pane.active_item_index;
222 pane.nav_history.borrow_mut().set_mode(mode);
223 pane.activate_item(index, true, true, cx);
224 pane.nav_history
225 .borrow_mut()
226 .set_mode(NavigationMode::Normal);
227
228 let mut navigated = prev_active_item_index != pane.active_item_index;
229 if let Some(data) = entry.data {
230 navigated |= pane.active_item()?.navigate(data, cx);
231 }
232
233 if navigated {
234 break None;
235 }
236 }
237 // If the item is no longer present in this pane, then retrieve its
238 // project path in order to reopen it.
239 else {
240 break pane
241 .nav_history
242 .borrow_mut()
243 .paths_by_item
244 .get(&entry.item.id())
245 .cloned()
246 .map(|project_path| (project_path, entry));
247 }
248 }
249 });
250
251 if let Some((project_path, entry)) = to_load {
252 // If the item was no longer present, then load it again from its previous path.
253 let pane = pane.downgrade();
254 let task = workspace.load_path(project_path, cx);
255 cx.spawn(|workspace, mut cx| async move {
256 let task = task.await;
257 if let Some(pane) = pane.upgrade(&cx) {
258 if let Some((project_entry_id, build_item)) = task.log_err() {
259 pane.update(&mut cx, |pane, _| {
260 pane.nav_history.borrow_mut().set_mode(mode);
261 });
262 let item = workspace.update(&mut cx, |workspace, cx| {
263 Self::open_item(
264 workspace,
265 pane.clone(),
266 project_entry_id,
267 true,
268 cx,
269 build_item,
270 )
271 });
272 pane.update(&mut cx, |pane, cx| {
273 pane.nav_history
274 .borrow_mut()
275 .set_mode(NavigationMode::Normal);
276 if let Some(data) = entry.data {
277 item.navigate(data, cx);
278 }
279 });
280 } else {
281 workspace
282 .update(&mut cx, |workspace, cx| {
283 Self::navigate_history(workspace, pane, mode, cx)
284 })
285 .await;
286 }
287 }
288 })
289 } else {
290 Task::ready(())
291 }
292 }
293
294 pub(crate) fn open_item(
295 workspace: &mut Workspace,
296 pane: ViewHandle<Pane>,
297 project_entry_id: ProjectEntryId,
298 focus_item: bool,
299 cx: &mut ViewContext<Workspace>,
300 build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
301 ) -> Box<dyn ItemHandle> {
302 let existing_item = pane.update(cx, |pane, cx| {
303 for (ix, item) in pane.items.iter().enumerate() {
304 if item.project_path(cx).is_some()
305 && item.project_entry_ids(cx).as_slice() == &[project_entry_id]
306 {
307 let item = item.boxed_clone();
308 pane.activate_item(ix, true, focus_item, cx);
309 return Some(item);
310 }
311 }
312 None
313 });
314 if let Some(existing_item) = existing_item {
315 existing_item
316 } else {
317 let item = build_item(cx);
318 Self::add_item(workspace, pane, item.boxed_clone(), true, focus_item, cx);
319 item
320 }
321 }
322
323 pub(crate) fn add_item(
324 workspace: &mut Workspace,
325 pane: ViewHandle<Pane>,
326 item: Box<dyn ItemHandle>,
327 activate_pane: bool,
328 focus_item: bool,
329 cx: &mut ViewContext<Workspace>,
330 ) {
331 // Prevent adding the same item to the pane more than once.
332 if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
333 pane.update(cx, |pane, cx| {
334 pane.activate_item(item_ix, activate_pane, focus_item, cx)
335 });
336 return;
337 }
338
339 item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
340 item.added_to_pane(workspace, pane.clone(), cx);
341 pane.update(cx, |pane, cx| {
342 // If there is already an active item, then insert the new item
343 // right after it. Otherwise, adjust the `active_item_index` field
344 // before activating the new item, so that in the `activate_item`
345 // method, we can detect that the active item is changing.
346 let item_ix;
347 if pane.active_item_index < pane.items.len() {
348 item_ix = pane.active_item_index + 1
349 } else {
350 item_ix = pane.items.len();
351 pane.active_item_index = usize::MAX;
352 };
353
354 pane.items.insert(item_ix, item);
355 pane.activate_item(item_ix, activate_pane, focus_item, cx);
356 cx.notify();
357 });
358 }
359
360 pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
361 self.items.iter()
362 }
363
364 pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
365 self.items
366 .iter()
367 .filter_map(|item| item.to_any().downcast())
368 }
369
370 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
371 self.items.get(self.active_item_index).cloned()
372 }
373
374 pub fn item_for_entry(
375 &self,
376 entry_id: ProjectEntryId,
377 cx: &AppContext,
378 ) -> Option<Box<dyn ItemHandle>> {
379 self.items.iter().find_map(|item| {
380 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == &[entry_id] {
381 Some(item.boxed_clone())
382 } else {
383 None
384 }
385 })
386 }
387
388 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
389 self.items.iter().position(|i| i.id() == item.id())
390 }
391
392 pub fn activate_item(
393 &mut self,
394 index: usize,
395 activate_pane: bool,
396 focus_item: bool,
397 cx: &mut ViewContext<Self>,
398 ) {
399 use NavigationMode::{GoingBack, GoingForward};
400 if index < self.items.len() {
401 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
402 if prev_active_item_ix != self.active_item_index
403 || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
404 {
405 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
406 prev_item.deactivated(cx);
407 }
408 cx.emit(Event::ActivateItem {
409 local: activate_pane,
410 });
411 }
412 self.update_toolbar(cx);
413 if focus_item {
414 self.focus_active_item(cx);
415 }
416 if activate_pane {
417 self.activate(cx);
418 }
419 self.autoscroll = true;
420 cx.notify();
421 }
422 }
423
424 pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
425 let mut index = self.active_item_index;
426 if index > 0 {
427 index -= 1;
428 } else if self.items.len() > 0 {
429 index = self.items.len() - 1;
430 }
431 self.activate_item(index, true, true, cx);
432 }
433
434 pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
435 let mut index = self.active_item_index;
436 if index + 1 < self.items.len() {
437 index += 1;
438 } else {
439 index = 0;
440 }
441 self.activate_item(index, true, true, cx);
442 }
443
444 pub fn close_active_item(
445 workspace: &mut Workspace,
446 _: &CloseActiveItem,
447 cx: &mut ViewContext<Workspace>,
448 ) -> Option<Task<Result<()>>> {
449 let pane_handle = workspace.active_pane().clone();
450 let pane = pane_handle.read(cx);
451 if pane.items.is_empty() {
452 None
453 } else {
454 let item_id_to_close = pane.items[pane.active_item_index].id();
455 let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
456 item_id == item_id_to_close
457 });
458 Some(cx.foreground().spawn(async move {
459 task.await?;
460 Ok(())
461 }))
462 }
463 }
464
465 pub fn close_inactive_items(
466 workspace: &mut Workspace,
467 _: &CloseInactiveItems,
468 cx: &mut ViewContext<Workspace>,
469 ) -> Option<Task<Result<()>>> {
470 let pane_handle = workspace.active_pane().clone();
471 let pane = pane_handle.read(cx);
472 if pane.items.is_empty() {
473 None
474 } else {
475 let active_item_id = pane.items[pane.active_item_index].id();
476 let task =
477 Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
478 Some(cx.foreground().spawn(async move {
479 task.await?;
480 Ok(())
481 }))
482 }
483 }
484
485 pub fn close_item(
486 workspace: &mut Workspace,
487 pane: ViewHandle<Pane>,
488 item_id_to_close: usize,
489 cx: &mut ViewContext<Workspace>,
490 ) -> Task<Result<bool>> {
491 Self::close_items(workspace, pane, cx, move |view_id| {
492 view_id == item_id_to_close
493 })
494 }
495
496 pub fn close_items(
497 workspace: &mut Workspace,
498 pane: ViewHandle<Pane>,
499 cx: &mut ViewContext<Workspace>,
500 should_close: impl 'static + Fn(usize) -> bool,
501 ) -> Task<Result<bool>> {
502 let project = workspace.project().clone();
503
504 // Find the items to close.
505 let mut items_to_close = Vec::new();
506 for item in &pane.read(cx).items {
507 if should_close(item.id()) {
508 items_to_close.push(item.boxed_clone());
509 }
510 }
511
512 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
513 // to focus the singleton buffer when prompting to save that buffer, as opposed
514 // to focusing the multibuffer, because this gives the user a more clear idea
515 // of what content they would be saving.
516 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
517
518 cx.spawn(|workspace, mut cx| async move {
519 let mut saved_project_entry_ids = HashSet::default();
520 for item in items_to_close.clone() {
521 // Find the item's current index and its set of project entries. Avoid
522 // storing these in advance, in case they have changed since this task
523 // was started.
524 let (item_ix, mut project_entry_ids) = pane.read_with(&cx, |pane, cx| {
525 (pane.index_for_item(&*item), item.project_entry_ids(cx))
526 });
527 let item_ix = if let Some(ix) = item_ix {
528 ix
529 } else {
530 continue;
531 };
532
533 // If an item hasn't yet been associated with a project entry, then always
534 // prompt to save it before closing it. Otherwise, check if the item has
535 // any project entries that are not open anywhere else in the workspace,
536 // AND that the user has not already been prompted to save. If there are
537 // any such project entries, prompt the user to save this item.
538 let should_save = if project_entry_ids.is_empty() {
539 true
540 } else {
541 workspace.read_with(&cx, |workspace, cx| {
542 for item in workspace.items(cx) {
543 if !items_to_close
544 .iter()
545 .any(|item_to_close| item_to_close.id() == item.id())
546 {
547 let other_project_entry_ids = item.project_entry_ids(cx);
548 project_entry_ids
549 .retain(|id| !other_project_entry_ids.contains(&id));
550 }
551 }
552 });
553 project_entry_ids
554 .iter()
555 .any(|id| saved_project_entry_ids.insert(*id))
556 };
557
558 if should_save {
559 if !Self::save_item(project.clone(), &pane, item_ix, &item, true, &mut cx)
560 .await?
561 {
562 break;
563 }
564 }
565
566 // Remove the item from the pane.
567 pane.update(&mut cx, |pane, cx| {
568 if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
569 if item_ix == pane.active_item_index {
570 if item_ix + 1 < pane.items.len() {
571 pane.activate_next_item(cx);
572 } else if item_ix > 0 {
573 pane.activate_prev_item(cx);
574 }
575 }
576
577 let item = pane.items.remove(item_ix);
578 if pane.items.is_empty() {
579 item.deactivated(cx);
580 pane.update_toolbar(cx);
581 cx.emit(Event::Remove);
582 }
583
584 if item_ix < pane.active_item_index {
585 pane.active_item_index -= 1;
586 }
587
588 let mut nav_history = pane.nav_history.borrow_mut();
589 if let Some(path) = item.project_path(cx) {
590 nav_history.paths_by_item.insert(item.id(), path);
591 } else {
592 nav_history.paths_by_item.remove(&item.id());
593 }
594 }
595 });
596 }
597
598 pane.update(&mut cx, |_, cx| cx.notify());
599 Ok(true)
600 })
601 }
602
603 pub async fn save_item(
604 project: ModelHandle<Project>,
605 pane: &ViewHandle<Pane>,
606 item_ix: usize,
607 item: &Box<dyn ItemHandle>,
608 should_prompt_for_save: bool,
609 cx: &mut AsyncAppContext,
610 ) -> Result<bool> {
611 const CONFLICT_MESSAGE: &'static str =
612 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
613 const DIRTY_MESSAGE: &'static str =
614 "This file contains unsaved edits. Do you want to save it?";
615
616 let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
617 (
618 item.has_conflict(cx),
619 item.is_dirty(cx),
620 item.can_save(cx),
621 item.is_singleton(cx),
622 )
623 });
624
625 if has_conflict && can_save {
626 let mut answer = pane.update(cx, |pane, cx| {
627 pane.activate_item(item_ix, true, true, cx);
628 cx.prompt(
629 PromptLevel::Warning,
630 CONFLICT_MESSAGE,
631 &["Overwrite", "Discard", "Cancel"],
632 )
633 });
634 match answer.next().await {
635 Some(0) => cx.update(|cx| item.save(project, cx)).await?,
636 Some(1) => cx.update(|cx| item.reload(project, cx)).await?,
637 _ => return Ok(false),
638 }
639 } else if is_dirty && (can_save || is_singleton) {
640 let should_save = if should_prompt_for_save {
641 let mut answer = pane.update(cx, |pane, cx| {
642 pane.activate_item(item_ix, true, true, cx);
643 cx.prompt(
644 PromptLevel::Warning,
645 DIRTY_MESSAGE,
646 &["Save", "Don't Save", "Cancel"],
647 )
648 });
649 match answer.next().await {
650 Some(0) => true,
651 Some(1) => false,
652 _ => return Ok(false),
653 }
654 } else {
655 true
656 };
657
658 if should_save {
659 if can_save {
660 cx.update(|cx| item.save(project, cx)).await?;
661 } else if is_singleton {
662 let start_abs_path = project
663 .read_with(cx, |project, cx| {
664 let worktree = project.visible_worktrees(cx).next()?;
665 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
666 })
667 .unwrap_or(Path::new("").into());
668
669 let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
670 if let Some(abs_path) = abs_path.next().await.flatten() {
671 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
672 } else {
673 return Ok(false);
674 }
675 }
676 }
677 }
678 Ok(true)
679 }
680
681 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
682 if let Some(active_item) = self.active_item() {
683 cx.focus(active_item);
684 }
685 }
686
687 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
688 cx.emit(Event::Split(direction));
689 }
690
691 pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
692 &self.toolbar
693 }
694
695 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
696 let active_item = self
697 .items
698 .get(self.active_item_index)
699 .map(|item| item.as_ref());
700 self.toolbar.update(cx, |toolbar, cx| {
701 toolbar.set_active_pane_item(active_item, cx);
702 });
703 }
704
705 fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
706 let theme = cx.global::<Settings>().theme.clone();
707
708 enum Tabs {}
709 enum Tab {}
710 let pane = cx.handle();
711 let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
712 let autoscroll = if mem::take(&mut self.autoscroll) {
713 Some(self.active_item_index)
714 } else {
715 None
716 };
717 let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
718 for (ix, item) in self.items.iter().enumerate() {
719 let is_active = ix == self.active_item_index;
720
721 row.add_child({
722 let tab_style = if is_active {
723 theme.workspace.active_tab.clone()
724 } else {
725 theme.workspace.tab.clone()
726 };
727 let title = item.tab_content(&tab_style, cx);
728
729 let mut style = if is_active {
730 theme.workspace.active_tab.clone()
731 } else {
732 theme.workspace.tab.clone()
733 };
734 if ix == 0 {
735 style.container.border.left = false;
736 }
737
738 MouseEventHandler::new::<Tab, _, _>(ix, cx, |_, cx| {
739 Container::new(
740 Flex::row()
741 .with_child(
742 Align::new({
743 let diameter = 7.0;
744 let icon_color = if item.has_conflict(cx) {
745 Some(style.icon_conflict)
746 } else if item.is_dirty(cx) {
747 Some(style.icon_dirty)
748 } else {
749 None
750 };
751
752 ConstrainedBox::new(
753 Canvas::new(move |bounds, _, cx| {
754 if let Some(color) = icon_color {
755 let square = RectF::new(
756 bounds.origin(),
757 vec2f(diameter, diameter),
758 );
759 cx.scene.push_quad(Quad {
760 bounds: square,
761 background: Some(color),
762 border: Default::default(),
763 corner_radius: diameter / 2.,
764 });
765 }
766 })
767 .boxed(),
768 )
769 .with_width(diameter)
770 .with_height(diameter)
771 .boxed()
772 })
773 .boxed(),
774 )
775 .with_child(
776 Container::new(Align::new(title).boxed())
777 .with_style(ContainerStyle {
778 margin: Margin {
779 left: style.spacing,
780 right: style.spacing,
781 ..Default::default()
782 },
783 ..Default::default()
784 })
785 .boxed(),
786 )
787 .with_child(
788 Align::new(
789 ConstrainedBox::new(if mouse_state.hovered {
790 let item_id = item.id();
791 enum TabCloseButton {}
792 let icon = Svg::new("icons/x.svg");
793 MouseEventHandler::new::<TabCloseButton, _, _>(
794 item_id,
795 cx,
796 |mouse_state, _| {
797 if mouse_state.hovered {
798 icon.with_color(style.icon_close_active)
799 .boxed()
800 } else {
801 icon.with_color(style.icon_close).boxed()
802 }
803 },
804 )
805 .with_padding(Padding::uniform(4.))
806 .with_cursor_style(CursorStyle::PointingHand)
807 .on_click({
808 let pane = pane.clone();
809 move |_, _, cx| {
810 cx.dispatch_action(CloseItem {
811 item_id,
812 pane: pane.clone(),
813 })
814 }
815 })
816 .named("close-tab-icon")
817 } else {
818 Empty::new().boxed()
819 })
820 .with_width(style.icon_width)
821 .boxed(),
822 )
823 .boxed(),
824 )
825 .boxed(),
826 )
827 .with_style(style.container)
828 .boxed()
829 })
830 .on_mouse_down(move |_, cx| {
831 cx.dispatch_action(ActivateItem(ix));
832 })
833 .boxed()
834 })
835 }
836
837 row.add_child(
838 Empty::new()
839 .contained()
840 .with_border(theme.workspace.tab.container.border)
841 .flex(0., true)
842 .named("filler"),
843 );
844
845 row.boxed()
846 });
847
848 ConstrainedBox::new(tabs.boxed())
849 .with_height(theme.workspace.tab.height)
850 .named("tabs")
851 }
852}
853
854impl Entity for Pane {
855 type Event = Event;
856}
857
858impl View for Pane {
859 fn ui_name() -> &'static str {
860 "Pane"
861 }
862
863 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
864 let this = cx.handle();
865
866 EventHandler::new(if let Some(active_item) = self.active_item() {
867 Flex::column()
868 .with_child(self.render_tabs(cx))
869 .with_child(ChildView::new(&self.toolbar).boxed())
870 .with_child(ChildView::new(active_item).flex(1., true).boxed())
871 .boxed()
872 } else {
873 Empty::new().boxed()
874 })
875 .on_navigate_mouse_down(move |direction, cx| {
876 let this = this.clone();
877 match direction {
878 NavigationDirection::Back => cx.dispatch_action(GoBack { pane: Some(this) }),
879 NavigationDirection::Forward => cx.dispatch_action(GoForward { pane: Some(this) }),
880 }
881
882 true
883 })
884 .named("pane")
885 }
886
887 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
888 self.focus_active_item(cx);
889 }
890}
891
892impl ItemNavHistory {
893 pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
894 Self {
895 history,
896 item: Rc::new(item.downgrade()),
897 }
898 }
899
900 pub fn history(&self) -> Rc<RefCell<NavHistory>> {
901 self.history.clone()
902 }
903
904 pub fn push<D: 'static + Any>(&self, data: Option<D>) {
905 self.history.borrow_mut().push(data, self.item.clone());
906 }
907}
908
909impl NavHistory {
910 pub fn disable(&mut self) {
911 self.mode = NavigationMode::Disabled;
912 }
913
914 pub fn enable(&mut self) {
915 self.mode = NavigationMode::Normal;
916 }
917
918 pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
919 self.backward_stack.pop_back()
920 }
921
922 pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
923 self.forward_stack.pop_back()
924 }
925
926 fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
927 match mode {
928 NavigationMode::Normal | NavigationMode::Disabled => None,
929 NavigationMode::GoingBack => self.pop_backward(),
930 NavigationMode::GoingForward => self.pop_forward(),
931 }
932 }
933
934 fn set_mode(&mut self, mode: NavigationMode) {
935 self.mode = mode;
936 }
937
938 pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
939 match self.mode {
940 NavigationMode::Disabled => {}
941 NavigationMode::Normal => {
942 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
943 self.backward_stack.pop_front();
944 }
945 self.backward_stack.push_back(NavigationEntry {
946 item,
947 data: data.map(|data| Box::new(data) as Box<dyn Any>),
948 });
949 self.forward_stack.clear();
950 }
951 NavigationMode::GoingBack => {
952 if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
953 self.forward_stack.pop_front();
954 }
955 self.forward_stack.push_back(NavigationEntry {
956 item,
957 data: data.map(|data| Box::new(data) as Box<dyn Any>),
958 });
959 }
960 NavigationMode::GoingForward => {
961 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
962 self.backward_stack.pop_front();
963 }
964 self.backward_stack.push_back(NavigationEntry {
965 item,
966 data: data.map(|data| Box::new(data) as Box<dyn Any>),
967 });
968 }
969 }
970 }
971}