282 lines
9.5 KiB
Rust
282 lines
9.5 KiB
Rust
use std::{path::PathBuf, rc::Rc};
|
|
|
|
use gpui::{
|
|
App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
|
|
};
|
|
|
|
use crate::{
|
|
buttons::{Button, ButtonVariant, ConfirmButton, ConfirmMode, IconButton},
|
|
data_display::UsageBar,
|
|
forms::DropZone,
|
|
primitives::IconName,
|
|
theme::{ActiveTheme, TextSize},
|
|
typography::{Ellipsis, format},
|
|
};
|
|
|
|
/// A store on disk: its key and name, its size in bytes, and whether it may be cleared.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Store {
|
|
pub key: SharedString,
|
|
pub name: SharedString,
|
|
pub bytes: u64,
|
|
pub clearable: bool,
|
|
}
|
|
|
|
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
|
|
|
/// What an app keeps on disk: a bar of each store's share, then each store with its size and, where it may go, a Clear that asks twice.
|
|
#[derive(IntoElement)]
|
|
pub struct StorageSettings {
|
|
id: ElementId,
|
|
stores: Vec<Store>,
|
|
on_clear: OnKey,
|
|
}
|
|
|
|
impl StorageSettings {
|
|
pub fn new(
|
|
id: impl Into<ElementId>,
|
|
stores: impl IntoIterator<Item = Store>,
|
|
on_clear: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
stores: stores.into_iter().collect(),
|
|
on_clear: Rc::new(on_clear),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for StorageSettings {
|
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let on_clear = self.on_clear;
|
|
let theme = cx.theme();
|
|
let total: u64 = self.stores.iter().map(|store| store.bytes).sum();
|
|
let bar = self.stores.iter().fold(
|
|
UsageBar::new(total.max(1) as f64)
|
|
.amounts(|bytes| format::file_size(bytes as u64, true)),
|
|
|bar, store| bar.part(store.name.clone(), store.bytes as f64),
|
|
);
|
|
let rows = self.stores.iter().map(|store| {
|
|
let (key, on_clear) = (store.key.clone(), on_clear.clone());
|
|
div()
|
|
.flex()
|
|
.flex_wrap()
|
|
.items_center()
|
|
.justify_between()
|
|
.gap_2()
|
|
.py_2()
|
|
.border_b_1()
|
|
.border_color(theme.colors.border)
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.min_w(theme.label_width())
|
|
.flex()
|
|
.flex_col()
|
|
.child(
|
|
div()
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.child(Ellipsis::new(store.name.clone())),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(theme.text_size(TextSize::Xs))
|
|
.text_color(theme.colors.fg_muted)
|
|
.child(format::file_size(store.bytes, true)),
|
|
),
|
|
)
|
|
.children(store.clearable.then(|| {
|
|
ConfirmButton::new(
|
|
(self.id.clone(), format!("clear-{}", store.key)),
|
|
"Clear",
|
|
ConfirmMode::Twice,
|
|
)
|
|
.on_confirm(move |window, cx| {
|
|
log::info!("storage: clear {key}");
|
|
on_clear(&key, window, cx);
|
|
})
|
|
}))
|
|
});
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_3()
|
|
.child(
|
|
div()
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.text_color(theme.colors.fg_muted)
|
|
.child(format!("{} in all", format::file_size(total, true))),
|
|
)
|
|
.child(bar)
|
|
.child(div().flex().flex_col().children(rows))
|
|
}
|
|
}
|
|
|
|
/// A setting changed from its default: its key and name, and both values as words.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Changed {
|
|
pub key: SharedString,
|
|
pub name: SharedString,
|
|
pub now: SharedString,
|
|
pub default: SharedString,
|
|
}
|
|
|
|
type Run = Rc<dyn Fn(&mut Window, &mut App)>;
|
|
|
|
/// The settings changed from their defaults, each with what it is and what it was, and a way back for each; Reset all asks twice. With none changed it says so.
|
|
#[derive(IntoElement)]
|
|
pub struct ResetToDefault {
|
|
id: ElementId,
|
|
changed: Vec<Changed>,
|
|
on_reset: OnKey,
|
|
on_reset_all: Run,
|
|
}
|
|
|
|
impl ResetToDefault {
|
|
pub fn new(
|
|
id: impl Into<ElementId>,
|
|
changed: impl IntoIterator<Item = Changed>,
|
|
on_reset: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
on_reset_all: impl Fn(&mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
changed: changed.into_iter().collect(),
|
|
on_reset: Rc::new(on_reset),
|
|
on_reset_all: Rc::new(on_reset_all),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for ResetToDefault {
|
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let on_reset = self.on_reset;
|
|
let on_reset_all = self.on_reset_all;
|
|
let theme = cx.theme();
|
|
if self.changed.is_empty() {
|
|
return div()
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.text_color(theme.colors.fg_muted)
|
|
.child("Every setting is at its default.")
|
|
.into_any_element();
|
|
}
|
|
let rows = self.changed.iter().map(|changed| {
|
|
let (key, on_reset) = (changed.key.clone(), on_reset.clone());
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.gap_2()
|
|
.py_2()
|
|
.border_b_1()
|
|
.border_color(theme.colors.border)
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.flex()
|
|
.flex_col()
|
|
.child(
|
|
div()
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.child(Ellipsis::new(changed.name.clone())),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(theme.text_size(TextSize::Xs))
|
|
.text_color(theme.colors.fg_muted)
|
|
.child(Ellipsis::new(format!(
|
|
"{} · was {}",
|
|
changed.now, changed.default
|
|
))),
|
|
),
|
|
)
|
|
.child(
|
|
IconButton::new(
|
|
(self.id.clone(), format!("reset-{}", changed.key)),
|
|
IconName::RotateCcw,
|
|
)
|
|
.tooltip("Back to the default")
|
|
.on_click(move |_, window, cx| {
|
|
log::info!("reset: {key}");
|
|
on_reset(&key, window, cx);
|
|
}),
|
|
)
|
|
});
|
|
let on_reset_all = on_reset_all;
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_3()
|
|
.child(div().flex().flex_col().children(rows))
|
|
.child(
|
|
div().flex().child(
|
|
ConfirmButton::new(
|
|
(self.id, "all"),
|
|
format!("Reset all {}", self.changed.len()),
|
|
ConfirmMode::Twice,
|
|
)
|
|
.on_confirm(move |window, cx| {
|
|
log::info!("reset: all");
|
|
on_reset_all(window, cx);
|
|
}),
|
|
),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
}
|
|
|
|
type OnFile = Rc<dyn Fn(PathBuf, &mut Window, &mut App)>;
|
|
|
|
/// Settings out to a file and back: Export asks the owner to write them, and a JSON file dropped or picked asks it to read one.
|
|
#[derive(IntoElement)]
|
|
pub struct ImportExportSettings {
|
|
id: ElementId,
|
|
on_export: Run,
|
|
on_import: OnFile,
|
|
}
|
|
|
|
impl ImportExportSettings {
|
|
pub fn new(
|
|
id: impl Into<ElementId>,
|
|
on_export: impl Fn(&mut Window, &mut App) + 'static,
|
|
on_import: impl Fn(PathBuf, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
on_export: Rc::new(on_export),
|
|
on_import: Rc::new(on_import),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for ImportExportSettings {
|
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
|
let on_export = self.on_export;
|
|
let on_import = self.on_import;
|
|
let (on_export, on_import) = (on_export, on_import);
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_3()
|
|
.child(
|
|
div().flex().child(
|
|
Button::new((self.id.clone(), "export"), "Export settings")
|
|
.variant(ButtonVariant::Secondary)
|
|
.on_click(move |_, window, cx| {
|
|
log::info!("settings: export");
|
|
on_export(window, cx);
|
|
}),
|
|
),
|
|
)
|
|
.child(DropZone::new((self.id, "import")).kinds(&["json"]).on_drop(
|
|
move |paths, window, cx| {
|
|
let Some(path) = paths.first().cloned() else {
|
|
return;
|
|
};
|
|
log::info!("settings: import {}", path.display());
|
|
on_import(path, window, cx);
|
|
},
|
|
))
|
|
}
|
|
}
|