pane.rs

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