themes: add bundled theme registry and picker
This commit is contained in:
@@ -21,7 +21,7 @@ use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::progress::Progress;
|
||||
use gpui_kit::component::separator::Separator;
|
||||
use gpui_kit::component::switch::Switch;
|
||||
use gpui_kit::component::{Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||
use gpui_kit::component::{Disableable, Sizable, ThemeRegistry, WindowExt, h_flex, v_flex};
|
||||
use settings::{AppSettings, AppearanceMode, NIP4E_ROTATION_DEFAULT_DAYS};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use speech::model::{self as speech_model};
|
||||
@@ -250,6 +250,69 @@ impl Preferences {
|
||||
});
|
||||
}
|
||||
|
||||
/// Dropdown to pick the bundled theme applied in an appearance.
|
||||
fn render_theme_choice(
|
||||
&self,
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
description: &'static str,
|
||||
dark: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
const DEFAULT: &str = "Default";
|
||||
|
||||
let selected = if dark {
|
||||
AppSettings::get_theme_dark(cx)
|
||||
} else {
|
||||
AppSettings::get_theme_light(cx)
|
||||
};
|
||||
|
||||
let themes: Vec<String> = ThemeRegistry::global(cx)
|
||||
.sorted_themes()
|
||||
.into_iter()
|
||||
.filter(|theme| theme.mode.is_dark() == dark)
|
||||
.map(|theme| theme.name.to_string())
|
||||
.collect();
|
||||
|
||||
let current = selected
|
||||
.filter(|name| themes.contains(name))
|
||||
.unwrap_or_else(|| DEFAULT.into());
|
||||
|
||||
settings_row(
|
||||
label,
|
||||
description,
|
||||
Button::new(id)
|
||||
.label(current)
|
||||
.ghost()
|
||||
.small()
|
||||
.dropdown_menu(move |menu, _window, _cx| {
|
||||
let reset = move |name: Option<String>, cx: &mut App| {
|
||||
if dark {
|
||||
AppSettings::set_theme_dark(name, cx);
|
||||
} else {
|
||||
AppSettings::set_theme_light(name, cx);
|
||||
}
|
||||
};
|
||||
|
||||
let menu =
|
||||
menu.item(PopupMenuItem::new(DEFAULT).on_click(move |_, _window, cx| {
|
||||
reset(None, cx);
|
||||
}));
|
||||
|
||||
themes.iter().fold(menu, |menu, name| {
|
||||
let name = name.clone();
|
||||
|
||||
menu.item(PopupMenuItem::new(name.clone()).on_click(
|
||||
move |_, _window, cx| {
|
||||
reset(Some(name.clone()), cx);
|
||||
},
|
||||
))
|
||||
})
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask the user for an image to show behind chat and group panels
|
||||
fn choose_background(
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_variables))] window: &mut Window,
|
||||
@@ -388,6 +451,8 @@ impl Render for Preferences {
|
||||
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
||||
const APPEARANCE: &str =
|
||||
"Choose whether the app follows the system theme or uses a light/dark theme.";
|
||||
const LIGHT_THEME: &str = "Bundled theme applied in light appearance.";
|
||||
const DARK_THEME: &str = "Bundled theme applied in dark appearance.";
|
||||
const FONT_SIZE: &str = "The base font size for the interface.";
|
||||
const RADIUS: &str = "Corner radius for UI elements. Empty follows the OS theme.";
|
||||
const RADIUS_LG: &str =
|
||||
@@ -471,6 +536,8 @@ impl Render for Preferences {
|
||||
}),
|
||||
cx,
|
||||
))
|
||||
.child(self.render_theme_choice("theme-light", "Light Theme", LIGHT_THEME, false, cx))
|
||||
.child(self.render_theme_choice("theme-dark", "Dark Theme", DARK_THEME, true, cx))
|
||||
.child(Separator::horizontal())
|
||||
.child(
|
||||
v_flex()
|
||||
|
||||
+108
-3
@@ -1,3 +1,5 @@
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::settings::{AppSettings, AppearanceMode};
|
||||
@@ -8,13 +10,18 @@ use chat::{ChatEvent, ChatRegistry};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use common::download_dir;
|
||||
use gpui::{
|
||||
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||
Pixels, Render, SharedString, Styled, Subscription, Task, Window, div, px,
|
||||
Action, App, AppContext, Context, Entity, Focusable, InteractiveElement, IntoElement,
|
||||
ParentElement, Pixels, Render, SharedString, Styled, Subscription, Task, Window, div, px,
|
||||
};
|
||||
use gpui_kit::component::button::Button;
|
||||
use gpui_kit::component::command::{
|
||||
Command as CommandPalette, CommandGroup, CommandItem, CommandState,
|
||||
};
|
||||
use gpui_kit::component::dock::{DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
|
||||
use gpui_kit::component::notification::{Notification, NotificationType};
|
||||
use gpui_kit::component::{ActiveTheme, Theme, WindowExt, v_flex};
|
||||
use gpui_kit::component::{
|
||||
ActiveTheme, IndexPath, Theme, ThemeConfig, ThemeRegistry, WindowExt, v_flex,
|
||||
};
|
||||
use nip4e::{DeviceEvent, DeviceRegistry};
|
||||
use nip29::{GroupId, GroupKey};
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -57,6 +64,7 @@ enum Command {
|
||||
ShowRelayList,
|
||||
ShowProfile,
|
||||
ShowSettings,
|
||||
ToggleTheme,
|
||||
ShowBackup,
|
||||
ShowContactList,
|
||||
ShowInbox,
|
||||
@@ -310,6 +318,9 @@ impl Workspace {
|
||||
.child(view.clone())
|
||||
});
|
||||
}
|
||||
Command::ToggleTheme => {
|
||||
self.theme_selector(window, cx);
|
||||
}
|
||||
Command::ShowProfile => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
@@ -440,6 +451,100 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Browse and apply the bundled themes.
|
||||
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let state = cx.new(|cx| CommandState::new(window, cx));
|
||||
let focus_on_mount = Rc::new(Cell::new(true));
|
||||
|
||||
window.open_dialog(cx, move |dialog, _, _| {
|
||||
let state = state.clone();
|
||||
let focus_on_mount = focus_on_mount.clone();
|
||||
|
||||
dialog
|
||||
.close_button(false)
|
||||
.p_0()
|
||||
.width(px(420.))
|
||||
.content(move |content, window, cx| {
|
||||
if focus_on_mount.replace(false) {
|
||||
let state = state.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
state.read(cx).focus_handle(cx).focus(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let active = Theme::global(cx).theme_name().clone();
|
||||
let (lights, darks) = Self::theme_entries(cx);
|
||||
|
||||
content.child(
|
||||
CommandPalette::new(&state)
|
||||
.bordered(false)
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.placeholder("Search themes...")
|
||||
.max_h(px(420.))
|
||||
.on_confirm(move |index, _, cx| {
|
||||
if let Some(name) = Self::theme_name_at(index, cx) {
|
||||
AppSettings::select_theme(&name, cx);
|
||||
}
|
||||
})
|
||||
.group(
|
||||
CommandGroup::new()
|
||||
.label("Light Themes")
|
||||
.items(Self::theme_items(&lights, &active)),
|
||||
)
|
||||
.group(
|
||||
CommandGroup::new()
|
||||
.label("Dark Themes")
|
||||
.items(Self::theme_items(&darks, &active)),
|
||||
),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// Sorted bundled themes partitioned into light and dark groups
|
||||
fn theme_entries(cx: &App) -> (Vec<Rc<ThemeConfig>>, Vec<Rc<ThemeConfig>>) {
|
||||
let (mut lights, mut darks) = (vec![], vec![]);
|
||||
|
||||
for theme in ThemeRegistry::global(cx).sorted_themes() {
|
||||
if theme.mode.is_dark() {
|
||||
darks.push(theme.clone());
|
||||
} else {
|
||||
lights.push(theme.clone());
|
||||
}
|
||||
}
|
||||
|
||||
(lights, darks)
|
||||
}
|
||||
|
||||
/// One palette row per theme.
|
||||
fn theme_items(themes: &[Rc<ThemeConfig>], active: &SharedString) -> Vec<CommandItem> {
|
||||
themes
|
||||
.iter()
|
||||
.map(|theme| {
|
||||
CommandItem::new()
|
||||
.label(theme.name.clone())
|
||||
.checked(&theme.name == active)
|
||||
.keywords([if theme.mode.is_dark() {
|
||||
"dark"
|
||||
} else {
|
||||
"light"
|
||||
}])
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The theme registered at the palette's original group position.
|
||||
fn theme_name_at(index: IndexPath, cx: &App) -> Option<SharedString> {
|
||||
let (lights, darks) = Self::theme_entries(cx);
|
||||
|
||||
match index.section {
|
||||
0 => lights.get(index.row),
|
||||
1 => darks.get(index.row),
|
||||
_ => None,
|
||||
}
|
||||
.map(|theme| theme.name.clone())
|
||||
}
|
||||
|
||||
/// Open the channel creation dialog under the action's group.
|
||||
fn on_create_channel(
|
||||
&mut self,
|
||||
|
||||
@@ -61,6 +61,7 @@ impl Sidebar {
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.menu("Profile", Box::new(Command::ShowProfile))
|
||||
.menu("Relays", Box::new(Command::ShowRelayList))
|
||||
.menu("Themes", Box::new(Command::ToggleTheme))
|
||||
.separator()
|
||||
.label(format!("{CLIENT_NAME} {}", env!("CARGO_PKG_VERSION")))
|
||||
.item(
|
||||
|
||||
Reference in New Issue
Block a user