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 App, AppContext, Border, Entity, Quad, View, ViewContext,
9};
10use std::cmp;
11
12pub fn init(app: &mut App) {
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(
109 &mut self,
110 entry_id: (usize, usize),
111 ctx: &mut ViewContext<Self>,
112 ) -> bool {
113 if let Some(index) = self
114 .items
115 .iter()
116 .position(|item| item.entry_id(ctx.app()).map_or(false, |id| id == entry_id))
117 {
118 self.activate_item(index, ctx);
119 true
120 } else {
121 false
122 }
123 }
124
125 pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
126 self.items.iter().position(|i| i.id() == item.id())
127 }
128
129 pub fn activate_item(&mut self, index: usize, ctx: &mut ViewContext<Self>) {
130 if index < self.items.len() {
131 self.active_item = index;
132 self.focus_active_item(ctx);
133 ctx.notify();
134 }
135 }
136
137 pub fn activate_prev_item(&mut self, ctx: &mut ViewContext<Self>) {
138 if self.active_item > 0 {
139 self.active_item -= 1;
140 } else {
141 self.active_item = self.items.len() - 1;
142 }
143 self.focus_active_item(ctx);
144 ctx.notify();
145 }
146
147 pub fn activate_next_item(&mut self, ctx: &mut ViewContext<Self>) {
148 if self.active_item + 1 < self.items.len() {
149 self.active_item += 1;
150 } else {
151 self.active_item = 0;
152 }
153 self.focus_active_item(ctx);
154 ctx.notify();
155 }
156
157 pub fn close_active_item(&mut self, ctx: &mut ViewContext<Self>) {
158 if !self.items.is_empty() {
159 self.items.remove(self.active_item);
160 if self.active_item >= self.items.len() {
161 self.active_item = self.items.len().saturating_sub(1);
162 }
163 ctx.notify();
164 }
165 if self.items.is_empty() {
166 ctx.emit(Event::Remove);
167 }
168 }
169
170 fn focus_active_item(&mut self, ctx: &mut ViewContext<Self>) {
171 if let Some(active_item) = self.active_item() {
172 ctx.focus(active_item.to_any());
173 }
174 }
175
176 pub fn split(&mut self, direction: SplitDirection, ctx: &mut ViewContext<Self>) {
177 ctx.emit(Event::Split(direction));
178 }
179
180 fn render_tabs(&self, app: &AppContext) -> ElementBox {
181 let settings = smol::block_on(self.settings.read());
182 let border_color = ColorU::from_u32(0xdbdbdcff);
183
184 let mut row = Flex::row();
185 let last_item_ix = self.items.len() - 1;
186 for (ix, item) in self.items.iter().enumerate() {
187 let title = item.title(app);
188
189 let mut border = Border::new(1.0, border_color);
190 border.left = ix > 0;
191 border.right = ix == last_item_ix;
192 border.bottom = ix != self.active_item;
193
194 let padding = 6.;
195 let mut container = Container::new(
196 Align::new(
197 Flex::row()
198 .with_child(
199 Label::new(title, settings.ui_font_family, settings.ui_font_size)
200 .boxed(),
201 )
202 .with_child(
203 Container::new(
204 LineBox::new(
205 settings.ui_font_family,
206 settings.ui_font_size,
207 ConstrainedBox::new(Self::render_modified_icon(
208 item.is_dirty(app),
209 ))
210 .with_max_width(12.)
211 .boxed(),
212 )
213 .boxed(),
214 )
215 .with_margin_left(20.)
216 .boxed(),
217 )
218 .boxed(),
219 )
220 .boxed(),
221 )
222 .with_uniform_padding(padding)
223 .with_border(border);
224
225 if ix == self.active_item {
226 container = container
227 .with_background_color(ColorU::white())
228 .with_padding_bottom(padding + border.width);
229 } else {
230 container = container.with_background_color(ColorU::from_u32(0xeaeaebff));
231 }
232
233 row.add_child(
234 Expanded::new(
235 1.0,
236 ConstrainedBox::new(
237 EventHandler::new(container.boxed())
238 .on_mouse_down(move |ctx| {
239 ctx.dispatch_action("pane:activate_item", ix);
240 true
241 })
242 .boxed(),
243 )
244 .with_max_width(264.0)
245 .boxed(),
246 )
247 .boxed(),
248 );
249 }
250
251 row.add_child(
252 Expanded::new(
253 1.0,
254 Container::new(
255 LineBox::new(
256 settings.ui_font_family,
257 settings.ui_font_size,
258 Empty::new().boxed(),
259 )
260 .boxed(),
261 )
262 .with_uniform_padding(6.0)
263 .with_border(Border::bottom(1.0, border_color))
264 .boxed(),
265 )
266 .boxed(),
267 );
268
269 row.boxed()
270 }
271
272 fn render_modified_icon(is_modified: bool) -> ElementBox {
273 Canvas::new(move |bounds, ctx| {
274 if is_modified {
275 let padding = if bounds.height() < bounds.width() {
276 vec2f(bounds.width() - bounds.height(), 0.0)
277 } else {
278 vec2f(0.0, bounds.height() - bounds.width())
279 };
280 let square = RectF::new(bounds.origin() + padding / 2., bounds.size() - padding);
281 ctx.scene.push_quad(Quad {
282 bounds: square,
283 background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
284 border: Default::default(),
285 corner_radius: square.width() / 2.,
286 });
287 }
288 })
289 .boxed()
290 }
291}
292
293impl Entity for Pane {
294 type Event = Event;
295}
296
297impl View for Pane {
298 fn ui_name() -> &'static str {
299 "Pane"
300 }
301
302 fn render<'a>(&self, app: &AppContext) -> ElementBox {
303 if let Some(active_item) = self.active_item() {
304 Flex::column()
305 .with_child(self.render_tabs(app))
306 .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
307 .boxed()
308 } else {
309 Empty::new().boxed()
310 }
311 }
312
313 fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
314 self.focus_active_item(ctx);
315 }
316
317 // fn state(&self, app: &AppContext) -> Self::State {
318 // State {
319 // tabs: self
320 // .items
321 // .iter()
322 // .enumerate()
323 // .map(|(idx, item)| TabState {
324 // title: item.title(app),
325 // active: idx == self.active_item,
326 // })
327 // .collect(),
328 // }
329 // }
330}