Vectrum
Sign In
199 lines · 7.2 KB · Rust
//! The right panel, as T3 Code has it: it shows one surface at a time (the
//! subagents, the browser, a device or a terminal), and opens on "Open a surface", a
//! short list to pick one from, each with a key of its own.

use gpui_kit::component::{Icon, IconName, h_flex, v_flex};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;

use super::Workspace;
use crate::ui::icons::Lucide;
use crate::ui::metal::{self, MetalStyled as _};

/// What the right panel shows.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Surface {
    /// "Open a surface": the list of the others.
    Picker,
    Subagents,
    Browser,
    Device,
    Terminal,
}

/// The surfaces the picker lists: each with its icon, name and key.
const SURFACES: [(Surface, &str, &str); 4] = [
    (Surface::Subagents, "Subagents", "S"),
    (Surface::Browser, "Browser", "B"),
    (Surface::Device, "Device", "D"),
    (Surface::Terminal, "Terminal", "T"),
];

/// How wide a terminal in the right panel is.
pub(super) const TERMINAL_PANEL_WIDTH: f32 = 520.;

impl Workspace {
    /// What the open thread's right panel shows.
    pub(super) fn right_panel(&self) -> Option<Surface> {
        match &self.active {
            Some(session) => self.right_panels.get(&session.entity_id()).copied(),
            None => self.idle_right_panel,
        }
    }

    /// Sets what the open thread's right panel shows. Other threads keep theirs.
    pub(super) fn set_right_panel(&mut self, surface: Option<Surface>) {
        match self.active.as_ref().map(|session| session.entity_id()) {
            Some(id) => self.set_thread_surface(id, surface),
            None => self.idle_right_panel = surface,
        }
    }

    /// What a thread's right panel shows, open or not.
    pub(super) fn thread_surface(&self, thread: EntityId) -> Option<Surface> {
        self.right_panels.get(&thread).copied()
    }

    /// Sets what a thread's right panel shows, open or not.
    pub(super) fn set_thread_surface(&mut self, thread: EntityId, surface: Option<Surface>) {
        match surface {
            Some(surface) => {
                self.right_panels.insert(thread, surface);
            }
            None => {
                self.right_panels.remove(&thread);
            }
        }
    }

    pub(super) fn surface_open(&self, surface: Surface) -> bool {
        self.right_panel() == Some(surface)
    }

    /// The right panel's button: opens "Open a surface", or closes the panel.
    pub(super) fn toggle_right_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if self.right_panel().is_some() {
            self.set_right_panel(None);
        } else {
            self.set_right_panel(Some(Surface::Picker));
            self.surface_focus.focus(window, cx);
        }
        cx.notify();
    }

    /// Shows a surface in the right panel, or closes the panel if it's
    /// showing it already.
    pub(super) fn toggle_surface(&mut self, surface: Surface, window: &mut Window, cx: &mut Context<Self>) {
        if self.right_panel() == Some(surface) {
            self.set_right_panel(None);
            cx.notify();
            return;
        }
        self.open_surface(surface, window, cx);
    }

    pub(super) fn open_surface(&mut self, surface: Surface, window: &mut Window, cx: &mut Context<Self>) {
        if surface == Surface::Terminal {
            if !self.terminal_available(cx) {
                return;
            }
            // The terminals move over from the drawer, if it's open.
            self.dock_terminal(window, cx);
        }
        self.set_right_panel(Some(surface));
        if surface == Surface::Picker {
            self.surface_focus.focus(window, cx);
        }
        cx.notify();
    }

    /// The picker's keys: S, B, D and T open their surfaces; Escape closes it.
    fn surface_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
        let keystroke = &event.keystroke;
        if keystroke.modifiers.modified() {
            return;
        }
        if keystroke.key == "escape" {
            self.set_right_panel(None);
            cx.notify();
            return;
        }
        if let Some((surface, _, _)) = SURFACES.iter().find(|(_, _, key)| key.eq_ignore_ascii_case(&keystroke.key)) {
            cx.stop_propagation();
            self.open_surface(*surface, window, cx);
        }
    }

    pub(super) fn render_surface_picker(&self, cx: &mut Context<Self>) -> impl IntoElement {
        let p = metal::palette(cx);
        let terminal = self.terminal_available(cx);
        let rows = SURFACES.iter().map(|(surface, name, key)| {
            let surface = *surface;
            let enabled = surface != Surface::Terminal || terminal;
            let icon = match surface {
                Surface::Subagents => Icon::new(Lucide::Bot),
                Surface::Browser => Icon::new(IconName::Globe),
                Surface::Device => Icon::new(Lucide::Smartphone),
                _ => Icon::new(Lucide::SquareTerminal),
            };
            h_flex()
                .id(SharedString::from(format!("surface-{name}")))
                .h(px(31.))
                .px(px(10.))
                .gap(px(10.))
                .rounded(px(8.))
                .text_size(px(13.5))
                .text_color(if enabled { p.ink } else { p.ink3 })
                .when(enabled, |this| {
                    this.cursor_pointer()
                        .hover(|style| style.bg(p.row_hover))
                        .on_click(cx.listener(move |this, _, window, cx| this.open_surface(surface, window, cx)))
                })
                .child(icon.size(px(16.)).text_color(if enabled { p.ink2 } else { p.ink3 }))
                .child(div().flex_1().child(*name))
                .child(
                    div()
                        .flex_none()
                        .min_w(px(18.))
                        .h(px(18.))
                        .px(px(5.))
                        .flex()
                        .items_center()
                        .justify_center()
                        .rounded(px(4.))
                        .bg(p.row_hover)
                        .font_family(metal::MONO)
                        .text_size(px(10.5))
                        .text_color(p.ink3)
                        .child(*key),
                )
        });
        v_flex()
            .id("surface-picker")
            .key_context("SurfacePicker")
            .track_focus(&self.surface_focus)
            .on_key_down(cx.listener(Self::surface_key))
            .w(px(super::PANEL_WIDTH))
            .flex_none()
            .h_full()
            .p(px(12.))
            .pl(px(0.))
            .child(
                v_flex()
                    .flex_1()
                    .justify_center()
                    .px(px(16.))
                    .rounded(px(18.))
                    .raised(p)
                    .child(
                        div()
                            .pb(px(14.))
                            .text_center()
                            .text_size(px(14.))
                            .font_weight(FontWeight::MEDIUM)
                            .child("Open a surface"),
                    )
                    .child(v_flex().gap(px(2.)).children(rows)),
            )
    }
}