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