Files
Ely-GPUI-Components/src/settings/appearance.rs
T
ZacharyZhang-NYandClaude Opus 5.5 d05b1b78ca
CI / platforms (windows-latest) (push) Failing after 16s
CI / site (push) Successful in 25s
CI / crate (push) Failing after 10m59s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
i18n: shipped components read their copy from the catalog
Select's default placeholder, the clear, close, remove and unpin tooltips,
the completion menu's empty line and the size sample showed English under
zh-CN. A source test now fails on any fixed English literal in them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:53:00 -04:00

312 lines
9.8 KiB
Rust

use std::rc::Rc;
use gpui::{
App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
SharedString, Styled, Window, div,
};
use crate::{
buttons::{IconButton, SegmentedControl},
canvas::color_well,
forms::{ColorPalette, Slider},
i18n,
primitives::IconName,
theme::{ActiveTheme, Density, Mode, TextSize},
};
/// How an app picks light or dark: always one, or as the system does.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Appearance {
Light,
Dark,
System,
}
impl Appearance {
/// Each with its value, catalog key and icon.
const ALL: [(Appearance, &'static str, IconName); 3] = [
(Appearance::Light, "theme.light", IconName::Sun),
(Appearance::Dark, "theme.dark", IconName::Moon),
(Appearance::System, "theme.system", IconName::Monitor),
];
/// The mode it shows when the system is in `system`.
pub fn mode(self, system: Mode) -> Mode {
match self {
Appearance::Light => Mode::Light,
Appearance::Dark => Mode::Dark,
Appearance::System => system,
}
}
}
type OnAppearance = Rc<dyn Fn(Appearance, &mut Window, &mut App)>;
/// Light, dark, or as the system does, as three segments with their icons. The owner applies the choice, as through `Theme::set_mode`.
#[derive(IntoElement)]
pub struct ThemeSelector {
id: ElementId,
appearance: Appearance,
on_change: OnAppearance,
}
impl ThemeSelector {
pub fn new(
id: impl Into<ElementId>,
appearance: Appearance,
on_change: impl Fn(Appearance, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
appearance,
on_change: Rc::new(on_change),
}
}
}
impl RenderOnce for ThemeSelector {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_change = self.on_change;
let (_, words, _) = Appearance::ALL
.iter()
.find(|(each, _, _)| *each == self.appearance)
.expect("every appearance is offered");
Appearance::ALL
.iter()
.fold(
SegmentedControl::new(self.id, *words),
|control, (_, words, icon)| {
control.segment(*words, i18n::text(cx, words, &[]), Some(*icon))
},
)
.on_change(move |value, window, cx| {
let (chosen, _, _) = Appearance::ALL
.iter()
.find(|(_, words, _)| *words == value.as_ref())
.expect("an appearance the control offers");
log::info!("theme selector: {chosen:?}");
on_change(*chosen, window, cx);
})
}
}
type OnColor = Rc<dyn Fn(Hsla, &mut Window, &mut App)>;
/// An accent picked from named presets, or any color from the well beside them.
#[derive(IntoElement)]
pub struct AccentColorPicker {
id: ElementId,
color: Hsla,
presets: Vec<(SharedString, Hsla)>,
on_change: OnColor,
}
impl AccentColorPicker {
pub fn new(
id: impl Into<ElementId>,
color: impl Into<Hsla>,
presets: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<Hsla>)>,
on_change: impl Fn(Hsla, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
color: color.into(),
presets: presets
.into_iter()
.map(|(name, color)| (name.into(), color.into()))
.collect(),
on_change: Rc::new(on_change),
}
}
}
impl RenderOnce for AccentColorPicker {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_change = self.on_change;
let (picked, custom) = (on_change.clone(), on_change);
let report = |on_change: OnColor| {
move |color: Hsla, window: &mut Window, cx: &mut App| {
log::info!("accent: {color:?}");
on_change(color, window, cx);
}
};
let columns = u16::try_from(self.presets.len()).expect("a handful of presets");
div()
.debug_selector(|| "accent-color-picker".into())
.flex()
.items_start()
.gap_2()
.child(
div().flex_1().min_w_0().child(
ColorPalette::new((self.id.clone(), "presets"), self.presets)
.columns(columns)
.selected(self.color)
.on_change(report(picked)),
),
)
.child(color_well(
(self.id, "custom").into(),
self.color,
report(custom),
))
}
}
type OnSize = Rc<dyn Fn(f32, &mut Window, &mut App)>;
/// A text size on a slider, in pixels, over a line set at it, with a way back to the default size.
#[derive(IntoElement)]
pub struct FontSizeControl {
id: ElementId,
size: f32,
default: f32,
range: (f32, f32),
on_change: OnSize,
}
impl FontSizeControl {
/// `size` and `default` lie within `range`, in pixels.
pub fn new(
id: impl Into<ElementId>,
size: f32,
default: f32,
range: (f32, f32),
on_change: impl Fn(f32, &mut Window, &mut App) + 'static,
) -> Self {
assert!(range.0 < range.1, "font sizes {range:?} run backwards");
let held = |what: &str, value: f32| {
if !(range.0..=range.1).contains(&value) {
log::error!("font size control: {what} {value} outside {range:?}; pegged");
}
value.clamp(range.0, range.1)
};
Self {
id: id.into(),
size: held("size", size),
default: held("default", default),
range,
on_change: Rc::new(on_change),
}
}
}
impl RenderOnce for FontSizeControl {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_change = self.on_change;
let theme = cx.theme();
let (slid, reset, default) = (on_change.clone(), on_change, self.default);
let (low, high) = self.range;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
div().flex_none().w(theme.label_width()).child(
Slider::new((self.id.clone(), "slider"), self.size as f64)
.range(low as f64, high as f64)
.step(1.0)
.on_change(move |size, window, cx| {
slid(size as f32, window, cx);
}),
),
)
.child(
div()
.flex_none()
.text_size(theme.text_size(TextSize::Sm))
.child(format!("{} px", self.size)),
)
.child(
IconButton::new((self.id, "reset"), IconName::RotateCcw)
.tooltip(i18n::text(cx, "theme.size.reset", &[]))
.disabled(self.size == default)
.on_click(move |_, window, cx| {
reset(default, window, cx);
}),
),
)
.child(
div().flex().child(
div()
.flex_1()
.min_w_0()
.text_size(Pixels::from(self.size))
.text_color(theme.colors.fg)
.child(i18n::text(cx, "theme.size.sample", &[])),
),
)
}
}
type OnDensity = Rc<dyn Fn(Density, &mut Window, &mut App)>;
/// How close things sit: compact, standard or comfortable, as three segments.
#[derive(IntoElement)]
pub struct DensitySelector {
id: ElementId,
density: Density,
on_change: OnDensity,
}
const DENSITIES: [(Density, &str); 3] = [
(Density::Compact, "Compact"),
(Density::Standard, "Standard"),
(Density::Comfortable, "Comfortable"),
];
impl DensitySelector {
pub fn new(
id: impl Into<ElementId>,
density: Density,
on_change: impl Fn(Density, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
density,
on_change: Rc::new(on_change),
}
}
}
impl RenderOnce for DensitySelector {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_change = self.on_change;
let (_, words) = DENSITIES
.iter()
.find(|(each, _)| *each == self.density)
.expect("every density is offered");
DENSITIES
.iter()
.fold(
SegmentedControl::new(self.id, *words),
|control, (_, words)| control.segment(*words, *words, None),
)
.on_change(move |value, window, cx| {
let (chosen, _) = DENSITIES
.iter()
.find(|(_, words)| *words == value.as_ref())
.expect("a density the control offers");
log::info!("density: {chosen:?}");
on_change(*chosen, window, cx);
})
}
}
#[cfg(test)]
mod tests {
use super::Appearance;
use crate::theme::Mode;
#[test]
fn system_follows_the_system_and_the_rest_hold() {
assert_eq!(Appearance::System.mode(Mode::Dark), Mode::Dark);
assert_eq!(Appearance::Light.mode(Mode::Dark), Mode::Light);
assert_eq!(Appearance::Dark.mode(Mode::Light), Mode::Dark);
}
}