use crate::{
model::{cover, watch, workspace::Resident},
view::settings::{self, SettingsWindow, Switch},
};
use bezel::{
gpui::{AnyElement, Context, Focusable as _, Window, div, prelude::*, px},
theme::{TextStyle, Theme, Typeset},
ui::{
input::{Shape, TextField},
widgets::{ButtonStyle, Buttons, Scaffolding},
},
};
const FLOOR: u64 = cover::RASTER_BYTES / 1_000_000 + 1;
pub(super) const DIALOG_WIDTH: f32 = 320.;
const BOUNCES: [(u64, &str); 3] = [
(50, "Instant"),
(watch::BOUNCE, "Balanced"),
(500, "Relaxed"),
];
fn covers_under(mb: u64) -> String {
match mb * 1_000_000 / cover::RASTER_BYTES {
1 => "one cover".to_owned(),
n => format!("{n} covers"),
}
}
impl SettingsWindow {
pub(super) fn performance_body(&self, cx: &mut Context<Self>) -> AnyElement {
let theme = Theme::of(cx).clone();
div()
.flex()
.flex_col()
.gap(px(settings::GROUP_GAP))
.child(
theme
.group_box()
.child(self.meter_row(cx))
.child(self.cover_memory_row(cx))
.child(self.watch_bounce_row(cx)),
)
.child(self.resident_group(cx))
.into_any_element()
}
fn meter_row(&self, cx: &mut Context<Self>) -> AnyElement {
let on = self.workspace.read(cx).meter;
self.switch_row(
Switch::new(
"meter",
"Frame meter",
"What this window draws while you use it.",
on,
)
.first(true),
cx,
|this, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.meter = !workspace.meter;
cx.notify();
});
},
)
}
fn cover_memory_row(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
let theme = Theme::of(cx).clone();
let mb = self.workspace.read(cx).settings.cover_memory;
theme
.card_row(false)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.child(theme.row_title("Cover memory"))
.child(
div()
.mt(px(4.))
.text_style(TextStyle::Subheadline)
.text_color(theme.text_muted)
.child(format!("Room for {}.", covers_under(mb))),
),
)
.child(
theme
.ghost("cover-memory")
.px(px(10.))
.py(px(3.))
.border_1()
.border_color(theme.border)
.bg(theme.input_bg)
.text_style(TextStyle::Callout)
.text_color(theme.text)
.child(format!("{mb} MB"))
.on_click(
cx.listener(|this, _, window, cx| this.edit_cover_memory(window, cx)),
),
)
}
fn watch_bounce_row(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
let theme = Theme::of(cx).clone();
let ms = watch::bounce(self.workspace.read(cx).settings.watch_bounce).as_millis();
theme
.card_row(false)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.child(theme.row_title("Watch delay"))
.child(
div()
.mt(px(4.))
.text_style(TextStyle::Subheadline)
.text_color(theme.text_muted)
.child(format!("Re-read after {ms} ms of quiet.")),
),
)
.child(
div()
.flex_none()
.flex()
.flex_row()
.gap(px(2.))
.p(px(2.))
.rounded(px(Theme::button_radius()))
.border_1()
.border_color(theme.border)
.children(BOUNCES.into_iter().map(|(value, label)| {
let selected = u128::from(value) == ms;
div()
.id(("watch-bounce", value as usize))
.px(px(10.))
.py(px(4.))
.rounded(px(Theme::control_radius()))
.text_style(TextStyle::Callout)
.cursor_pointer()
.when(selected, |el| {
el.bg(theme.element_active).text_color(theme.text)
})
.when(!selected, |el| {
el.text_color(theme.text_muted)
.hover(|el| el.bg(theme.element_hover))
})
.child(label)
.on_click(cx.listener(move |this, _, _, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.set_watch_bounce(value, cx)
});
cx.notify();
}))
})),
)
}
fn edit_cover_memory(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let mb = self.workspace.read(cx).settings.cover_memory;
let field = cx.new(|cx| {
let mut field = TextField::new(cx).with_shape(Shape::Line);
field.set_content(mb.to_string(), cx);
field
});
field.focus_handle(cx).focus(window, cx);
self.editing = Some(field);
cx.notify();
}
fn save_cover_memory(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(field) = self.editing.take() else {
return;
};
if let Ok(mb) = field.read(cx).content().trim().parse::<u64>() {
let mb = mb.max(FLOOR);
self.workspace.update(cx, |workspace, cx| {
workspace.set_cover_memory(mb, window, cx)
});
}
cx.notify();
}
pub(super) fn cover_dialog(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let field = self.editing.clone()?;
let theme = Theme::of(cx).clone();
Some(
div()
.id("cover-scrim")
.absolute()
.inset_0()
.flex()
.items_center()
.justify_center()
.bg(theme.scrim())
.on_click(cx.listener(|this, _, _, cx| {
this.editing = None;
cx.notify();
}))
.child(
div()
.id("cover-dialog")
.w(px(DIALOG_WIDTH))
.flex()
.flex_col()
.gap(px(settings::LABEL_GAP))
.p(px(20.))
.rounded(px(Theme::panel_radius()))
.border_1()
.border_color(theme.border)
.bg(theme.surface)
.on_click(|_, _, cx| cx.stop_propagation())
.child(theme.row_title("Cover memory"))
.child(
div()
.text_style(TextStyle::Subheadline)
.text_color(theme.text_muted)
.child("In megabytes."),
)
.child(field)
.child(
div()
.flex()
.flex_row()
.justify_end()
.gap(px(8.))
.child(
theme
.button("Cancel", ButtonStyle::Ghost, None)
.id("cover-cancel")
.on_click(cx.listener(|this, _, _, cx| {
this.editing = None;
cx.notify();
})),
)
.child(
theme
.button("Save", ButtonStyle::Prominent, None)
.id("cover-save")
.on_click(cx.listener(|this, _, window, cx| {
this.save_cover_memory(window, cx)
})),
),
),
)
.into_any_element(),
)
}
fn resident_group(&self, cx: &mut Context<Self>) -> AnyElement {
let theme = Theme::of(cx).clone();
let Resident {
projects,
articles,
editors,
covers,
sessions,
items,
} = self.workspace.read(cx).resident(cx);
div()
.flex()
.flex_col()
.gap(px(settings::LABEL_GAP))
.child(theme.field_label("Resident"))
.child(
theme
.group_box()
.child(self.stat_row(
true,
"Projects",
"Open on the rail.",
projects.to_string(),
cx,
))
.child(self.stat_row(
false,
"Articles",
"Holding an editor, of those listed.",
format!("{editors} of {articles}"),
cx,
))
.child(self.stat_row(
false,
"Covers",
"Decoded and held, at twice their declared size.",
format!("{covers} · {} MB each", cover::RASTER_BYTES / 1_000_000),
cx,
))
.child(self.stat_row(
false,
"Transcripts",
"Sessions in memory, and the entries across them.",
format!("{sessions} · {items} entries"),
cx,
)),
)
.into_any_element()
}
fn stat_row(
&self,
first: bool,
title: &'static str,
note: &'static str,
value: String,
cx: &Context<Self>,
) -> impl IntoElement + use<> {
let theme = Theme::of(cx).clone();
theme
.card_row(first)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.child(theme.row_title(title))
.child(
div()
.mt(px(4.))
.text_style(TextStyle::Subheadline)
.text_color(theme.text_muted)
.child(note),
),
)
.child(
div()
.flex_none()
.text_style(TextStyle::Callout)
.text_color(theme.text)
.child(value),
)
}
}