termden 0.1.2

Workspaces and tiled terminals for coding agents (Claude Code, opencode, pi)
//! The settings popup: theme, text size and the shortcut reference.

use gpui::{AnyElement, Context, FontWeight, div, prelude::*, px};
use termden_core::ThemeId;

use crate::panel::tip;
use crate::state::TermdenApp;
use crate::theme::Theme;
use crate::widgets::{ICON_FONT, icon_button, icons};

pub fn render(app: &TermdenApp, cx: &mut Context<TermdenApp>) -> Option<AnyElement> {
    if !app.settings_open && !app.popups[crate::popup::SETTINGS].closing() {
        return None;
    }
    let theme = &app.theme;

    let card =
        div()
            .id("settings-card")
            .w(px(560.))
            .max_h(px(640.))
            .flex()
            .flex_col()
            .rounded(px(14.))
            .bg(theme.mantle)
            .border_1()
            .border_color(theme.text.opacity(0.10))
            .shadow_lg()
            .overflow_hidden()
            .child(crate::popup::drag_handle(
                div()
                    .flex()
                    .items_center()
                    .h(px(54.))
                    .px(px(18.))
                    .flex_shrink_0()
                    .border_b_1()
                    .border_color(theme.text.opacity(0.06))
                    .child(
                        div()
                            .flex_1()
                            .text_size(theme.ui(15.))
                            .font_weight(FontWeight::SEMIBOLD)
                            .text_color(theme.text)
                            .child("Settings"),
                    )
                    .child(
                        div()
                            .mr(px(10.))
                            .text_size(theme.ui(9.5))
                            .text_color(theme.overlay)
                            .child("Esc to close"),
                    )
                    .child(icon_button("settings-close", icons::CLOSE, theme).on_click(
                        cx.listener(|this, _, _, cx| {
                            this.settings_open = false;
                            cx.notify();
                        }),
                    )),
                crate::popup::SETTINGS,
                (560., 640.),
                cx,
            ))
            .child(
                div()
                    .id("settings-body")
                    .flex_1()
                    .overflow_y_scroll()
                    .p(px(18.))
                    .flex()
                    .flex_col()
                    .gap(px(20.))
                    .child(section(
                        "APPEARANCE",
                        theme,
                        div()
                            .flex()
                            .flex_col()
                            .gap(px(14.))
                            .child(row("Theme", theme).child(theme_choices(app, cx)))
                            .child(row("Text size", theme).child(font_stepper(app, cx))),
                    ))
                    .child(section(
                        "SHORTCUTS · CTRL+SHIFT +",
                        theme,
                        div()
                            .flex()
                            .flex_col()
                            .gap(px(9.))
                            .child(tip("T", "new shell · agents via +", theme))
                            .child(tip("←→", "move focus between tiles (or hjkl)", theme))
                            .child(tip("Z", "zoom tile · double-click its header", theme))
                            .child(tip("W", "close tile (press twice)", theme))
                            .child(tip("PgUp", "previous / next workspace (PgDn)", theme))
                            .child(tip("V", "paste · drag to select and copy", theme))
                            .child(tip("+ −", "terminal text size (0 resets)", theme))
                            .child(tip(",", "open these settings", theme))
                            .child(tip("R", "reload termden (rebuilds reload too)", theme)),
                    )),
            );

    Some(crate::popup::frame(
        "settings-backdrop",
        crate::popup::SETTINGS,
        app,
        card,
        |this| this.settings_open = false,
        cx,
    ))
}

fn section(title: &'static str, theme: &Theme, body: impl IntoElement) -> gpui::Div {
    div()
        .flex()
        .flex_col()
        .gap(px(10.))
        .child(
            div()
                .text_size(theme.ui(9.))
                .font_weight(FontWeight::SEMIBOLD)
                .text_color(theme.overlay)
                .child(title),
        )
        .child(
            div()
                .rounded(px(10.))
                .border_1()
                .border_color(theme.text.opacity(0.06))
                .bg(theme.text.opacity(0.02))
                .p(px(14.))
                .child(body),
        )
}

fn row(label: &'static str, theme: &Theme) -> gpui::Div {
    div()
        .flex()
        .items_center()
        .justify_between()
        .gap(px(14.))
        .child(
            div()
                .text_size(theme.ui(11.))
                .text_color(theme.text)
                .child(label),
        )
}

fn theme_choices(app: &TermdenApp, cx: &mut Context<TermdenApp>) -> AnyElement {
    let theme = &app.theme;
    div()
        .flex()
        .gap(px(10.))
        .children(ThemeId::ALL.into_iter().enumerate().map(|(index, id)| {
            let preview = Theme::for_id(id, &app.config);
            let selected = app.config.theme == id;
            let swatch = |color| div().size(px(14.)).rounded(px(7.)).bg(color);
            div()
                .id(("theme-choice", index))
                .flex()
                .flex_col()
                .gap(px(8.))
                .w(px(150.))
                .p(px(10.))
                .rounded(px(10.))
                .bg(preview.base)
                .border_2()
                .border_color(if selected {
                    theme.accent(0)
                } else {
                    theme.text.opacity(0.08)
                })
                .cursor_pointer()
                .hover(|style| style.opacity(0.85))
                .on_click(cx.listener(move |this, _, _, cx| {
                    this.set_theme(id);
                    cx.notify();
                }))
                .child(
                    div()
                        .flex()
                        .gap(px(5.))
                        .child(swatch(preview.accent(0)))
                        .child(swatch(preview.accent(1)))
                        .child(swatch(preview.accent(2)))
                        .child(swatch(preview.accent(3)))
                        .child(swatch(preview.accent(4))),
                )
                .child(
                    div()
                        .flex()
                        .items_center()
                        .child(
                            div()
                                .flex_1()
                                .text_size(theme.ui(10.5))
                                .text_color(preview.text)
                                .child(id.label()),
                        )
                        .children(selected.then(|| {
                            div()
                                .text_size(theme.ui(11.))
                                .text_color(preview.accent(0))
                                .font_family(ICON_FONT)
                                .child(icons::CHECK)
                        })),
                )
        }))
        .into_any_element()
}

fn font_stepper(app: &TermdenApp, cx: &mut Context<TermdenApp>) -> AnyElement {
    let theme = &app.theme;
    let size = app.config.font_size;
    div()
        .flex()
        .items_center()
        .gap(px(6.))
        .child(
            icon_button("font-dec", icons::REMOVE, theme).on_click(cx.listener(
                |this, _, _, cx| {
                    this.set_font_size(this.config.font_size - 1.0);
                    cx.notify();
                },
            )),
        )
        .child(
            div()
                .min_w(px(46.))
                .flex()
                .justify_center()
                .font_family(theme.mono_font.clone())
                .text_size(theme.ui(11.))
                .text_color(theme.text)
                .child(format!("{size:.0} px")),
        )
        .child(
            icon_button("font-inc", icons::ADD, theme).on_click(cx.listener(|this, _, _, cx| {
                this.set_font_size(this.config.font_size + 1.0);
                cx.notify();
            })),
        )
        .into_any_element()
}