1use super::{ItemViewHandle, SplitDirection};
2use crate::{ItemHandle, Settings, Workspace};
3use gpui::{
4 action,
5 elements::*,
6 geometry::{rect::RectF, vector::vec2f},
7 keymap::Binding,
8 platform::CursorStyle,
9 Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
10};
11use postage::watch;
12use std::cmp;
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
58const MAX_TAB_TITLE_LEN: usize = 24;
59
60#[derive(Debug, Eq, PartialEq)]
61pub struct State {
62 pub tabs: Vec<TabState>,
63}
64
65#[derive(Debug, Eq, PartialEq)]
66pub struct TabState {
67 pub title: String,
68 pub active: bool,
69}
70
71pub struct Pane {
72 item_views: Vec<Box<dyn ItemViewHandle>>,
73 active_item: usize,
74 settings: watch::Receiver<Settings>,
75}
76
77impl Pane {
78 pub fn new(settings: watch::Receiver<Settings>) -> Self {
79 Self {
80 item_views: Vec::new(),
81 active_item: 0,
82 settings,
83 }
84 }
85
86 pub fn activate(&self, cx: &mut ViewContext<Self>) {
87 cx.emit(Event::Activate);
88 }
89
90 pub fn open_item<T>(
91 &mut self,
92 item_handle: T,
93 workspace: &Workspace,
94 cx: &mut ViewContext<Self>,
95 ) -> Box<dyn ItemViewHandle>
96 where
97 T: 'static + ItemHandle,
98 {
99 for (ix, item_view) in self.item_views.iter().enumerate() {
100 if item_view.item_handle(cx).id() == item_handle.id() {
101 let item_view = item_view.boxed_clone();
102 self.activate_item(ix, cx);
103 return item_view;
104 }
105 }
106
107 let item_view = item_handle.add_view(cx.window_id(), workspace, cx);
108 self.add_item_view(item_view.boxed_clone(), cx);
109 item_view
110 }
111
112 pub fn add_item_view(
113 &mut self,
114 item_view: Box<dyn ItemViewHandle>,
115 cx: &mut ViewContext<Self>,
116 ) {
117 item_view.added_to_pane(cx);
118 let item_idx = cmp::min(self.active_item + 1, self.item_views.len());
119 self.item_views.insert(item_idx, item_view);
120 self.activate_item(item_idx, cx);
121 cx.notify();
122 }
123
124 pub fn item_views(&self) -> &[Box<dyn ItemViewHandle>] {
125 &self.item_views
126 }
127
128 pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
129 self.item_views.get(self.active_item).cloned()
130 }
131
132 pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
133 self.item_views.iter().position(|i| i.id() == item.id())
134 }
135
136 pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
137 if index < self.item_views.len() {
138 self.active_item = index;
139 self.focus_active_item(cx);
140 cx.notify();
141 }
142 }
143
144 pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
145 if self.active_item > 0 {
146 self.active_item -= 1;
147 } else if self.item_views.len() > 0 {
148 self.active_item = self.item_views.len() - 1;
149 }
150 self.focus_active_item(cx);
151 cx.notify();
152 }
153
154 pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
155 if self.active_item + 1 < self.item_views.len() {
156 self.active_item += 1;
157 } else {
158 self.active_item = 0;
159 }
160 self.focus_active_item(cx);
161 cx.notify();
162 }
163
164 pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
165 if !self.item_views.is_empty() {
166 self.close_item(self.item_views[self.active_item].id(), cx)
167 }
168 }
169
170 pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
171 self.item_views.retain(|item| item.id() != item_id);
172 self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1));
173 if self.item_views.is_empty() {
174 cx.emit(Event::Remove);
175 }
176 cx.notify();
177 }
178
179 fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
180 if let Some(active_item) = self.active_item() {
181 cx.focus(active_item.to_any());
182 }
183 }
184
185 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
186 cx.emit(Event::Split(direction));
187 }
188
189 fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
190 let settings = self.settings.borrow();
191 let theme = &settings.theme;
192
193 enum Tabs {}
194 let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
195 let mut row = Flex::row();
196 for (ix, item_view) in self.item_views.iter().enumerate() {
197 let is_active = ix == self.active_item;
198
199 row.add_child({
200 let mut title = item_view.title(cx);
201 if title.len() > MAX_TAB_TITLE_LEN {
202 let mut truncated_len = MAX_TAB_TITLE_LEN;
203 while !title.is_char_boundary(truncated_len) {
204 truncated_len -= 1;
205 }
206 title.truncate(truncated_len);
207 title.push('…');
208 }
209
210 let mut style = if is_active {
211 theme.workspace.active_tab.clone()
212 } else {
213 theme.workspace.tab.clone()
214 };
215 if ix == 0 {
216 style.container.border.left = false;
217 }
218
219 EventHandler::new(
220 Container::new(
221 Flex::row()
222 .with_child(
223 Align::new({
224 let diameter = 7.0;
225 let icon_color = if item_view.has_conflict(cx) {
226 Some(style.icon_conflict)
227 } else if item_view.is_dirty(cx) {
228 Some(style.icon_dirty)
229 } else {
230 None
231 };
232
233 ConstrainedBox::new(
234 Canvas::new(move |bounds, _, cx| {
235 if let Some(color) = icon_color {
236 let square = RectF::new(
237 bounds.origin(),
238 vec2f(diameter, diameter),
239 );
240 cx.scene.push_quad(Quad {
241 bounds: square,
242 background: Some(color),
243 border: Default::default(),
244 corner_radius: diameter / 2.,
245 });
246 }
247 })
248 .boxed(),
249 )
250 .with_width(diameter)
251 .with_height(diameter)
252 .boxed()
253 })
254 .boxed(),
255 )
256 .with_child(
257 Container::new(
258 Align::new(
259 Label::new(
260 title,
261 if is_active {
262 theme.workspace.active_tab.label.clone()
263 } else {
264 theme.workspace.tab.label.clone()
265 },
266 )
267 .boxed(),
268 )
269 .boxed(),
270 )
271 .with_style(ContainerStyle {
272 margin: Margin {
273 left: style.spacing,
274 right: style.spacing,
275 ..Default::default()
276 },
277 ..Default::default()
278 })
279 .boxed(),
280 )
281 .with_child(
282 Align::new(
283 ConstrainedBox::new(if mouse_state.hovered {
284 let item_id = item_view.id();
285 enum TabCloseButton {}
286 let icon = Svg::new("icons/x.svg");
287 MouseEventHandler::new::<TabCloseButton, _, _, _>(
288 item_id,
289 cx,
290 |mouse_state, _| {
291 if mouse_state.hovered {
292 icon.with_color(style.icon_close_active)
293 .boxed()
294 } else {
295 icon.with_color(style.icon_close).boxed()
296 }
297 },
298 )
299 .with_padding(Padding::uniform(4.))
300 .with_cursor_style(CursorStyle::PointingHand)
301 .on_click(move |cx| {
302 cx.dispatch_action(CloseItem(item_id))
303 })
304 .named("close-tab-icon")
305 } else {
306 Empty::new().boxed()
307 })
308 .with_width(style.icon_width)
309 .boxed(),
310 )
311 .boxed(),
312 )
313 .boxed(),
314 )
315 .with_style(style.container)
316 .boxed(),
317 )
318 .on_mouse_down(move |cx| {
319 cx.dispatch_action(ActivateItem(ix));
320 true
321 })
322 .boxed()
323 })
324 }
325
326 row.add_child(
327 Empty::new()
328 .contained()
329 .with_border(theme.workspace.tab.container.border)
330 .flexible(0., true)
331 .named("filler"),
332 );
333
334 row.boxed()
335 });
336
337 ConstrainedBox::new(tabs.boxed())
338 .with_height(theme.workspace.tab.height)
339 .named("tabs")
340 }
341}
342
343impl Entity for Pane {
344 type Event = Event;
345}
346
347impl View for Pane {
348 fn ui_name() -> &'static str {
349 "Pane"
350 }
351
352 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
353 if let Some(active_item) = self.active_item() {
354 Flex::column()
355 .with_child(self.render_tabs(cx))
356 .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
357 .named("pane")
358 } else {
359 Empty::new().named("pane")
360 }
361 }
362
363 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
364 self.focus_active_item(cx);
365 }
366}