1use super::{ItemViewHandle, SplitDirection};
2use crate::{settings::Settings, theme};
3use gpui::{
4 action,
5 color::Color,
6 elements::*,
7 geometry::{rect::RectF, vector::vec2f},
8 keymap::Binding,
9 Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
10};
11use postage::watch;
12use std::{cmp, path::Path, sync::Arc};
13
14action!(Split, SplitDirection);
15action!(ActivateItem, usize);
16action!(ActivatePrevItem);
17action!(ActivateNextItem);
18action!(CloseActiveItem);
19action!(CloseItem, usize);
20
21pub fn init(cx: &mut MutableAppContext) {
22 cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
23 pane.activate_item(action.0, cx);
24 });
25 cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
26 pane.activate_prev_item(cx);
27 });
28 cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
29 pane.activate_next_item(cx);
30 });
31 cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
32 pane.close_active_item(cx);
33 });
34 cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
35 pane.close_item(action.0, cx);
36 });
37 cx.add_action(|pane: &mut Pane, action: &Split, cx| {
38 pane.split(action.0, cx);
39 });
40
41 cx.add_bindings(vec![
42 Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
43 Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
44 Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
45 Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
46 Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
47 Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
48 Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
49 ]);
50}
51
52pub enum Event {
53 Activate,
54 Remove,
55 Split(SplitDirection),
56}
57
58#[derive(Debug, Eq, PartialEq)]
59pub struct State {
60 pub tabs: Vec<TabState>,
61}
62
63#[derive(Debug, Eq, PartialEq)]
64pub struct TabState {
65 pub title: String,
66 pub active: bool,
67}
68
69pub struct Pane {
70 items: Vec<Box<dyn ItemViewHandle>>,
71 active_item: usize,
72 settings: watch::Receiver<Settings>,
73}
74
75impl Pane {
76 pub fn new(settings: watch::Receiver<Settings>) -> Self {
77 Self {
78 items: Vec::new(),
79 active_item: 0,
80 settings,
81 }
82 }
83
84 pub fn activate(&self, cx: &mut ViewContext<Self>) {
85 cx.emit(Event::Activate);
86 }
87
88 pub fn add_item(&mut self, item: Box<dyn ItemViewHandle>, cx: &mut ViewContext<Self>) -> usize {
89 let item_idx = cmp::min(self.active_item + 1, self.items.len());
90 self.items.insert(item_idx, item);
91 cx.notify();
92 item_idx
93 }
94
95 #[cfg(test)]
96 pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
97 &self.items
98 }
99
100 pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
101 self.items.get(self.active_item).cloned()
102 }
103
104 pub fn activate_entry(
105 &mut self,
106 entry_id: (usize, Arc<Path>),
107 cx: &mut ViewContext<Self>,
108 ) -> bool {
109 if let Some(index) = self.items.iter().position(|item| {
110 item.entry_id(cx.as_ref())
111 .map_or(false, |id| id == entry_id)
112 }) {
113 self.activate_item(index, cx);
114 true
115 } else {
116 false
117 }
118 }
119
120 pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
121 self.items.iter().position(|i| i.id() == item.id())
122 }
123
124 pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
125 if index < self.items.len() {
126 self.active_item = index;
127 self.focus_active_item(cx);
128 cx.notify();
129 }
130 }
131
132 pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
133 if self.active_item > 0 {
134 self.active_item -= 1;
135 } else if self.items.len() > 0 {
136 self.active_item = self.items.len() - 1;
137 }
138 self.focus_active_item(cx);
139 cx.notify();
140 }
141
142 pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
143 if self.active_item + 1 < self.items.len() {
144 self.active_item += 1;
145 } else {
146 self.active_item = 0;
147 }
148 self.focus_active_item(cx);
149 cx.notify();
150 }
151
152 pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
153 if !self.items.is_empty() {
154 self.close_item(self.items[self.active_item].id(), cx)
155 }
156 }
157
158 pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
159 self.items.retain(|item| item.id() != item_id);
160 self.active_item = cmp::min(self.active_item, self.items.len().saturating_sub(1));
161 if self.items.is_empty() {
162 cx.emit(Event::Remove);
163 }
164 cx.notify();
165 }
166
167 fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
168 if let Some(active_item) = self.active_item() {
169 cx.focus(active_item.to_any());
170 }
171 }
172
173 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
174 cx.emit(Event::Split(direction));
175 }
176
177 fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
178 let settings = self.settings.borrow();
179 let theme = &settings.theme;
180 let line_height = cx.font_cache().line_height(
181 theme.workspace.tab.label.text.font_id,
182 theme.workspace.tab.label.text.font_size,
183 );
184
185 let mut row = Flex::row();
186 let last_item_ix = self.items.len() - 1;
187 for (ix, item) in self.items.iter().enumerate() {
188 let is_active = ix == self.active_item;
189
190 enum Tab {}
191 let border = &theme.workspace.tab.container.border;
192
193 row.add_child(
194 Expanded::new(
195 1.0,
196 MouseEventHandler::new::<Tab, _, _>(item.id(), cx, |mouse_state, cx| {
197 let title = item.title(cx);
198
199 let mut border = border.clone();
200 border.left = ix > 0;
201 border.right = ix == last_item_ix;
202 border.bottom = !is_active;
203
204 let mut container = Container::new(
205 Stack::new()
206 .with_child(
207 Align::new(
208 Label::new(
209 title,
210 if is_active {
211 theme.workspace.active_tab.label.clone()
212 } else {
213 theme.workspace.tab.label.clone()
214 },
215 )
216 .boxed(),
217 )
218 .boxed(),
219 )
220 .with_child(
221 Align::new(Self::render_tab_icon(
222 item.id(),
223 line_height - 2.,
224 mouse_state.hovered,
225 item.is_dirty(cx),
226 item.has_conflict(cx),
227 theme,
228 cx,
229 ))
230 .right()
231 .boxed(),
232 )
233 .boxed(),
234 )
235 .with_style(if is_active {
236 &theme.workspace.active_tab.container
237 } else {
238 &theme.workspace.tab.container
239 })
240 .with_border(border);
241
242 if is_active {
243 container = container.with_padding_bottom(border.width);
244 }
245
246 ConstrainedBox::new(
247 EventHandler::new(container.boxed())
248 .on_mouse_down(move |cx| {
249 cx.dispatch_action(ActivateItem(ix));
250 true
251 })
252 .boxed(),
253 )
254 .with_min_width(80.0)
255 .with_max_width(264.0)
256 .boxed()
257 })
258 .boxed(),
259 )
260 .named("tab"),
261 );
262 }
263
264 // Ensure there's always a minimum amount of space after the last tab,
265 // so that the tab's border doesn't abut the window's border.
266 let mut border = Border::bottom(1.0, Color::default());
267 border.color = theme.workspace.tab.container.border.color;
268
269 row.add_child(
270 ConstrainedBox::new(
271 Container::new(Empty::new().boxed())
272 .with_border(border)
273 .boxed(),
274 )
275 .with_min_width(20.)
276 .named("fixed-filler"),
277 );
278
279 row.add_child(
280 Expanded::new(
281 0.0,
282 Container::new(Empty::new().boxed())
283 .with_border(border)
284 .boxed(),
285 )
286 .named("filler"),
287 );
288
289 ConstrainedBox::new(row.boxed())
290 .with_height(line_height + 16.)
291 .named("tabs")
292 }
293
294 fn render_tab_icon(
295 item_id: usize,
296 close_icon_size: f32,
297 tab_hovered: bool,
298 is_dirty: bool,
299 has_conflict: bool,
300 theme: &theme::Theme,
301 cx: &mut RenderContext<Self>,
302 ) -> ElementBox {
303 enum TabCloseButton {}
304
305 let mut clicked_color = theme.workspace.tab.icon_dirty;
306 clicked_color.a = 180;
307
308 let current_color = if has_conflict {
309 Some(theme.workspace.tab.icon_conflict)
310 } else if is_dirty {
311 Some(theme.workspace.tab.icon_dirty)
312 } else {
313 None
314 };
315
316 let icon = if tab_hovered {
317 let close_color = current_color.unwrap_or(theme.workspace.tab.icon_close);
318 let icon = Svg::new("icons/x.svg").with_color(close_color);
319
320 MouseEventHandler::new::<TabCloseButton, _, _>(item_id, cx, |mouse_state, _| {
321 if mouse_state.hovered {
322 Container::new(icon.with_color(Color::white()).boxed())
323 .with_background_color(if mouse_state.clicked {
324 clicked_color
325 } else {
326 theme.workspace.tab.icon_dirty
327 })
328 .with_corner_radius(close_icon_size / 2.)
329 .boxed()
330 } else {
331 icon.boxed()
332 }
333 })
334 .on_click(move |cx| cx.dispatch_action(CloseItem(item_id)))
335 .named("close-tab-icon")
336 } else {
337 let diameter = 8.;
338 ConstrainedBox::new(
339 Canvas::new(move |bounds, cx| {
340 if let Some(current_color) = current_color {
341 let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
342 cx.scene.push_quad(Quad {
343 bounds: square,
344 background: Some(current_color),
345 border: Default::default(),
346 corner_radius: diameter / 2.,
347 });
348 }
349 })
350 .boxed(),
351 )
352 .with_width(diameter)
353 .with_height(diameter)
354 .named("unsaved-tab-icon")
355 };
356
357 ConstrainedBox::new(Align::new(icon).boxed())
358 .with_width(close_icon_size)
359 .named("tab-icon")
360 }
361}
362
363impl Entity for Pane {
364 type Event = Event;
365}
366
367impl View for Pane {
368 fn ui_name() -> &'static str {
369 "Pane"
370 }
371
372 fn render(&self, cx: &mut RenderContext<Self>) -> ElementBox {
373 if let Some(active_item) = self.active_item() {
374 Flex::column()
375 .with_child(self.render_tabs(cx))
376 .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
377 .named("pane")
378 } else {
379 Empty::new().named("pane")
380 }
381 }
382
383 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
384 self.focus_active_item(cx);
385 }
386}
387
388pub trait PaneHandle {
389 fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext);
390}
391
392impl PaneHandle for ViewHandle<Pane> {
393 fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext) {
394 item.set_parent_pane(self, cx);
395 self.update(cx, |pane, cx| {
396 let item_idx = pane.add_item(item, cx);
397 pane.activate_item(item_idx, cx);
398 });
399 }
400}