1use super::{ItemViewHandle, SplitDirection};
2use crate::{settings::Settings, watch};
3use gpui::{
4 color::{ColorF, ColorU},
5 elements::*,
6 geometry::{rect::RectF, vector::vec2f},
7 keymap::Binding,
8 AppContext, Border, Entity, MutableAppContext, Quad, View, ViewContext,
9};
10use std::cmp;
11
12pub fn init(app: &mut MutableAppContext) {
13 app.add_action(
14 "pane:activate_item",
15 |pane: &mut Pane, index: &usize, ctx| {
16 pane.activate_item(*index, ctx);
17 },
18 );
19 app.add_action("pane:activate_prev_item", |pane: &mut Pane, _: &(), ctx| {
20 pane.activate_prev_item(ctx);
21 });
22 app.add_action("pane:activate_next_item", |pane: &mut Pane, _: &(), ctx| {
23 pane.activate_next_item(ctx);
24 });
25 app.add_action("pane:close_active_item", |pane: &mut Pane, _: &(), ctx| {
26 pane.close_active_item(ctx);
27 });
28 app.add_action("pane:split_up", |pane: &mut Pane, _: &(), ctx| {
29 pane.split(SplitDirection::Up, ctx);
30 });
31 app.add_action("pane:split_down", |pane: &mut Pane, _: &(), ctx| {
32 pane.split(SplitDirection::Down, ctx);
33 });
34 app.add_action("pane:split_left", |pane: &mut Pane, _: &(), ctx| {
35 pane.split(SplitDirection::Left, ctx);
36 });
37 app.add_action("pane:split_right", |pane: &mut Pane, _: &(), ctx| {
38 pane.split(SplitDirection::Right, ctx);
39 });
40
41 app.add_bindings(vec![
42 Binding::new("shift-cmd-{", "pane:activate_prev_item", Some("Pane")),
43 Binding::new("shift-cmd-}", "pane:activate_next_item", Some("Pane")),
44 Binding::new("cmd-w", "pane:close_active_item", Some("Pane")),
45 Binding::new("cmd-k up", "pane:split_up", Some("Pane")),
46 Binding::new("cmd-k down", "pane:split_down", Some("Pane")),
47 Binding::new("cmd-k left", "pane:split_left", Some("Pane")),
48 Binding::new("cmd-k right", "pane:split_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, ctx: &mut ViewContext<Self>) {
85 ctx.emit(Event::Activate);
86 }
87
88 pub fn add_item(
89 &mut self,
90 item: Box<dyn ItemViewHandle>,
91 ctx: &mut ViewContext<Self>,
92 ) -> usize {
93 let item_idx = cmp::min(self.active_item + 1, self.items.len());
94 self.items.insert(item_idx, item);
95 ctx.notify();
96 item_idx
97 }
98
99 #[cfg(test)]
100 pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
101 &self.items
102 }
103
104 pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
105 self.items.get(self.active_item).cloned()
106 }
107
108 pub fn activate_entry(&mut self, entry_id: (usize, u64), ctx: &mut ViewContext<Self>) -> bool {
109 if let Some(index) = self
110 .items
111 .iter()
112 .position(|item| item.entry_id(ctx.as_ref()).map_or(false, |id| id == entry_id))
113 {
114 self.activate_item(index, ctx);
115 true
116 } else {
117 false
118 }
119 }
120
121 pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
122 self.items.iter().position(|i| i.id() == item.id())
123 }
124
125 pub fn activate_item(&mut self, index: usize, ctx: &mut ViewContext<Self>) {
126 if index < self.items.len() {
127 self.active_item = index;
128 self.focus_active_item(ctx);
129 ctx.notify();
130 }
131 }
132
133 pub fn activate_prev_item(&mut self, ctx: &mut ViewContext<Self>) {
134 if self.active_item > 0 {
135 self.active_item -= 1;
136 } else {
137 self.active_item = self.items.len() - 1;
138 }
139 self.focus_active_item(ctx);
140 ctx.notify();
141 }
142
143 pub fn activate_next_item(&mut self, ctx: &mut ViewContext<Self>) {
144 if self.active_item + 1 < self.items.len() {
145 self.active_item += 1;
146 } else {
147 self.active_item = 0;
148 }
149 self.focus_active_item(ctx);
150 ctx.notify();
151 }
152
153 pub fn close_active_item(&mut self, ctx: &mut ViewContext<Self>) {
154 if !self.items.is_empty() {
155 self.items.remove(self.active_item);
156 if self.active_item >= self.items.len() {
157 self.active_item = self.items.len().saturating_sub(1);
158 }
159 ctx.notify();
160 }
161 if self.items.is_empty() {
162 ctx.emit(Event::Remove);
163 }
164 }
165
166 fn focus_active_item(&mut self, ctx: &mut ViewContext<Self>) {
167 if let Some(active_item) = self.active_item() {
168 ctx.focus(active_item.to_any());
169 }
170 }
171
172 pub fn split(&mut self, direction: SplitDirection, ctx: &mut ViewContext<Self>) {
173 ctx.emit(Event::Split(direction));
174 }
175
176 fn render_tabs(&self, app: &AppContext) -> ElementBox {
177 let settings = smol::block_on(self.settings.read());
178 let border_color = ColorU::from_u32(0xdbdbdcff);
179
180 let mut row = Flex::row();
181 let last_item_ix = self.items.len() - 1;
182 for (ix, item) in self.items.iter().enumerate() {
183 let title = item.title(app);
184
185 let mut border = Border::new(1.0, border_color);
186 border.left = ix > 0;
187 border.right = ix == last_item_ix;
188 border.bottom = ix != self.active_item;
189
190 let padding = 6.;
191 let mut container = Container::new(
192 Stack::new()
193 .with_child(
194 Align::new(
195 Label::new(title, settings.ui_font_family, settings.ui_font_size)
196 .boxed(),
197 )
198 .boxed(),
199 )
200 .with_child(
201 LineBox::new(
202 settings.ui_font_family,
203 settings.ui_font_size,
204 Align::new(Self::render_modified_icon(item.is_dirty(app)))
205 .right()
206 .boxed(),
207 )
208 .boxed(),
209 )
210 .boxed(),
211 )
212 .with_vertical_padding(padding)
213 .with_horizontal_padding(10.)
214 .with_border(border);
215
216 if ix == self.active_item {
217 container = container
218 .with_background_color(ColorU::white())
219 .with_padding_bottom(padding + border.width);
220 } else {
221 container = container.with_background_color(ColorU::from_u32(0xeaeaebff));
222 }
223
224 row.add_child(
225 Expanded::new(
226 1.0,
227 ConstrainedBox::new(
228 EventHandler::new(container.boxed())
229 .on_mouse_down(move |ctx| {
230 ctx.dispatch_action("pane:activate_item", ix);
231 true
232 })
233 .boxed(),
234 )
235 .with_min_width(80.0)
236 .with_max_width(264.0)
237 .boxed(),
238 )
239 .named("tab"),
240 );
241 }
242
243 // Ensure there's always a minimum amount of space after the last tab,
244 // so that the tab's border doesn't abut the window's border.
245 row.add_child(
246 ConstrainedBox::new(
247 Container::new(
248 LineBox::new(
249 settings.ui_font_family,
250 settings.ui_font_size,
251 Empty::new().boxed(),
252 )
253 .boxed(),
254 )
255 .with_uniform_padding(6.0)
256 .with_border(Border::bottom(1.0, border_color))
257 .boxed(),
258 )
259 .with_min_width(20.)
260 .named("fixed-filler"),
261 );
262
263 row.add_child(
264 Expanded::new(
265 0.0,
266 Container::new(
267 LineBox::new(
268 settings.ui_font_family,
269 settings.ui_font_size,
270 Empty::new().boxed(),
271 )
272 .boxed(),
273 )
274 .with_uniform_padding(6.0)
275 .with_border(Border::bottom(1.0, border_color))
276 .boxed(),
277 )
278 .named("filler"),
279 );
280
281 row.named("tabs")
282 }
283
284 fn render_modified_icon(is_modified: bool) -> ElementBox {
285 let diameter = 8.;
286 ConstrainedBox::new(
287 Canvas::new(move |bounds, ctx| {
288 if is_modified {
289 let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
290 ctx.scene.push_quad(Quad {
291 bounds: square,
292 background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
293 border: Default::default(),
294 corner_radius: diameter / 2.,
295 });
296 }
297 })
298 .boxed(),
299 )
300 .with_width(diameter)
301 .with_height(diameter)
302 .named("tab-right-icon")
303 }
304}
305
306impl Entity for Pane {
307 type Event = Event;
308}
309
310impl View for Pane {
311 fn ui_name() -> &'static str {
312 "Pane"
313 }
314
315 fn render<'a>(&self, app: &AppContext) -> ElementBox {
316 if let Some(active_item) = self.active_item() {
317 Flex::column()
318 .with_child(self.render_tabs(app))
319 .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
320 .named("pane")
321 } else {
322 Empty::new().named("pane")
323 }
324 }
325
326 fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
327 self.focus_active_item(ctx);
328 }
329
330 // fn state(&self, app: &AppContext) -> Self::State {
331 // State {
332 // tabs: self
333 // .items
334 // .iter()
335 // .enumerate()
336 // .map(|(idx, item)| TabState {
337 // title: item.title(app),
338 // active: idx == self.active_item,
339 // })
340 // .collect(),
341 // }
342 // }
343}