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<(usize, 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_id, item_view)) in self.item_views.iter().enumerate() {
100 if *item_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
120 .insert(item_idx, (item_view.item_handle(cx).id(), item_view));
121 self.activate_item(item_idx, cx);
122 cx.notify();
123 }
124
125 pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
126 let item_id = item.id();
127 self.item_views
128 .iter()
129 .any(|(existing_item_id, _)| *existing_item_id == item_id)
130 }
131
132 pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
133 self.item_views.iter().map(|(_, view)| view)
134 }
135
136 pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
137 self.item_views
138 .get(self.active_item)
139 .map(|(_, view)| view.clone())
140 }
141
142 pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
143 self.item_views
144 .iter()
145 .position(|(_, i)| i.id() == item_view.id())
146 }
147
148 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
149 self.item_views.iter().position(|(id, _)| *id == item.id())
150 }
151
152 pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
153 if index < self.item_views.len() {
154 self.active_item = index;
155 self.focus_active_item(cx);
156 cx.notify();
157 }
158 }
159
160 pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
161 if self.active_item > 0 {
162 self.active_item -= 1;
163 } else if self.item_views.len() > 0 {
164 self.active_item = self.item_views.len() - 1;
165 }
166 self.focus_active_item(cx);
167 cx.notify();
168 }
169
170 pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
171 if self.active_item + 1 < self.item_views.len() {
172 self.active_item += 1;
173 } else {
174 self.active_item = 0;
175 }
176 self.focus_active_item(cx);
177 cx.notify();
178 }
179
180 pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
181 if !self.item_views.is_empty() {
182 self.close_item(self.item_views[self.active_item].1.id(), cx)
183 }
184 }
185
186 pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
187 self.item_views.retain(|(_, item)| item.id() != item_id);
188 self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1));
189 if self.item_views.is_empty() {
190 cx.emit(Event::Remove);
191 }
192 cx.notify();
193 }
194
195 fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
196 if let Some(active_item) = self.active_item() {
197 cx.focus(active_item.to_any());
198 }
199 }
200
201 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
202 cx.emit(Event::Split(direction));
203 }
204
205 fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
206 let settings = self.settings.borrow();
207 let theme = &settings.theme;
208
209 enum Tabs {}
210 let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
211 let mut row = Flex::row();
212 for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
213 let is_active = ix == self.active_item;
214
215 row.add_child({
216 let mut title = item_view.title(cx);
217 if title.len() > MAX_TAB_TITLE_LEN {
218 let mut truncated_len = MAX_TAB_TITLE_LEN;
219 while !title.is_char_boundary(truncated_len) {
220 truncated_len -= 1;
221 }
222 title.truncate(truncated_len);
223 title.push('…');
224 }
225
226 let mut style = if is_active {
227 theme.workspace.active_tab.clone()
228 } else {
229 theme.workspace.tab.clone()
230 };
231 if ix == 0 {
232 style.container.border.left = false;
233 }
234
235 EventHandler::new(
236 Container::new(
237 Flex::row()
238 .with_child(
239 Align::new({
240 let diameter = 7.0;
241 let icon_color = if item_view.has_conflict(cx) {
242 Some(style.icon_conflict)
243 } else if item_view.is_dirty(cx) {
244 Some(style.icon_dirty)
245 } else {
246 None
247 };
248
249 ConstrainedBox::new(
250 Canvas::new(move |bounds, _, cx| {
251 if let Some(color) = icon_color {
252 let square = RectF::new(
253 bounds.origin(),
254 vec2f(diameter, diameter),
255 );
256 cx.scene.push_quad(Quad {
257 bounds: square,
258 background: Some(color),
259 border: Default::default(),
260 corner_radius: diameter / 2.,
261 });
262 }
263 })
264 .boxed(),
265 )
266 .with_width(diameter)
267 .with_height(diameter)
268 .boxed()
269 })
270 .boxed(),
271 )
272 .with_child(
273 Container::new(
274 Align::new(
275 Label::new(
276 title,
277 if is_active {
278 theme.workspace.active_tab.label.clone()
279 } else {
280 theme.workspace.tab.label.clone()
281 },
282 )
283 .boxed(),
284 )
285 .boxed(),
286 )
287 .with_style(ContainerStyle {
288 margin: Margin {
289 left: style.spacing,
290 right: style.spacing,
291 ..Default::default()
292 },
293 ..Default::default()
294 })
295 .boxed(),
296 )
297 .with_child(
298 Align::new(
299 ConstrainedBox::new(if mouse_state.hovered {
300 let item_id = item_view.id();
301 enum TabCloseButton {}
302 let icon = Svg::new("icons/x.svg");
303 MouseEventHandler::new::<TabCloseButton, _, _, _>(
304 item_id,
305 cx,
306 |mouse_state, _| {
307 if mouse_state.hovered {
308 icon.with_color(style.icon_close_active)
309 .boxed()
310 } else {
311 icon.with_color(style.icon_close).boxed()
312 }
313 },
314 )
315 .with_padding(Padding::uniform(4.))
316 .with_cursor_style(CursorStyle::PointingHand)
317 .on_click(move |cx| {
318 cx.dispatch_action(CloseItem(item_id))
319 })
320 .named("close-tab-icon")
321 } else {
322 Empty::new().boxed()
323 })
324 .with_width(style.icon_width)
325 .boxed(),
326 )
327 .boxed(),
328 )
329 .boxed(),
330 )
331 .with_style(style.container)
332 .boxed(),
333 )
334 .on_mouse_down(move |cx| {
335 cx.dispatch_action(ActivateItem(ix));
336 true
337 })
338 .boxed()
339 })
340 }
341
342 row.add_child(
343 Empty::new()
344 .contained()
345 .with_border(theme.workspace.tab.container.border)
346 .flexible(0., true)
347 .named("filler"),
348 );
349
350 row.boxed()
351 });
352
353 ConstrainedBox::new(tabs.boxed())
354 .with_height(theme.workspace.tab.height)
355 .named("tabs")
356 }
357}
358
359impl Entity for Pane {
360 type Event = Event;
361}
362
363impl View for Pane {
364 fn ui_name() -> &'static str {
365 "Pane"
366 }
367
368 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
369 if let Some(active_item) = self.active_item() {
370 Flex::column()
371 .with_child(self.render_tabs(cx))
372 .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
373 .named("pane")
374 } else {
375 Empty::new().named("pane")
376 }
377 }
378
379 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
380 self.focus_active_item(cx);
381 }
382}