1use std::rc::Rc;
8
9use gpui_kit::assets::IconName;
10use gpui_kit::base::dock::{DockArea, DockLayout, DockPlacement, InsertTarget, PanelId};
11use gpui_kit::base::Placement;
12use gpui_kit::component::button::{Button, ButtonVariants as _};
13use gpui_kit::component::dock::{panel_handle, DockSkin, Panel};
14use gpui_kit::component::{h_flex, v_flex, Disableable as _, Icon, Selectable as _, Sizable as _};
15use gpui_kit::prelude::FluentBuilder as _;
16use gpui_kit::{
17 div, px, App, AppContext as _, Context, Entity, EntityId, EventEmitter, FocusHandle, FontWeight, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, ParentElement as _, Render,
18 SharedString, StatefulInteractiveElement as _, Styled as _, Window,
19};
20
21use kitchen_ui::theme::{self, color};
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum Arrangement {
28 Tabs,
30 Horizontal,
32 Vertical,
34 Grid,
36}
37
38impl Arrangement {
39 pub const ALL: [Arrangement; 4] = [Arrangement::Tabs, Arrangement::Horizontal, Arrangement::Vertical, Arrangement::Grid];
41
42 pub fn title(self) -> &'static str {
44 match self {
45 Arrangement::Tabs => "Tabs",
46 Arrangement::Horizontal => "Side by side",
47 Arrangement::Vertical => "Stacked",
48 Arrangement::Grid => "Grid",
49 }
50 }
51}
52
53#[derive(Clone, Debug, PartialEq)]
55pub struct ViewOption {
56 pub title: String,
58 pub icon: IconName,
60 pub hint: String,
62 pub enabled: bool,
64}
65
66impl ViewOption {
67 pub fn new(title: impl Into<String>, icon: IconName, hint: impl Into<String>) -> Self {
69 Self { title: title.into(), icon, hint: hint.into(), enabled: true }
70 }
71
72 pub fn unavailable(mut self, why: impl Into<String>) -> Self {
74 self.enabled = false;
75 self.hint = why.into();
76 self
77 }
78}
79
80pub fn new_dock(id: &'static str, window: &mut Window, cx: &mut App) -> Entity<DockArea> {
82 let mut skin = None;
84 let dock = cx.new(|cx| {
85 let s = DockSkin::new(cx);
86 skin = Some(s.clone());
87 DockArea::new(id, Some(2), window, cx).with_renderer(s)
88 });
89 if let Some(skin) = skin {
90 skin.set_toggle_button_visible(false, cx);
91 }
92 dock
93}
94
95pub fn add_beside<P: Panel>(dock: &Entity<DockArea>, panel: Entity<P>, beside: Option<(EntityId, Placement)>, window: &mut Window, cx: &mut App) {
98 let id = PanelId::from(panel.entity_id());
99 dock.update(cx, |d, cx| {
100 let target = beside.and_then(|(e, at)| Some((d.layout(DockPlacement::Center)?.find_panel_node(PanelId::from(e))?, at)));
101 d.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
102 if let Some((node, placement)) = target {
103 d.move_panel(id, InsertTarget::Split { node, placement, size: None }, window, cx);
104 }
105 });
106}
107
108pub fn arrange<P: Panel>(dock: &Entity<DockArea>, panels: &[Entity<P>], how: Arrangement, window: &mut Window, cx: &mut App) {
110 let layout = arranged(panels, how, cx);
111 dock.update(cx, |d, cx| d.set_center(layout, window, cx));
112}
113
114pub fn arranged<P: Panel>(panels: &[Entity<P>], how: Arrangement, cx: &App) -> DockLayout {
116 let group = |p: &Entity<P>, cx: &App| DockLayout::tabs().panel_view(panel_handle(p.clone()), cx);
117 match how {
118 Arrangement::Tabs => panels.iter().fold(DockLayout::tabs(), |l, p| l.panel_view(panel_handle(p.clone()), cx)),
119 Arrangement::Horizontal => panels.iter().fold(DockLayout::h_split(), |l, p| l.child(group(p, cx), None)),
120 Arrangement::Vertical => panels.iter().fold(DockLayout::v_split(), |l, p| l.child(group(p, cx), None)),
121 Arrangement::Grid => {
122 let cols = (panels.len() as f64).sqrt().ceil().max(1.0) as usize;
123 panels.chunks(cols).fold(DockLayout::v_split(), |l, row| l.child(row.iter().fold(DockLayout::h_split(), |r, p| r.child(group(p, cx), None)), None))
124 }
125 }
126}
127
128type Pick = Rc<dyn Fn(usize, &mut Window, &mut App)>;
129
130pub fn tab_buttons(key: &str, options: &[ViewOption], current: Option<usize>, show: impl Fn(usize, &mut Window, &mut App) + 'static, add: impl Fn(&mut Window, &mut App) + 'static) -> Vec<Button> {
134 let show: Pick = Rc::new(show);
135 let mut out: Vec<Button> = options
136 .iter()
137 .enumerate()
138 .map(|(i, o)| {
139 let show = show.clone();
140 Button::new(SharedString::from(format!("{key}-kind-{i}")))
141 .ghost()
142 .small()
143 .icon(Icon::new(o.icon))
144 .tooltip(format!("{}: {}", o.title, o.hint))
145 .selected(current == Some(i))
146 .disabled(!o.enabled && current != Some(i))
147 .on_click(move |_, window, cx| show(i, window, cx))
148 })
149 .collect();
150 out.push(Button::new(SharedString::from(format!("{key}-add"))).ghost().small().icon(Icon::new(IconName::Plus)).tooltip("Add a view, or arrange the views").on_click(move |_, window, cx| add(window, cx)));
151 out
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum DialogEvent {
157 Arrange(Arrangement),
159 Add(usize),
161 Close,
163}
164
165pub struct AddViewDialog {
168 options: Vec<ViewOption>,
169 can_arrange: bool,
171 cursor: usize,
172 focus: FocusHandle,
173}
174
175impl EventEmitter<DialogEvent> for AddViewDialog {}
176
177const ARRANGEMENTS: [(Arrangement, IconName, &str); 4] = [
179 (Arrangement::Tabs, IconName::AppWindow, "Every view as a tab of one group"),
180 (Arrangement::Horizontal, IconName::Columns2, "Every view side by side"),
181 (Arrangement::Vertical, IconName::Rows2, "Every view stacked"),
182 (Arrangement::Grid, IconName::Grid2x2, "Every view in a grid"),
183];
184
185impl AddViewDialog {
186 pub fn new(options: Vec<ViewOption>, can_arrange: bool, window: &mut Window, cx: &mut Context<Self>) -> Self {
188 let focus = cx.focus_handle();
189 window.focus(&focus, cx);
190 let mut d = Self { options, can_arrange, cursor: 0, focus };
191 d.cursor = d.enabled().first().copied().unwrap_or(0);
192 d
193 }
194
195 fn enabled(&self) -> Vec<usize> {
197 let arr = (0..ARRANGEMENTS.len()).filter(|_| self.can_arrange);
198 arr.chain(self.options.iter().enumerate().filter(|(_, o)| o.enabled).map(|(i, _)| ARRANGEMENTS.len() + i)).collect()
199 }
200
201 pub fn run(&mut self, row: usize, cx: &mut Context<Self>) {
203 cx.emit(if row < ARRANGEMENTS.len() { DialogEvent::Arrange(ARRANGEMENTS[row].0) } else { DialogEvent::Add(row - ARRANGEMENTS.len()) });
204 }
205
206 fn on_key(&mut self, e: &KeyDownEvent, _: &mut Window, cx: &mut Context<Self>) {
207 let enabled = self.enabled();
208 let at = enabled.iter().position(|&i| i == self.cursor);
209 match e.keystroke.key.as_str() {
210 "escape" => cx.emit(DialogEvent::Close),
211 "down" | "up" if !enabled.is_empty() => {
212 let n = enabled.len();
213 let next = match (at, e.keystroke.key.as_str()) {
214 (Some(i), "down") => (i + 1) % n,
215 (Some(i), _) => (i + n - 1) % n,
216 (None, "down") => 0,
217 (None, _) => n - 1,
218 };
219 self.cursor = enabled[next];
220 cx.notify();
221 }
222 "enter" => {
223 if enabled.contains(&self.cursor) {
224 self.run(self.cursor, cx);
225 }
226 }
227 _ => return,
228 }
229 cx.stop_propagation();
230 }
231}
232
233impl Render for AddViewDialog {
234 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
235 let me = cx.entity().downgrade();
236 let cursor = self.cursor;
237 let row = |ix: usize, icon: IconName, title: String, text: String, enabled: bool| {
238 let me = me.clone();
239 h_flex()
240 .id(SharedString::from(format!("add-view-row-{ix}")))
241 .min_h(px(48.))
242 .px(px(theme::SM))
243 .gap(px(theme::MD))
244 .rounded(px(theme::RADIUS))
245 .when(enabled && ix == cursor, |d| d.bg(color::control()))
246 .when(enabled, |d| {
247 d.cursor_pointer().hover(|d| d.bg(color::control())).on_click(move |_, _, cx| {
248 let _ = me.update(cx, |d, cx| d.run(ix, cx));
249 })
250 })
251 .when(!enabled, |d| d.opacity(0.5))
252 .child(div().flex_none().size(px(32.)).rounded(px(theme::RADIUS)).border_1().border_color(color::hover()).flex().items_center().justify_center().child(Icon::new(icon)))
253 .child(v_flex().min_w_0().child(div().text_sm().font_weight(FontWeight::SEMIBOLD).child(title)).child(div().text_xs().text_color(color::text_muted()).truncate().child(text)))
254 };
255 let heading = |text: &'static str, hint: &'static str| v_flex().px(px(theme::SM)).pt(px(theme::MD)).pb(px(theme::XS)).child(div().text_xs().font_weight(FontWeight::SEMIBOLD).child(text)).child(div().text_xs().text_color(color::text_muted()).child(hint));
256 let close = me.clone();
257 let n = ARRANGEMENTS.len();
258 let card = v_flex()
259 .id("add-view-dialog")
260 .occlude()
261 .w(px(460.))
262 .max_h(px(620.))
263 .overflow_y_scroll()
264 .p(px(theme::SM))
265 .rounded(px(theme::RADIUS_LG))
266 .border_1()
267 .border_color(color::border())
268 .bg(color::raised())
269 .shadow_lg()
270 .track_focus(&self.focus)
271 .on_key_down(cx.listener(Self::on_key))
272 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
273 .child(
274 h_flex()
275 .px(px(theme::SM))
276 .py(px(theme::XS))
277 .justify_between()
278 .child(h_flex().gap(px(theme::SM)).items_baseline().child(div().text_sm().font_weight(FontWeight::SEMIBOLD).child("Add view or container")).child(div().text_xs().text_color(color::text_muted()).child("↑ ↓ Enter · Esc")))
279 .child(Button::new("add-view-close").ghost().small().icon(Icon::new(IconName::X)).tooltip("Close").on_click(move |_, _, cx| {
280 let _ = close.update(cx, |_, cx| cx.emit(DialogEvent::Close));
281 })),
282 )
283 .child(heading("Containers", "Arrange the views"))
284 .children(ARRANGEMENTS.iter().enumerate().map(|(i, (how, icon, text))| row(i, *icon, how.title().to_string(), text.to_string(), self.can_arrange)))
285 .child(div().mx(px(theme::SM)).my(px(theme::SM)).h(px(1.)).bg(color::border()))
286 .child(heading("Views", "Added beside the focused view"))
287 .children(self.options.iter().enumerate().map(|(i, o)| row(n + i, o.icon, o.title.clone(), o.hint.clone(), o.enabled)));
288 let back = me.clone();
289 div()
291 .id("add-view-backdrop")
292 .absolute()
293 .inset_0()
294 .bg(gpui_kit::black().opacity(0.35))
295 .flex()
296 .items_start()
297 .justify_center()
298 .pt(px(48.))
299 .on_mouse_down(MouseButton::Left, move |_, _, cx| {
300 let _ = back.update(cx, |_, cx| cx.emit(DialogEvent::Close));
301 })
302 .child(card)
303 }
304}