use crate::{
agent::Listing,
model::workspace::Workspace,
view::root::{HEADER_HEIGHT, TRAFFIC_LIGHT_X, TRAFFIC_LIGHT_Y},
};
use bezel::{
gpui::{
App, Bounds, Context, Entity, Render, SharedString, TitlebarOptions, Window,
WindowBackgroundAppearance, WindowBounds, WindowHandle, WindowOptions, div, point,
prelude::*, px, size,
},
motion::{Fade, Painter},
theme::{TextStyle, Theme, Typeset, appearance},
ui::{
icons,
input::{FieldEvent, Shape, TextField},
widgets::{Layout, Scaffolding},
},
};
use std::collections::HashSet;
mod agents;
mod features;
mod general;
mod performance;
mod theme;
mod typography;
const SIDEBAR_WIDTH: f32 = 200.;
pub(super) const GROUP_GAP: f32 = 20.;
pub(super) const LABEL_GAP: f32 = 8.;
const CONTENT_MAX_WIDTH: f32 = 860.;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Section {
General,
Appearance,
Features,
Agents,
Performance,
}
impl Section {
const ALL: [Self; 5] = [
Self::General,
Self::Appearance,
Self::Features,
Self::Agents,
Self::Performance,
];
fn title(self) -> &'static str {
match self {
Self::General => "General",
Self::Appearance => "Appearance",
Self::Features => "Features",
Self::Agents => "Agents",
Self::Performance => "Performance",
}
}
fn subtitle(self) -> Option<&'static str> {
match self {
Self::Features => Some("Parts of cydonia that stay off until you ask for them."),
Self::General | Self::Appearance | Self::Agents | Self::Performance => None,
}
}
fn glyph(self) -> &'static [u8] {
match self {
Self::General => icons::account::Settings,
Self::Appearance => icons::weather::Sun,
Self::Features => icons::account::SlidersHorizontal,
Self::Agents => icons::layout::LayoutGrid,
Self::Performance => icons::devices::Cpu,
}
}
}
pub struct SettingsWindow {
workspace: Entity<Workspace>,
section: Section,
listings: Option<Vec<Listing>>,
busy: HashSet<String>,
search: Entity<TextField>,
editing: Option<Entity<TextField>>,
error: Option<SharedString>,
}
pub fn open(
workspace: Entity<Workspace>,
existing: Option<WindowHandle<SettingsWindow>>,
section: Section,
cx: &mut App,
) -> Option<WindowHandle<SettingsWindow>> {
if let Some(handle) = existing
&& handle
.update(cx, |this, window, cx| {
this.show(section, cx);
window.activate_window();
})
.is_ok()
{
return Some(handle);
}
let bounds = Bounds::centered(None, size(px(900.), px(620.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(TitlebarOptions {
appears_transparent: true,
traffic_light_position: Some(point(px(TRAFFIC_LIGHT_X), px(TRAFFIC_LIGHT_Y))),
..Default::default()
}),
window_background: WindowBackgroundAppearance::Opaque,
app_id: Some("cydonia-settings".into()),
..Default::default()
},
|window, cx| {
appearance::observe_window(window, cx).detach();
cx.new(|cx| {
let search = cx.new(|cx| {
TextField::new(cx)
.with_shape(Shape::Line)
.with_frame(false)
.with_placeholder("Search agents…")
});
cx.subscribe(&search, |_, _, event: &FieldEvent, cx| {
if *event == FieldEvent::Changed {
cx.notify();
}
})
.detach();
let mut this = SettingsWindow {
workspace,
section,
listings: None,
busy: HashSet::new(),
search,
editing: None,
error: None,
};
this.load(cx);
this
})
},
)
.ok()
}
impl SettingsWindow {
fn show(&mut self, section: Section, cx: &mut Context<Self>) {
self.section = section;
match section {
Section::Agents => self.load(cx),
Section::General | Section::Appearance | Section::Features | Section::Performance => {}
}
cx.notify();
}
fn sidebar(&self, cx: &Context<Self>) -> impl IntoElement + use<> {
let theme = Theme::of(cx).clone();
let painter = Painter::of(cx);
div()
.flex_none()
.w(px(SIDEBAR_WIDTH))
.h_full()
.bg(theme.surface)
.border_r_1()
.border_color(theme.border)
.flex()
.flex_col()
.gap(px(2.))
.px(px(8.))
.pb(px(8.))
.pt(px(HEADER_HEIGHT))
.children(Section::ALL.into_iter().enumerate().map(|(ix, section)| {
theme
.nav_row(
Some(section.glyph().into()),
section.title(),
section == self.section,
Fade::new(painter, format!("section-{ix}")),
)
.id(("section", ix))
.on_click(cx.listener(move |this, _, _, cx| this.show(section, cx)))
}))
}
}
impl Render for SettingsWindow {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::of(cx).clone();
div()
.size_full()
.relative()
.flex()
.flex_row()
.bg(theme.bg)
.font_family(theme.font_sans.clone())
.text_color(theme.text)
.text_style(TextStyle::Body)
.child(self.sidebar(cx))
.child(
div()
.id("settings-body")
.flex_1()
.min_w_0()
.h_full()
.overflow_y_scroll()
.px(px(32.))
.py(px(32.))
.flex()
.flex_col()
.items_center()
.child(
div()
.w_full()
.max_w(px(CONTENT_MAX_WIDTH))
.flex()
.flex_col()
.child(
div()
.mb(px(GROUP_GAP))
.child(theme.page_header(self.section.title(), None))
.children(
self.section
.subtitle()
.map(|copy| theme.page_subtitle(copy)),
),
)
.child(match self.section {
Section::General => self.general_body(cx),
Section::Appearance => self.appearance_body(cx),
Section::Features => self.features_body(cx),
Section::Agents => self.agents_body(cx),
Section::Performance => self.performance_body(cx),
}),
),
)
.children(self.cover_dialog(cx))
}
}