feat: adopt the native OS theme (#33)
Reviewed-on: #33
This commit was merged in pull request #33.
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Follow the operating system's native theme on macOS, Windows and Linux (KDE, GNOME), applying the OS colors, fonts, radii and accessibility preferences.
|
||||||
- Add an identity import dialog, importing an existing identity by pasting its `nsec` secret key (encrypted with a new passphrase), NIP-49 `ncryptsec` (unlocked with its passphrase), or `bunker://` NIP-46 URI
|
- Add an identity import dialog, importing an existing identity by pasting its `nsec` secret key (encrypted with a new passphrase), NIP-49 `ncryptsec` (unlocked with its passphrase), or `bunker://` NIP-46 URI
|
||||||
- Add a dropdown menu on the signed-in user button in the sidebar
|
- Add a dropdown menu on the signed-in user button in the sidebar
|
||||||
- Add sign-out feature, confirmed by an alert dialog before deleting the stored session credential
|
- Add sign-out feature, confirmed by an alert dialog before deleting the stored session credential
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Use the built-in light/dark syntax highlighting palette instead of the Signed palette bundled in `signed.json` when the native theme is active
|
||||||
- Increase the default theme corner radii from 2 to 4 and large radii from 6 to 8
|
- Increase the default theme corner radii from 2 to 4 and large radii from 6 to 8
|
||||||
- Surface backend errors as error notifications in the workspace instead of silently dropping them
|
- Surface backend errors as error notifications in the workspace instead of silently dropping them
|
||||||
- Break ties in repository activity lists by event id, so same-second events order deterministically
|
- Break ties in repository activity lists by event id, so same-second events order deterministically
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
- Remove the light/dark theme pickers and the font size, corner radius, focus ring and shadow settings from the settings dialog; colors and geometry now come from the native theme
|
||||||
- Remove dead code: the unused `login`/`logout` family, the unwired `SyncProgress` pipeline, `merge_pull_request`, inbox mark-read/archive APIs and the wasm32-only code paths
|
- Remove dead code: the unused `login`/`logout` family, the unwired `SyncProgress` pipeline, `merge_pull_request`, inbox mark-read/archive APIs and the wasm32-only code paths
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|||||||
Generated
+433
-201
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -13,9 +13,13 @@ gpui = { package = "gpui-pre", version = "0.3.8" }
|
|||||||
gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland"] }
|
gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland"] }
|
||||||
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" }
|
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" }
|
||||||
|
|
||||||
# GPUI Kit
|
# GPUI Kit; exact pins keep the graph on the gpui-pre 0.3.8 closure that
|
||||||
gpui-component = { version = "0.7.1", features = ["tree-sitter-languages"] }
|
# native-theme-gpui 0.6.1 is verified against (0.7.3 and 0.8 pin 0.3.9).
|
||||||
gpui-base = "0.7.1"
|
gpui-component = { version = "=0.7.1", features = ["tree-sitter-languages"] }
|
||||||
|
gpui-base = "=0.7.1"
|
||||||
|
|
||||||
|
native-theme = { version = "0.6.1", features = ["watch"] }
|
||||||
|
native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
|
||||||
|
|
||||||
dock = { path = "crates/dock" }
|
dock = { path = "crates/dock" }
|
||||||
settings = { path = "crates/settings" }
|
settings = { path = "crates/settings" }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::{Error, anyhow, bail};
|
use anyhow::{Error, anyhow, bail};
|
||||||
@@ -24,8 +25,6 @@ use crate::repos::RepoListStore;
|
|||||||
use crate::state::{GitPushMode, GitPushPolicy, GitRef, PushStepStatus, SyncState};
|
use crate::state::{GitPushMode, GitPushPolicy, GitRef, PushStepStatus, SyncState};
|
||||||
|
|
||||||
pub const USER_KEYRING: &str = "Signed Safe Storage";
|
pub const USER_KEYRING: &str = "Signed Safe Storage";
|
||||||
pub const NOSTR_CONNECT_TIMEOUT: u64 = 60;
|
|
||||||
|
|
||||||
const PUMP_DEBOUNCE: Duration = Duration::from_millis(200);
|
const PUMP_DEBOUNCE: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -199,13 +198,9 @@ impl Backend {
|
|||||||
} else if content.starts_with("bunker://") {
|
} else if content.starts_with("bunker://") {
|
||||||
let (base, keys) = extract_master_key(&content)?;
|
let (base, keys) = extract_master_key(&content)?;
|
||||||
let uri = NostrConnectUri::parse(base)?;
|
let uri = NostrConnectUri::parse(base)?;
|
||||||
let mut signer = NostrConnect::new(
|
let signer = NostrConnect::builder(uri, keys)
|
||||||
uri,
|
.auth_url_handler(Arc::new(SignedAuthUrlHandler))
|
||||||
keys,
|
.build()?;
|
||||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
signer.auth_url_handler(SignedAuthUrlHandler);
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_signer(signer, cx);
|
this.set_signer(signer, cx);
|
||||||
@@ -366,13 +361,9 @@ impl Backend {
|
|||||||
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let mut signer = NostrConnect::new(
|
let signer = NostrConnect::builder(connect_uri, keys)
|
||||||
connect_uri,
|
.auth_url_handler(Arc::new(SignedAuthUrlHandler))
|
||||||
keys,
|
.build()?;
|
||||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
signer.auth_url_handler(SignedAuthUrlHandler);
|
|
||||||
|
|
||||||
// Verify the bunker responds before persisting the credential.
|
// Verify the bunker responds before persisting the credential.
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{
|
use gpui::{App, Entity, PathPromptOptions, SharedString, Subscription, Window, div, px};
|
||||||
AnyWindowHandle, App, Entity, PathPromptOptions, SharedString, Subscription, Window, div, px,
|
|
||||||
};
|
|
||||||
use gpui_component::button::{Button, ButtonVariants};
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
use gpui_component::input::{
|
use gpui_component::input::{Input, InputState};
|
||||||
Input, InputEvent, InputState, NumberInput, NumberInputEvent, StepAction,
|
|
||||||
};
|
|
||||||
use gpui_component::select::{Select, SelectEvent, SelectState};
|
use gpui_component::select::{Select, SelectEvent, SelectState};
|
||||||
use gpui_component::separator::Separator;
|
use gpui_component::separator::Separator;
|
||||||
use gpui_component::setting::NumberFieldOptions;
|
|
||||||
use gpui_component::switch::Switch;
|
use gpui_component::switch::Switch;
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, ThemeRegistry, WindowExt, h_flex,
|
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, WindowExt, h_flex, v_flex,
|
||||||
v_flex,
|
|
||||||
};
|
};
|
||||||
use settings::{AppearanceMode, EventFetchingStrategy, Settings, SettingsStore};
|
use settings::{AppearanceMode, EventFetchingStrategy, Settings, SettingsStore};
|
||||||
use signed_ui::{SelectOption, setting_block, setting_row};
|
use signed_ui::{SelectOption, setting_block, setting_row};
|
||||||
@@ -31,34 +24,10 @@ fn selected_index(options: &[SelectOption], value: &str) -> Option<IndexPath> {
|
|||||||
.map(|row| IndexPath::default().row(row))
|
.map(|row| IndexPath::default().row(row))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registered themes split into light and dark options, light first.
|
|
||||||
fn theme_options(cx: &App) -> (Vec<SelectOption>, Vec<SelectOption>) {
|
|
||||||
let registry = ThemeRegistry::global(cx);
|
|
||||||
let mut light = Vec::new();
|
|
||||||
let mut dark = Vec::new();
|
|
||||||
for config in registry.themes().values() {
|
|
||||||
let name = config.name.clone();
|
|
||||||
if config.mode.is_dark() {
|
|
||||||
dark.push(SelectOption::new(name.clone(), name));
|
|
||||||
} else {
|
|
||||||
light.push(SelectOption::new(name.clone(), name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
light.sort_by(|a, b| a.label().as_ref().cmp(b.label().as_ref()));
|
|
||||||
dark.sort_by(|a, b| a.label().as_ref().cmp(b.label().as_ref()));
|
|
||||||
(light, dark)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Created once when the dialog opens, so control state survives re-renders.
|
/// Created once when the dialog opens, so control state survives re-renders.
|
||||||
struct SettingsControls {
|
struct SettingsControls {
|
||||||
appearance: Entity<SelectState<Vec<SelectOption>>>,
|
appearance: Entity<SelectState<Vec<SelectOption>>>,
|
||||||
event_fetching: Entity<SelectState<Vec<SelectOption>>>,
|
event_fetching: Entity<SelectState<Vec<SelectOption>>>,
|
||||||
light_theme: Entity<SelectState<Vec<SelectOption>>>,
|
|
||||||
dark_theme: Entity<SelectState<Vec<SelectOption>>>,
|
|
||||||
font_size: Entity<InputState>,
|
|
||||||
mono_font_size: Entity<InputState>,
|
|
||||||
radius: Entity<InputState>,
|
|
||||||
radius_lg: Entity<InputState>,
|
|
||||||
grasp_server_input: Entity<InputState>,
|
grasp_server_input: Entity<InputState>,
|
||||||
/// The effective create-repository folder, shown in a disabled input.
|
/// The effective create-repository folder, shown in a disabled input.
|
||||||
default_folder: Entity<InputState>,
|
default_folder: Entity<InputState>,
|
||||||
@@ -107,38 +76,9 @@ impl SettingsControls {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let (light_options, dark_options) = theme_options(cx);
|
|
||||||
let light_theme = cx.new(|cx| {
|
|
||||||
SelectState::new(
|
|
||||||
light_options.clone(),
|
|
||||||
selected_index(&light_options, &settings.theme.light_theme),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let dark_theme = cx.new(|cx| {
|
|
||||||
SelectState::new(
|
|
||||||
dark_options.clone(),
|
|
||||||
selected_index(&dark_options, &settings.theme.dark_theme),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let font_size = cx.new(|cx| {
|
|
||||||
InputState::new(window, cx).default_value(settings.theme.font_size.to_string())
|
|
||||||
});
|
|
||||||
let mono_font_size = cx.new(|cx| {
|
|
||||||
InputState::new(window, cx).default_value(settings.theme.mono_font_size.to_string())
|
|
||||||
});
|
|
||||||
let radius = cx
|
|
||||||
.new(|cx| InputState::new(window, cx).default_value(settings.theme.radius.to_string()));
|
|
||||||
let radius_lg = cx.new(|cx| {
|
|
||||||
InputState::new(window, cx).default_value(settings.theme.radius_lg.to_string())
|
|
||||||
});
|
|
||||||
|
|
||||||
let grasp_server_input =
|
let grasp_server_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com"));
|
cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com"));
|
||||||
|
|
||||||
let default_folder = cx.new(|cx| {
|
let default_folder = cx.new(|cx| {
|
||||||
let folder = settings
|
let folder = settings
|
||||||
.create_repository
|
.create_repository
|
||||||
@@ -178,118 +118,9 @@ impl SettingsControls {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe(&light_theme, |_, event, cx| {
|
|
||||||
if let SelectEvent::Confirm(Some(value)) = event {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.edit(
|
|
||||||
|settings| settings.theme.light_theme = value.to_string(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
apply_theme(cx);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe(&dark_theme, |_, event, cx| {
|
|
||||||
if let SelectEvent::Confirm(Some(value)) = event {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.edit(|settings| settings.theme.dark_theme = value.to_string(), cx);
|
|
||||||
});
|
|
||||||
apply_theme(cx);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
wire_number_input(
|
|
||||||
&font_size,
|
|
||||||
&mut subscriptions,
|
|
||||||
window.window_handle(),
|
|
||||||
cx,
|
|
||||||
NumberFieldOptions {
|
|
||||||
min: 10.,
|
|
||||||
max: 32.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
settings.theme.font_size as f64,
|
|
||||||
|value, cx| {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.edit(|settings| settings.theme.font_size = value as f32, cx);
|
|
||||||
});
|
|
||||||
apply_theme(cx);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
wire_number_input(
|
|
||||||
&mono_font_size,
|
|
||||||
&mut subscriptions,
|
|
||||||
window.window_handle(),
|
|
||||||
cx,
|
|
||||||
NumberFieldOptions {
|
|
||||||
min: 8.,
|
|
||||||
max: 32.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
settings.theme.mono_font_size as f64,
|
|
||||||
|value, cx| {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.edit(|settings| settings.theme.mono_font_size = value as f32, cx);
|
|
||||||
});
|
|
||||||
apply_theme(cx);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
wire_number_input(
|
|
||||||
&radius,
|
|
||||||
&mut subscriptions,
|
|
||||||
window.window_handle(),
|
|
||||||
cx,
|
|
||||||
NumberFieldOptions {
|
|
||||||
min: 0.,
|
|
||||||
max: 24.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
settings.theme.radius as f64,
|
|
||||||
|value, cx| {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.edit(|settings| settings.theme.radius = value as f32, cx);
|
|
||||||
});
|
|
||||||
apply_theme(cx);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
wire_number_input(
|
|
||||||
&radius_lg,
|
|
||||||
&mut subscriptions,
|
|
||||||
window.window_handle(),
|
|
||||||
cx,
|
|
||||||
NumberFieldOptions {
|
|
||||||
min: 0.,
|
|
||||||
max: 24.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
settings.theme.radius_lg as f64,
|
|
||||||
|value, cx| {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.edit(|settings| settings.theme.radius_lg = value as f32, cx);
|
|
||||||
});
|
|
||||||
apply_theme(cx);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
appearance,
|
appearance,
|
||||||
event_fetching,
|
event_fetching,
|
||||||
light_theme,
|
|
||||||
dark_theme,
|
|
||||||
font_size,
|
|
||||||
mono_font_size,
|
|
||||||
radius,
|
|
||||||
radius_lg,
|
|
||||||
grasp_server_input,
|
grasp_server_input,
|
||||||
default_folder,
|
default_folder,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
@@ -320,8 +151,6 @@ fn settings_view(controls: &SettingsControls, cx: &mut App) -> impl IntoElement
|
|||||||
.w_full()
|
.w_full()
|
||||||
.child(appearance_section(controls, cx))
|
.child(appearance_section(controls, cx))
|
||||||
.child(Separator::horizontal())
|
.child(Separator::horizontal())
|
||||||
.child(theme_section(&settings, controls, cx))
|
|
||||||
.child(Separator::horizontal())
|
|
||||||
.child(tab_bar_section(&settings, cx))
|
.child(tab_bar_section(&settings, cx))
|
||||||
.child(Separator::horizontal())
|
.child(Separator::horizontal())
|
||||||
.child(grasp_servers_section(&settings, controls, cx))
|
.child(grasp_servers_section(&settings, controls, cx))
|
||||||
@@ -350,76 +179,6 @@ fn event_fetching_section(controls: &SettingsControls, cx: &App) -> impl IntoEle
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.gap_3()
|
|
||||||
.w_full()
|
|
||||||
.child(setting_row(
|
|
||||||
cx,
|
|
||||||
"Light Theme",
|
|
||||||
"The theme to use when the appearance is light.",
|
|
||||||
Select::new(&controls.light_theme).w_full(),
|
|
||||||
))
|
|
||||||
.child(setting_row(
|
|
||||||
cx,
|
|
||||||
"Dark Theme",
|
|
||||||
"The theme to use when the appearance is dark.",
|
|
||||||
Select::new(&controls.dark_theme).w_full(),
|
|
||||||
))
|
|
||||||
.child(setting_row(
|
|
||||||
cx,
|
|
||||||
"UI Font Size",
|
|
||||||
"Font size for the UI.",
|
|
||||||
NumberInput::new(&controls.font_size).w_full(),
|
|
||||||
))
|
|
||||||
.child(setting_row(
|
|
||||||
cx,
|
|
||||||
"Editor Font Size",
|
|
||||||
"Font size for editor text.",
|
|
||||||
NumberInput::new(&controls.mono_font_size).w_full(),
|
|
||||||
))
|
|
||||||
.child(setting_row(
|
|
||||||
cx,
|
|
||||||
"Corner Radius",
|
|
||||||
"The corner radius for UI elements.",
|
|
||||||
NumberInput::new(&controls.radius).w_full(),
|
|
||||||
))
|
|
||||||
.child(setting_row(
|
|
||||||
cx,
|
|
||||||
"Large Corner Radius",
|
|
||||||
"The corner radius for large UI elements (dialogs, notifications).",
|
|
||||||
NumberInput::new(&controls.radius_lg).w_full(),
|
|
||||||
))
|
|
||||||
.child(setting_row(
|
|
||||||
cx,
|
|
||||||
"Focus Ring",
|
|
||||||
"Draw a ring around focused controls.",
|
|
||||||
Switch::new("focus-ring")
|
|
||||||
.checked(settings.theme.focus_ring)
|
|
||||||
.on_click(move |checked: &bool, _window, cx| {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.edit(|settings| settings.theme.focus_ring = *checked, cx);
|
|
||||||
});
|
|
||||||
apply_theme(cx);
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
.child(setting_row(
|
|
||||||
cx,
|
|
||||||
"Shadows",
|
|
||||||
"The shadow effect for UI elements.",
|
|
||||||
Switch::new("shadows")
|
|
||||||
.checked(settings.theme.shadow)
|
|
||||||
.on_click(move |checked: &bool, _window, cx| {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.edit(|settings| settings.theme.shadow = *checked, cx);
|
|
||||||
});
|
|
||||||
apply_theme(cx);
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tab bar chrome, such as its next/previous tab buttons.
|
/// Tab bar chrome, such as its next/previous tab buttons.
|
||||||
fn tab_bar_section(settings: &Settings, cx: &App) -> impl IntoElement {
|
fn tab_bar_section(settings: &Settings, cx: &App) -> impl IntoElement {
|
||||||
v_flex().w_full().gap_3().child(setting_row(
|
v_flex().w_full().gap_3().child(setting_row(
|
||||||
@@ -724,79 +483,6 @@ fn choose_default_folder(default_folder: &Entity<InputState>, window: &mut Windo
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Step actions clamp and persist the value; typed changes parse, clamp and persist.
|
|
||||||
fn wire_number_input(
|
|
||||||
state: &Entity<InputState>,
|
|
||||||
subscriptions: &mut Vec<Subscription>,
|
|
||||||
window_handle: AnyWindowHandle,
|
|
||||||
cx: &mut App,
|
|
||||||
options: NumberFieldOptions,
|
|
||||||
initial: f64,
|
|
||||||
on_change: impl Fn(f64, &mut App) + 'static,
|
|
||||||
) {
|
|
||||||
let on_change = Rc::new(on_change);
|
|
||||||
let initial_value = Rc::new(Cell::new(initial));
|
|
||||||
let (min, max, step) = (options.min, options.max, options.step);
|
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe(&state.clone(), {
|
|
||||||
let state = state.clone();
|
|
||||||
let initial_value = initial_value.clone();
|
|
||||||
let on_change = on_change.clone();
|
|
||||||
move |_, event: &NumberInputEvent, cx| {
|
|
||||||
let NumberInputEvent::Step(action) = event;
|
|
||||||
let value = state.read(cx).value();
|
|
||||||
if let Ok(value) = value.parse::<f64>() {
|
|
||||||
let new_value = match action {
|
|
||||||
StepAction::Increment => value + step,
|
|
||||||
StepAction::Decrement => value - step,
|
|
||||||
};
|
|
||||||
let clamped = new_value.clamp(min, max);
|
|
||||||
window_handle
|
|
||||||
.update(cx, |_, window, cx| {
|
|
||||||
state.update(cx, |input, cx| {
|
|
||||||
input.set_value(SharedString::from(clamped.to_string()), window, cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
initial_value.set(clamped);
|
|
||||||
on_change(clamped, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe(&state.clone(), {
|
|
||||||
let state = state.clone();
|
|
||||||
let initial_value = initial_value.clone();
|
|
||||||
let on_change = on_change.clone();
|
|
||||||
move |_, event: &InputEvent, cx| {
|
|
||||||
if matches!(event, InputEvent::Change) {
|
|
||||||
let value = state.read(cx).value();
|
|
||||||
if value == initial_value.get().to_string() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Ok(parsed) = value.parse::<f64>() {
|
|
||||||
let clamped = parsed.clamp(min, max);
|
|
||||||
initial_value.set(clamped);
|
|
||||||
on_change(clamped, cx);
|
|
||||||
if clamped != parsed {
|
|
||||||
window_handle
|
|
||||||
.update(cx, |_, window, cx| {
|
|
||||||
state.update(cx, |input, cx| {
|
|
||||||
input.set_value(
|
|
||||||
SharedString::from(clamped.to_string()),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
|
fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
|
||||||
match appearance {
|
match appearance {
|
||||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||||
@@ -804,33 +490,3 @@ fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
|
|||||||
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_theme(cx: &mut App) {
|
|
||||||
let store = SettingsStore::global(cx);
|
|
||||||
let settings = store.read(cx).settings().theme.clone();
|
|
||||||
|
|
||||||
let registry = ThemeRegistry::global(cx);
|
|
||||||
let light_config = registry
|
|
||||||
.themes()
|
|
||||||
.get(settings.light_theme.as_str())
|
|
||||||
.cloned();
|
|
||||||
let dark_config = registry.themes().get(settings.dark_theme.as_str()).cloned();
|
|
||||||
let mode = Theme::global(cx).mode;
|
|
||||||
|
|
||||||
let theme = Theme::global_mut(cx);
|
|
||||||
theme.radius = px(settings.radius);
|
|
||||||
theme.radius_lg = px(settings.radius_lg);
|
|
||||||
theme.focus_ring = settings.focus_ring;
|
|
||||||
theme.shadow = settings.shadow;
|
|
||||||
theme.font_size = px(settings.font_size);
|
|
||||||
theme.mono_font_size = px(settings.mono_font_size);
|
|
||||||
if let Some(config) = light_config {
|
|
||||||
theme.light_theme = config;
|
|
||||||
}
|
|
||||||
if let Some(config) = dark_config {
|
|
||||||
theme.dark_theme = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-apply the active mode so the updated configs take effect.
|
|
||||||
Theme::change(mode, None, cx);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ gpui.workspace = true
|
|||||||
gpui_platform.workspace = true
|
gpui_platform.workspace = true
|
||||||
dock = { workspace = true }
|
dock = { workspace = true }
|
||||||
gpui-component.workspace = true
|
gpui-component.workspace = true
|
||||||
|
native-theme.workspace = true
|
||||||
|
native-theme-gpui.workspace = true
|
||||||
|
flume.workspace = true
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|||||||
+109
-41
@@ -5,7 +5,9 @@ use dock::TAB_BAR_HEIGHT;
|
|||||||
use gpui::*;
|
use gpui::*;
|
||||||
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
|
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
|
||||||
use gpui_platform::application;
|
use gpui_platform::application;
|
||||||
use settings::{AppearanceMode, SettingsStore};
|
use native_theme::watch::ThemeSubscription;
|
||||||
|
use native_theme_gpui::{SystemTheme, apply_system_theme};
|
||||||
|
use settings::{AppearanceMode, Settings, SettingsStore};
|
||||||
|
|
||||||
actions!(signed, [Quit]);
|
actions!(signed, [Quit]);
|
||||||
|
|
||||||
@@ -25,57 +27,64 @@ fn main() {
|
|||||||
// The persisted settings must load before the theme is applied.
|
// The persisted settings must load before the theme is applied.
|
||||||
let store = cx.new(|cx| SettingsStore::new(paths::settings_file(), cx));
|
let store = cx.new(|cx| SettingsStore::new(paths::settings_file(), cx));
|
||||||
SettingsStore::set_global(store.clone(), cx);
|
SettingsStore::set_global(store.clone(), cx);
|
||||||
let settings = store.read(cx).settings().clone();
|
|
||||||
|
|
||||||
let registry = ThemeRegistry::global_mut(cx);
|
let settings = store.read(cx).settings().clone();
|
||||||
for (name, content) in Assets.themes() {
|
{
|
||||||
if let Err(err) = registry.load_themes_from_str(&content) {
|
let registry = ThemeRegistry::global_mut(cx);
|
||||||
tracing::error!("Failed to load theme {name}: {err}");
|
for (name, content) in Assets.themes() {
|
||||||
|
if let Err(err) = registry.load_themes_from_str(&content) {
|
||||||
|
tracing::error!("Failed to load theme {name}: {err}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let light_theme = registry
|
|
||||||
.themes()
|
|
||||||
.get(settings.theme.light_theme.as_str())
|
|
||||||
.cloned();
|
|
||||||
let dark_theme = registry
|
|
||||||
.themes()
|
|
||||||
.get(settings.theme.dark_theme.as_str())
|
|
||||||
.cloned();
|
|
||||||
|
|
||||||
let theme = Theme::global_mut(cx);
|
match apply_native_theme(&settings, cx) {
|
||||||
theme.radius = px(settings.theme.radius);
|
Ok(()) => start_theme_watcher(cx),
|
||||||
theme.radius_lg = px(settings.theme.radius_lg);
|
Err(error) => {
|
||||||
theme.focus_ring = settings.theme.focus_ring;
|
tracing::warn!("native theme unavailable ({error})");
|
||||||
theme.shadow = settings.theme.shadow;
|
|
||||||
theme.font_size = px(settings.theme.font_size);
|
|
||||||
theme.mono_font_size = px(settings.theme.mono_font_size);
|
|
||||||
|
|
||||||
if let Some(light) = light_theme {
|
let registry = ThemeRegistry::global_mut(cx);
|
||||||
theme.light_theme = light;
|
let light_theme = registry
|
||||||
} else {
|
.themes()
|
||||||
tracing::warn!(
|
.get(settings.theme.light_theme.as_str())
|
||||||
"{} theme is missing from the registry",
|
.cloned();
|
||||||
settings.theme.light_theme
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dark) = dark_theme {
|
let dark_theme = registry
|
||||||
theme.dark_theme = dark;
|
.themes()
|
||||||
} else {
|
.get(settings.theme.dark_theme.as_str())
|
||||||
tracing::warn!(
|
.cloned();
|
||||||
"{} theme is missing from the registry",
|
|
||||||
settings.theme.dark_theme
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
match settings.appearance {
|
let theme = Theme::global_mut(cx);
|
||||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
theme.radius = px(settings.theme.radius);
|
||||||
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
theme.radius_lg = px(settings.theme.radius_lg);
|
||||||
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
theme.focus_ring = settings.theme.focus_ring;
|
||||||
|
theme.shadow = settings.theme.shadow;
|
||||||
|
theme.font_size = px(settings.theme.font_size);
|
||||||
|
theme.mono_font_size = px(settings.theme.mono_font_size);
|
||||||
|
|
||||||
|
if let Some(light) = light_theme {
|
||||||
|
theme.light_theme = light;
|
||||||
|
} else {
|
||||||
|
tracing::warn!("{} theme is missing", settings.theme.light_theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dark) = dark_theme {
|
||||||
|
theme.dark_theme = dark;
|
||||||
|
} else {
|
||||||
|
tracing::warn!("{} theme is missing", settings.theme.dark_theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
match settings.appearance {
|
||||||
|
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||||
|
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
||||||
|
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
||||||
std::fs::create_dir_all(paths::repos_dir()).ok();
|
std::fs::create_dir_all(paths::repos_dir()).ok();
|
||||||
|
|
||||||
signed_state::init(
|
signed_state::init(
|
||||||
paths::nostr_dir(),
|
paths::nostr_dir(),
|
||||||
paths::repos_dir().clone(),
|
paths::repos_dir().clone(),
|
||||||
@@ -115,3 +124,62 @@ fn main() {
|
|||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply the OS theme to the app, scaling the font size to match the system font size.
|
||||||
|
fn apply_native_theme(settings: &Settings, cx: &mut App) -> native_theme::Result<()> {
|
||||||
|
let mut system = SystemTheme::from_system()?;
|
||||||
|
let native = system.light.defaults.font.size;
|
||||||
|
if native > 0.0 {
|
||||||
|
let scaling = settings.theme.font_size / native;
|
||||||
|
system.accessibility.text_scaling_factor *= scaling;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_system_theme(&system, cx);
|
||||||
|
|
||||||
|
match settings.appearance {
|
||||||
|
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||||
|
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
||||||
|
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds the watcher subscription so it lives as long as the app.
|
||||||
|
struct ThemeWatcher {
|
||||||
|
_subscription: ThemeSubscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Global for ThemeWatcher {}
|
||||||
|
|
||||||
|
/// Re-apply the OS theme whenever it changes at runtime.
|
||||||
|
fn start_theme_watcher(cx: &mut App) {
|
||||||
|
let (sender, receiver) = flume::unbounded();
|
||||||
|
match native_theme::watch::on_theme_change(move |event| {
|
||||||
|
if let Err(error) = sender.send(event) {
|
||||||
|
tracing::warn!("theme change dropped ({error})");
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(subscription) => {
|
||||||
|
cx.set_global(ThemeWatcher {
|
||||||
|
_subscription: subscription,
|
||||||
|
});
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
while receiver.recv_async().await.is_ok() {
|
||||||
|
tracing::info!("OS theme changed; re-applying");
|
||||||
|
cx.update(|cx| {
|
||||||
|
let store = SettingsStore::global(cx);
|
||||||
|
let settings = store.read(cx).settings().clone();
|
||||||
|
if let Err(error) = apply_native_theme(&settings, cx) {
|
||||||
|
tracing::warn!("failed to apply updated OS theme ({error})");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!("OS theme watching unavailable ({error})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user