Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e8ceaf2db | ||
|
|
688b1ce418 |
Generated
+191
-77
@@ -1707,12 +1707,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "configparser"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "031ad3d167622974ed636505ae4556267ed4aa2aea0464122fa6bc9698a32825"
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
@@ -1811,8 +1805,7 @@ dependencies = [
|
||||
"auto_update",
|
||||
"chat",
|
||||
"chat_ui",
|
||||
"flume 0.11.1",
|
||||
"gpui-kit",
|
||||
"gpui-native-kit",
|
||||
"gpui-pre",
|
||||
"gpui-pre-linux",
|
||||
"gpui-pre-macos",
|
||||
@@ -1820,14 +1813,13 @@ dependencies = [
|
||||
"gpui-pre-reqwest-client",
|
||||
"gpui-pre-windows",
|
||||
"log",
|
||||
"native-theme",
|
||||
"native-theme-gpui",
|
||||
"nip29",
|
||||
"nip4e",
|
||||
"nip60",
|
||||
"nostr-sdk",
|
||||
"person",
|
||||
"settings",
|
||||
"speech",
|
||||
"state",
|
||||
"tracing-subscriber",
|
||||
"ui",
|
||||
@@ -1846,7 +1838,7 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"gpui-kit",
|
||||
"gpui-native-kit",
|
||||
"gpui-pre",
|
||||
"gpui-pre-platform",
|
||||
"gpui-pre-web",
|
||||
@@ -1857,6 +1849,7 @@ dependencies = [
|
||||
"nip4e",
|
||||
"person",
|
||||
"settings",
|
||||
"speech",
|
||||
"state",
|
||||
"tracing-wasm",
|
||||
"ui",
|
||||
@@ -3638,6 +3631,114 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-native-core"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=b75968ee0fecf78c3041248f1f6bfe8bfd85dee0#b75968ee0fecf78c3041248f1f6bfe8bfd85dee0"
|
||||
dependencies = [
|
||||
"gpui-native-icons",
|
||||
"gpui-native-theme",
|
||||
"gpui-native-utils",
|
||||
"gpui-pre",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-native-data"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=b75968ee0fecf78c3041248f1f6bfe8bfd85dee0#b75968ee0fecf78c3041248f1f6bfe8bfd85dee0"
|
||||
dependencies = [
|
||||
"gpui-native-core",
|
||||
"gpui-native-icons",
|
||||
"gpui-native-theme",
|
||||
"gpui-native-utils",
|
||||
"gpui-pre",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-native-editor"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=b75968ee0fecf78c3041248f1f6bfe8bfd85dee0#b75968ee0fecf78c3041248f1f6bfe8bfd85dee0"
|
||||
dependencies = [
|
||||
"gpui-native-core",
|
||||
"gpui-native-icons",
|
||||
"gpui-native-theme",
|
||||
"gpui-native-utils",
|
||||
"gpui-pre",
|
||||
"pulldown-cmark",
|
||||
"similar",
|
||||
"tree-sitter",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-native-icons"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=b75968ee0fecf78c3041248f1f6bfe8bfd85dee0#b75968ee0fecf78c3041248f1f6bfe8bfd85dee0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui-pre",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-native-kit"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=b75968ee0fecf78c3041248f1f6bfe8bfd85dee0#b75968ee0fecf78c3041248f1f6bfe8bfd85dee0"
|
||||
dependencies = [
|
||||
"gpui-native-core",
|
||||
"gpui-native-data",
|
||||
"gpui-native-editor",
|
||||
"gpui-native-icons",
|
||||
"gpui-native-layout",
|
||||
"gpui-native-theme",
|
||||
"gpui-pre",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-native-layout"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=b75968ee0fecf78c3041248f1f6bfe8bfd85dee0#b75968ee0fecf78c3041248f1f6bfe8bfd85dee0"
|
||||
dependencies = [
|
||||
"gpui-native-core",
|
||||
"gpui-native-data",
|
||||
"gpui-native-icons",
|
||||
"gpui-native-theme",
|
||||
"gpui-native-utils",
|
||||
"gpui-pre",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-native-theme"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=b75968ee0fecf78c3041248f1f6bfe8bfd85dee0#b75968ee0fecf78c3041248f1f6bfe8bfd85dee0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"block2 0.6.2",
|
||||
"gpui-pre",
|
||||
"notify",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"serde",
|
||||
"toml 0.8.23",
|
||||
"windows 0.62.2",
|
||||
"windows-registry 0.6.1",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-native-utils"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=b75968ee0fecf78c3041248f1f6bfe8bfd85dee0#b75968ee0fecf78c3041248f1f6bfe8bfd85dee0"
|
||||
dependencies = [
|
||||
"gpui-native-theme",
|
||||
"gpui-pre",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-pre"
|
||||
version = "0.3.10"
|
||||
@@ -3678,7 +3779,7 @@ dependencies = [
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"pin-project",
|
||||
"pollster 0.4.0",
|
||||
"pollster",
|
||||
"profiling",
|
||||
"rand 0.9.5",
|
||||
"raw-window-handle",
|
||||
@@ -5918,62 +6019,6 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-theme"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef2a6c2d7520b94d53802c0c94d0450280a7c10380246756661bd1c6ffc64dd1"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"ashpd",
|
||||
"async-io",
|
||||
"async-trait",
|
||||
"block2 0.6.2",
|
||||
"configparser",
|
||||
"fontdb",
|
||||
"inventory",
|
||||
"native-theme-derive",
|
||||
"notify",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-text",
|
||||
"objc2-foundation 0.3.2",
|
||||
"pollster 1.0.1",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"toml 1.1.8+spec-1.1.0",
|
||||
"unicase",
|
||||
"windows 0.62.2",
|
||||
"windows-version",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-theme-derive"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba9ec586e35be9d05bdd279adc9cbe213b5bd630536b8066b82caaaf4021dba7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-theme-gpui"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e13cbaa86f1f0a1ac947380f5d8d47a5e9f7f939c77afd05d7039041abae146"
|
||||
dependencies = [
|
||||
"gpui-base",
|
||||
"gpui-component",
|
||||
"gpui-kit",
|
||||
"gpui-pre",
|
||||
"image",
|
||||
"native-theme",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
@@ -6781,8 +6826,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2 0.6.2",
|
||||
"libc",
|
||||
"objc2 0.6.5",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
@@ -7587,12 +7630,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336"
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
@@ -7740,6 +7777,17 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3db2be46835f013dd3f4fa7f5f7402c537787b54ebb6fe2b94bbdd7e2669bb7b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulp"
|
||||
version = "0.22.3"
|
||||
@@ -9047,7 +9095,6 @@ version = "2.0.0-alpha3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"gpui-kit",
|
||||
"gpui-pre",
|
||||
"log",
|
||||
"nostr-sdk",
|
||||
@@ -9463,6 +9510,12 @@ version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6"
|
||||
|
||||
[[package]]
|
||||
name = "streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
|
||||
|
||||
[[package]]
|
||||
name = "strength_reduce"
|
||||
version = "0.2.4"
|
||||
@@ -10270,6 +10323,66 @@ dependencies = [
|
||||
"strength_reduce",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"serde_json",
|
||||
"streaming-iterator",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-javascript"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-language"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0af592be68c579aa78a16846bd19422978c3c52e438523d45ff5d1bff1f9d4a"
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-python"
|
||||
version = "0.23.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-rust"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-typescript"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.17"
|
||||
@@ -11957,6 +12070,7 @@ dependencies = [
|
||||
"common",
|
||||
"gpui-base",
|
||||
"gpui-kit",
|
||||
"gpui-native-kit",
|
||||
"gpui-pre",
|
||||
"instant",
|
||||
"log",
|
||||
|
||||
@@ -25,6 +25,8 @@ gpui-kit = "0.8.1"
|
||||
native-theme = { version = "0.7", features = ["native", "watch"] }
|
||||
native-theme-gpui = { version = "0.7", default-features = false, features = ["system-fonts"] }
|
||||
|
||||
gpui-native-kit = { git = "https://git.reya.info/reya/gpui-native-kit", rev = "b75968ee0fecf78c3041248f1f6bfe8bfd85dee0", features = ["core", "data", "layout", "editor"] }
|
||||
|
||||
# Nostr
|
||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||
|
||||
@@ -5,7 +5,6 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gpui-kit.workspace = true
|
||||
common = { path = "../common" }
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
|
||||
+5
-104
@@ -3,7 +3,7 @@ use std::path::PathBuf;
|
||||
use anyhow::{Error, anyhow};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use common::config_dir;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task, px};
|
||||
use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
@@ -36,33 +36,6 @@ impl AppearanceMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// The default base font size for the interface
|
||||
pub const DEFAULT_FONT_SIZE: f32 = 16.0;
|
||||
|
||||
/// User tuning applied on top of the native OS theme
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ThemeSettings {
|
||||
/// The base UI font size in pixels
|
||||
pub font_size: f32,
|
||||
/// Draw a ring around focused controls
|
||||
pub focus_ring: bool,
|
||||
/// Corner radius for general elements; follows the OS theme when unset
|
||||
pub radius: Option<f32>,
|
||||
/// Corner radius for large elements like dialogs; follows the OS theme when unset
|
||||
pub radius_lg: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for ThemeSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: DEFAULT_FONT_SIZE,
|
||||
focus_ring: false,
|
||||
radius: None,
|
||||
radius_lg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AppSettings::set_global(cx.new(AppSettings::new), cx)
|
||||
}
|
||||
@@ -91,13 +64,6 @@ pub fn persisted_appearance_mode() -> AppearanceMode {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Read the persisted theme tuning synchronously.
|
||||
pub fn persisted_theme() -> ThemeSettings {
|
||||
persisted_settings()
|
||||
.map(|settings| settings.theme)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
const DEFAULT_FILE_SERVER: &str = "https://nostr.download/";
|
||||
const LEGACY_FILE_SERVER: &str = "blossom.band";
|
||||
/// Minimum days between automatic encryption key rotations
|
||||
@@ -160,7 +126,6 @@ setting_accessors! {
|
||||
}
|
||||
|
||||
setting_getters! {
|
||||
pub theme: ThemeSettings,
|
||||
pub pinned_rooms: Vec<u64>,
|
||||
pub pinned_groups: Vec<String>,
|
||||
}
|
||||
@@ -234,9 +199,6 @@ pub struct Settings {
|
||||
/// Hide the notice that messages in public groups are not encrypted
|
||||
#[serde(default)]
|
||||
pub hide_group_notice: bool,
|
||||
/// Tuning applied on top of the native OS theme
|
||||
#[serde(default)]
|
||||
pub theme: ThemeSettings,
|
||||
/// Show an image behind chat and group panels
|
||||
#[serde(default)]
|
||||
pub panel_background: bool,
|
||||
@@ -265,7 +227,6 @@ impl Default for Settings {
|
||||
nip4e: false,
|
||||
nip4e_rotation_days: None,
|
||||
hide_group_notice: false,
|
||||
theme: ThemeSettings::default(),
|
||||
panel_background: true,
|
||||
panel_background_path: None,
|
||||
trusted_relays: vec![],
|
||||
@@ -330,52 +291,6 @@ impl AppSettings {
|
||||
});
|
||||
}
|
||||
|
||||
/// Edit the theme tuning
|
||||
pub fn update_theme(edit: impl FnOnce(&mut ThemeSettings), cx: &mut App) {
|
||||
Self::global(cx).update(cx, |this, cx| {
|
||||
this.inner.update(cx, |inner, cx| {
|
||||
edit(&mut inner.theme);
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn apply_theme_style(cx: &mut App) {
|
||||
Self::apply_theme_tuning(Self::get_theme(cx), cx);
|
||||
}
|
||||
|
||||
fn apply_theme_tuning(tuning: ThemeSettings, cx: &mut App) {
|
||||
use gpui_kit::component::Theme;
|
||||
|
||||
{
|
||||
let styled = Theme::global_mut(cx);
|
||||
styled.font_size = px(tuning.font_size);
|
||||
styled.focus_ring = tuning.focus_ring;
|
||||
if let Some(radius) = tuning.radius {
|
||||
styled.radius = px(radius);
|
||||
}
|
||||
if let Some(radius_lg) = tuning.radius_lg {
|
||||
styled.radius_lg = px(radius_lg);
|
||||
}
|
||||
}
|
||||
|
||||
Theme::update(cx, |styled| {
|
||||
let background = styled.background;
|
||||
let foreground = styled.foreground;
|
||||
let (hover_alpha, active_alpha) = if styled.is_dark() {
|
||||
(0.08, 0.12)
|
||||
} else {
|
||||
(0.04, 0.06)
|
||||
};
|
||||
if styled.list_hover == background {
|
||||
styled.list_hover = foreground.alpha(hover_alpha);
|
||||
}
|
||||
if styled.tab_active == background {
|
||||
styled.tab_active = foreground.alpha(active_alpha);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Load settings
|
||||
fn load(&mut self, cx: &mut Context<Self>) {
|
||||
let task: Task<Result<Settings, Error>> = cx.background_spawn(async move {
|
||||
@@ -408,7 +323,6 @@ impl AppSettings {
|
||||
// Update settings
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_settings(settings, cx);
|
||||
this.apply_theme(cx);
|
||||
cx.refresh_windows();
|
||||
})
|
||||
.ok();
|
||||
@@ -434,20 +348,6 @@ impl AppSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the persisted appearance mode to the app
|
||||
pub fn apply_theme(&mut self, cx: &mut Context<Self>) {
|
||||
use gpui_kit::component::Theme;
|
||||
|
||||
let mode = self.inner.read(cx).appearance_mode;
|
||||
match mode {
|
||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||
AppearanceMode::Light => Theme::change(gpui_kit::component::ThemeMode::Light, None, cx),
|
||||
AppearanceMode::Dark => Theme::change(gpui_kit::component::ThemeMode::Dark, None, cx),
|
||||
}
|
||||
let tuning = self.inner.read(cx).theme;
|
||||
Self::apply_theme_tuning(tuning, cx);
|
||||
}
|
||||
|
||||
/// Check if decoupling encryption key is enabled
|
||||
pub fn is_nip4e_enabled(&self, cx: &App) -> bool {
|
||||
self.inner.read(cx).nip4e
|
||||
@@ -507,9 +407,9 @@ impl AppSettings {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Settings written before theme tuning existed must keep loading.
|
||||
/// Settings written by older versions, including theme tuning, must keep loading.
|
||||
#[test]
|
||||
fn settings_without_theme_keep_defaults() {
|
||||
fn legacy_settings_keep_loading() {
|
||||
let settings: Settings = serde_json::from_str(
|
||||
r#"{
|
||||
"appearance_mode": "Dark",
|
||||
@@ -517,12 +417,13 @@ mod tests {
|
||||
"screening": true,
|
||||
"wot_auto_trust": true,
|
||||
"nip4e": false,
|
||||
"theme": { "font_size": 18.0, "focus_ring": true },
|
||||
"trusted_relays": [],
|
||||
"file_server": "https://nostr.download/"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(settings.theme, ThemeSettings::default());
|
||||
assert_eq!(settings.appearance_mode, AppearanceMode::Dark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ pub fn recognizer() -> impl SpeechRecognizer {
|
||||
parakeet::ParakeetRecognizer
|
||||
}
|
||||
|
||||
/// Initializes the old component kit that the retained speech widgets still use.
|
||||
pub fn init_compat(cx: &mut App) {
|
||||
gpui_kit::init(cx);
|
||||
}
|
||||
|
||||
/// The recognizer backing the composer's microphone button.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn recognizer() -> impl SpeechRecognizer {
|
||||
|
||||
@@ -7,6 +7,7 @@ publish.workspace = true
|
||||
[dependencies]
|
||||
ui = { path = "../ui" }
|
||||
gpui-kit.workspace = true
|
||||
gpui-native-kit.workspace = true
|
||||
common = { path = "../common" }
|
||||
state = { path = "../state" }
|
||||
nip29 = { path = "../nip29" }
|
||||
|
||||
@@ -13,7 +13,7 @@ use gpui::{
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState, NumberInput};
|
||||
use gpui_kit::component::input::{Input, InputState};
|
||||
use gpui_kit::component::kbd::Kbd;
|
||||
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
|
||||
use gpui_kit::component::notification::Notification;
|
||||
@@ -27,19 +27,12 @@ use settings::{AppSettings, AppearanceMode, NIP4E_ROTATION_DEFAULT_DAYS};
|
||||
use speech::model::{self as speech_model};
|
||||
use ui::{settings_block, settings_row};
|
||||
|
||||
/// Bounds for the theme number inputs
|
||||
const FONT_SIZE_RANGE: (f32, f32) = (10., 32.);
|
||||
const RADIUS_RANGE: (f32, f32) = (0., 24.);
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||
cx.new(|cx| Preferences::new(window, cx))
|
||||
}
|
||||
|
||||
pub struct Preferences {
|
||||
file_input: Entity<InputState>,
|
||||
font_size_input: Entity<InputState>,
|
||||
radius_input: Entity<InputState>,
|
||||
radius_lg_input: Entity<InputState>,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mint_input: Entity<InputState>,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -57,61 +50,9 @@ impl Preferences {
|
||||
.placeholder("https://myblossom.com")
|
||||
});
|
||||
|
||||
let theme = AppSettings::get_theme(cx);
|
||||
let font_size_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(theme.font_size.to_string())
|
||||
.step(1.)
|
||||
.min(FONT_SIZE_RANGE.0 as f64)
|
||||
.max(FONT_SIZE_RANGE.1 as f64)
|
||||
});
|
||||
let radius_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(
|
||||
theme
|
||||
.radius
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.placeholder("System")
|
||||
.step(1.)
|
||||
.min(RADIUS_RANGE.0 as f64)
|
||||
.max(RADIUS_RANGE.1 as f64)
|
||||
});
|
||||
let radius_lg_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(
|
||||
theme
|
||||
.radius_lg
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.placeholder("System")
|
||||
.step(1.)
|
||||
.min(RADIUS_RANGE.0 as f64)
|
||||
.max(RADIUS_RANGE.1 as f64)
|
||||
});
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mint_input = cx.new(|cx| InputState::new(window, cx).placeholder("https://mint…"));
|
||||
|
||||
let subscriptions = vec![
|
||||
Self::wire_number_input(&font_size_input, FONT_SIZE_RANGE, cx, |value, cx| {
|
||||
if let Some(font_size) = value {
|
||||
AppSettings::update_theme(|theme| theme.font_size = font_size, cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}
|
||||
}),
|
||||
Self::wire_number_input(&radius_input, RADIUS_RANGE, cx, |value, cx| {
|
||||
AppSettings::update_theme(|theme| theme.radius = value, cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}),
|
||||
Self::wire_number_input(&radius_lg_input, RADIUS_RANGE, cx, |value, cx| {
|
||||
AppSettings::update_theme(|theme| theme.radius_lg = value, cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}),
|
||||
];
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _speech_poll = cx.spawn_in(window, async move |this, cx| {
|
||||
let timer = Duration::from_millis(500);
|
||||
@@ -141,43 +82,14 @@ impl Preferences {
|
||||
|
||||
Self {
|
||||
file_input,
|
||||
font_size_input,
|
||||
radius_input,
|
||||
radius_lg_input,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mint_input,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
_speech_poll,
|
||||
_subscriptions: subscriptions,
|
||||
_subscriptions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist a theme number as it changes; an empty value stores `None`
|
||||
fn wire_number_input(
|
||||
state: &Entity<InputState>,
|
||||
range: (f32, f32),
|
||||
cx: &mut Context<Self>,
|
||||
persist: impl Fn(Option<f32>, &mut App) + 'static,
|
||||
) -> Subscription {
|
||||
let (min, max) = range;
|
||||
let state = state.clone();
|
||||
|
||||
cx.subscribe(&state, move |_, state, event: &InputEvent, cx| {
|
||||
if !matches!(event, InputEvent::Change) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = state
|
||||
.read(cx)
|
||||
.value()
|
||||
.trim()
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
.map(|value| value.clamp(min, max));
|
||||
persist(value, cx);
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the file server (blossom) URL
|
||||
fn update_file_server(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let value = self.file_input.read(cx).value();
|
||||
@@ -245,9 +157,7 @@ impl Preferences {
|
||||
/// Set the appearance mode (system, light or dark)
|
||||
fn set_appearance_mode(mode: AppearanceMode, cx: &mut App) {
|
||||
AppSettings::update_appearance_mode(mode, cx);
|
||||
AppSettings::global(cx).update(cx, |settings, cx| {
|
||||
settings.apply_theme(cx);
|
||||
});
|
||||
crate::apply_appearance_mode(mode, cx);
|
||||
}
|
||||
|
||||
/// Ask the user for an image to show behind chat and group panels
|
||||
@@ -388,11 +298,6 @@ 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 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 =
|
||||
"Corner radius for large elements like dialogs. Empty follows the OS theme.";
|
||||
const FOCUS_RING: &str = "Draw a ring around focused controls.";
|
||||
const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
|
||||
const NIP4E_ROTATION: &str = "Automatically rotate the encryption key every 30 days, keeping the previous key for messages still sealed to it.";
|
||||
const BACKGROUND: &str = "Show an image behind chat and group panels.";
|
||||
@@ -404,7 +309,6 @@ impl Render for Preferences {
|
||||
let nip4e = AppSettings::get_nip4e(cx);
|
||||
let nip4e_rotation_days = AppSettings::get_nip4e_rotation_days(cx);
|
||||
let appearance_mode = AppSettings::get_appearance_mode(cx);
|
||||
let theme = AppSettings::get_theme(cx);
|
||||
let panel_background = AppSettings::get_panel_background(cx);
|
||||
let panel_background_path = AppSettings::get_panel_background_path(cx);
|
||||
|
||||
@@ -472,44 +376,6 @@ impl Render for Preferences {
|
||||
cx,
|
||||
))
|
||||
.child(Separator::horizontal())
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.child(settings_row(
|
||||
"Font Size",
|
||||
FONT_SIZE,
|
||||
NumberInput::new(&self.font_size_input).w_24(),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Corner Radius",
|
||||
RADIUS,
|
||||
NumberInput::new(&self.radius_input).w_24(),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Large Corner Radius",
|
||||
RADIUS_LG,
|
||||
NumberInput::new(&self.radius_lg_input).w_24(),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Focus Ring",
|
||||
FOCUS_RING,
|
||||
Switch::new("focus-ring")
|
||||
.checked(theme.focus_ring)
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::update_theme(
|
||||
|theme| theme.focus_ring = !theme.focus_ring,
|
||||
cx,
|
||||
);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(Separator::horizontal())
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::settings::{AppSettings, AppearanceMode};
|
||||
use ::settings::AppearanceMode;
|
||||
use anyhow::Error;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use auto_update::AutoUpdater;
|
||||
@@ -14,7 +14,7 @@ use gpui::{
|
||||
use gpui_kit::component::button::Button;
|
||||
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, WindowExt, v_flex};
|
||||
use nip4e::{DeviceEvent, DeviceRegistry};
|
||||
use nip29::{GroupId, GroupKey};
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -42,6 +42,31 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
||||
cx.new(|cx| Workspace::new(window, cx))
|
||||
}
|
||||
|
||||
/// Apply the appearance mode to the kit and mirror it into the old theme
|
||||
/// that the speech components still use.
|
||||
pub fn apply_appearance_mode(mode: AppearanceMode, cx: &mut App) {
|
||||
match mode {
|
||||
AppearanceMode::System => {
|
||||
gpui_native_kit::theme::follow_system(cx);
|
||||
gpui_kit::component::Theme::sync_system_appearance(None, cx);
|
||||
}
|
||||
AppearanceMode::Light => {
|
||||
gpui_native_kit::theme::set_mode_override(
|
||||
Some(gpui_native_kit::theme::ThemeVariant::Light),
|
||||
cx,
|
||||
);
|
||||
gpui_kit::component::Theme::change(gpui_kit::component::ThemeMode::Light, None, cx);
|
||||
}
|
||||
AppearanceMode::Dark => {
|
||||
gpui_native_kit::theme::set_mode_override(
|
||||
Some(gpui_native_kit::theme::ThemeVariant::Dark),
|
||||
cx,
|
||||
);
|
||||
gpui_kit::component::Theme::change(gpui_kit::component::ThemeMode::Dark, None, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceNotifcation;
|
||||
struct MsgRelayNotification;
|
||||
|
||||
@@ -93,10 +118,7 @@ impl Workspace {
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.observe_window_appearance(window, |_this, window, cx| {
|
||||
if AppSettings::get_appearance_mode(cx) == AppearanceMode::System {
|
||||
Theme::sync_system_appearance(Some(window), cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}
|
||||
gpui_native_kit::theme::Theme::sync_from_window(window, cx);
|
||||
}));
|
||||
|
||||
subscriptions.push(
|
||||
@@ -536,8 +558,8 @@ impl Workspace {
|
||||
|
||||
impl Render for Workspace {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// Dialog, notification and tooltip layers are owned by the framework
|
||||
// Root that gpui_kit::open_window installed for this window.
|
||||
// Dialog, notification and tooltip layers previously came from the
|
||||
// old kit's window root; they move to common dialogs and toasts.
|
||||
div()
|
||||
.id("workspace")
|
||||
.on_action(cx.listener(Self::on_command))
|
||||
|
||||
+2
-4
@@ -47,13 +47,11 @@ person = { path = "../crates/person" }
|
||||
|
||||
gpui.workspace = true
|
||||
gpui_platform.workspace = true
|
||||
gpui-kit.workspace = true
|
||||
gpui_linux.workspace = true
|
||||
gpui_windows.workspace = true
|
||||
gpui_macos.workspace = true
|
||||
native-theme.workspace = true
|
||||
native-theme-gpui.workspace = true
|
||||
flume.workspace = true
|
||||
gpui-native-kit.workspace = true
|
||||
speech = { path = "../crates/speech" }
|
||||
reqwest_client.workspace = true
|
||||
|
||||
log.workspace = true
|
||||
|
||||
+21
-90
@@ -2,18 +2,14 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use chat_ui::ToggleDictation;
|
||||
use gpui::{
|
||||
App, Bounds, Global, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||
actions, point, px, size,
|
||||
App, Bounds, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions, WindowBounds,
|
||||
WindowDecorations, WindowKind, WindowOptions, actions, point, px, size,
|
||||
};
|
||||
use gpui_kit::component::{Theme, ThemeMode};
|
||||
use gpui_native_kit::theme::ActiveTheme;
|
||||
use gpui_platform::application;
|
||||
use native_theme::watch::ThemeSubscription;
|
||||
use native_theme_gpui::apply_system_theme;
|
||||
use nostr_sdk::prelude::SecretKey;
|
||||
use settings::{AppSettings, AppearanceMode};
|
||||
use state::{APP_ID, CLIENT_NAME};
|
||||
use workspace::TAB_BAR_HEIGHT;
|
||||
use workspace::{TAB_BAR_HEIGHT, apply_appearance_mode};
|
||||
|
||||
actions!(coop, [Quit]);
|
||||
|
||||
@@ -34,8 +30,9 @@ fn main() {
|
||||
.with_assets(assets::CombinedAssets)
|
||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()))
|
||||
.run(move |cx| {
|
||||
// Initialize the framework before anything renders
|
||||
gpui_kit::init(cx);
|
||||
// Initialize the frameworks before anything renders
|
||||
gpui_native_kit::init(cx);
|
||||
speech::init_compat(cx);
|
||||
|
||||
// Load embedded fonts in assets/fonts
|
||||
load_embedded_fonts(cx);
|
||||
@@ -71,14 +68,7 @@ fn main() {
|
||||
settings::init(cx);
|
||||
|
||||
let appearance = settings::persisted_appearance_mode();
|
||||
let font_size = settings::persisted_theme().font_size;
|
||||
|
||||
if let Err(error) = apply_native_theme(appearance, font_size, cx) {
|
||||
log::warn!("native theme unavailable ({error})");
|
||||
apply_appearance(appearance, cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}
|
||||
start_theme_watcher(cx);
|
||||
apply_appearance_mode(appearance, cx);
|
||||
|
||||
// Initialize the nostr client
|
||||
state::init(cx, cli_key);
|
||||
@@ -110,7 +100,7 @@ fn main() {
|
||||
|
||||
// Set up the window options
|
||||
let opts = WindowOptions {
|
||||
window_background: WindowBackgroundAppearance::Opaque,
|
||||
window_background: cx.theme().window_background,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
kind: WindowKind::Normal,
|
||||
@@ -127,8 +117,18 @@ fn main() {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
gpui_kit::open_window(opts, cx, workspace::init)
|
||||
.expect("Failed to open window. Please restart the application.");
|
||||
cx.open_window(opts, |window, cx| {
|
||||
gpui_native_kit::theme::Theme::sync_from_window(window, cx);
|
||||
gpui_native_kit::theme::Theme::sync_window_rem_size(window, cx);
|
||||
window
|
||||
.observe_window_appearance(|window, cx| {
|
||||
gpui_native_kit::theme::Theme::sync_from_window(window, cx);
|
||||
cx.refresh_windows();
|
||||
})
|
||||
.detach();
|
||||
workspace::init(window, cx)
|
||||
})
|
||||
.expect("Failed to open window. Please restart the application.");
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
@@ -181,72 +181,3 @@ fn quit(_ev: &Quit, cx: &mut App) {
|
||||
log::info!("Gracefully quitting the application . . .");
|
||||
cx.quit();
|
||||
}
|
||||
|
||||
/// Apply the native theme to the app, scaled to the user's font size,
|
||||
/// then layer the persisted theme tuning on top
|
||||
fn apply_native_theme(
|
||||
appearance: AppearanceMode,
|
||||
font_size: f32,
|
||||
cx: &mut App,
|
||||
) -> native_theme::Result<()> {
|
||||
let mut system = native_theme_gpui::SystemTheme::from_system()?;
|
||||
let native = system.light.defaults.font.size;
|
||||
|
||||
if native > 0.0 {
|
||||
system.accessibility.text_scaling_factor *= font_size / native;
|
||||
}
|
||||
|
||||
apply_system_theme(&system, cx);
|
||||
apply_appearance(appearance, cx);
|
||||
// Applying the native theme replaces the whole theme, so the user's
|
||||
// tuning must be layered on afterwards.
|
||||
AppSettings::apply_theme_style(cx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Honor the appearance mode against the installed (native) palettes
|
||||
fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
|
||||
match 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the watcher subscription so it lives as long as the app.
|
||||
struct ThemeWatcher {
|
||||
_subscription: ThemeSubscription,
|
||||
}
|
||||
|
||||
impl Global for ThemeWatcher {}
|
||||
|
||||
/// Start a background task to watch for OS theme changes and re-apply the native theme.
|
||||
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) {
|
||||
log::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() {
|
||||
log::info!("OS theme changed; re-applying");
|
||||
cx.update(|cx| {
|
||||
let appearance = AppSettings::get_appearance_mode(cx);
|
||||
let font_size = AppSettings::get_theme(cx).font_size;
|
||||
if let Err(error) = apply_native_theme(appearance, font_size, cx) {
|
||||
log::warn!("failed to apply updated OS theme ({error})");
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Err(error) => log::warn!("OS theme watching unavailable ({error})"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
# Migrate coop to gpui-native-kit
|
||||
|
||||
## Context
|
||||
|
||||
`coop` (workspace root `/Users/reya/Documents/Git/coop`) builds every screen on `gpui-kit` 0.8.1 (a facade over `gpui-component`/`gpui-base`) plus `native-theme` 0.7. The target is `gpui-native-kit` at `/Users/reya/Documents/Git/gpui-native-kit` (git `ssh://git@git.reya.info/reya/gpui-native-kit.git`), using its theme, icons, core components, data views, editor markdown, and layout dock. The app must end with one design system: delete local duplicates (`ui::Avatar`, `settings_row`, `settings_block`, `title_bar_drag_handlers`, the old dock skin/tab bar/window controls), keep app-specific product views (`MessageRow`, `ReactionBar`, `Nav`, etc.) rebased on kit primitives and tokens. `gpui-kit` remains only as the speech compatibility island because `gpui-native-kit` has no speech module.
|
||||
|
||||
Scope decisions taken: only design-system duplicates are deleted; product-specific views (`MessageRow`, `ReactionBar`, `Nav`, etc.) stay and are rebased on kit primitives/tokens. Speech/dictation stays on the old `gpui-kit` because the new kit has no speech module; that is the only remaining old-kit dependency.
|
||||
|
||||
The new kit uses gpui from zed git; coop uses the crates.io `gpui-pre` 0.3.10 package. Both must resolve to one `gpui` crate. The kit's workspace manifest is therefore the first edit.
|
||||
|
||||
## Step 0 — dependency wiring (prerequisite, outside coop)
|
||||
|
||||
0. Create the working branches before any edit:
|
||||
- In `/Users/reya/Documents/Git/coop`: `git switch -c feat/gpui-native-kit-migration`.
|
||||
- In `/Users/reya/Documents/Git/gpui-native-kit`: `git switch -c feat/gpui-pre`.
|
||||
1. In `/Users/reya/Documents/Git/gpui-native-kit/Cargo.toml`, change `[workspace.dependencies]` to the same gpui family coop already uses:
|
||||
|
||||
```toml
|
||||
gpui = { package = "gpui-pre", version = "0.3.10" }
|
||||
gpui_platform = { package = "gpui-pre-platform", version = "0.3.10", features = ["font-kit"] }
|
||||
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.10" }
|
||||
```
|
||||
|
||||
2. In `/Users/reya/Documents/Git/gpui-native-kit/src/lib.rs`, add the missing editor re-export:
|
||||
|
||||
```rust
|
||||
#[cfg(feature = "editor")]
|
||||
pub use gpui_native_editor as editor;
|
||||
```
|
||||
|
||||
3. Run `cargo check --workspace` in the kit; fix only API drift between zed rev `504a7f2` and `gpui-pre` 0.3.10. Commit and record the revision.
|
||||
|
||||
4. In coop's root `Cargo.toml`, add the dependency:
|
||||
|
||||
```toml
|
||||
gpui-native-kit = { git = "https://git.reya.info/reya/gpui-native-kit", rev = "<new-rev>", features = ["core", "data", "layout", "editor"] }
|
||||
```
|
||||
|
||||
For local iteration use `path = "../gpui-native-kit"`. Do not add a `[patch]` for `gpui`: Cargo cannot rename the `gpui-pre` package into the git `gpui` package through `[patch]`; the kit manifest change above is the supported route. The old `gpui-kit` stays in the workspace until Step 10.
|
||||
|
||||
Manifests that gain `gpui-native-kit = { workspace = true }`: `crates/assets` (icons only, may use `gpui-native-icons` directly), `crates/ui`, `crates/common`, `crates/chat_ui`, `crates/nip29_ui`, `crates/nip4e`, `crates/nip60_ui`, `crates/workspace`, `desktop`, `web`. `crates/speech` keeps `gpui-kit`. Remove `gpui-kit`, `gpui-base`, `native-theme`, and `native-theme-gpui` from every other manifest in Step 9.
|
||||
|
||||
## Step 1 — bootstrap and theme
|
||||
|
||||
Files: `desktop/src/main.rs`, `web/src/lib.rs`, `crates/settings/src/lib.rs`, `crates/workspace/src/lib.rs`.
|
||||
|
||||
1. Startup:
|
||||
- Replace `gpui_kit::init(cx)` with `gpui_native_kit::init(cx);` followed by `speech::init_compat(cx);` (the latter keeps the old theme global alive for the speech components). Define it in `crates/speech`:
|
||||
|
||||
```rust
|
||||
/// Initializes the old component kit that the retained speech widgets still use.
|
||||
pub fn init_compat(cx: &mut gpui::App) {
|
||||
gpui_kit::init(cx);
|
||||
}
|
||||
```
|
||||
|
||||
`desktop`/`web` add `speech = { path = "../crates/speech" }`; they do not depend on `gpui-kit` directly. Also call `common::dialogs::init(cx); common::toasts::init(cx);` once at startup (Step 5 defines them).
|
||||
- Replace `gpui_kit::open_window(opts, cx, workspace::init)` with `cx.open_window(opts, |window, cx| { gpui_native_kit::theme::Theme::sync_from_window(window, cx); gpui_native_kit::theme::Theme::sync_window_rem_size(window, cx); window.observe_window_appearance(|window, cx| { gpui_native_kit::theme::Theme::sync_from_window(window, cx); cx.refresh_windows(); }).detach(); workspace::init(window, cx) })`. `workspace::init` already returns `Entity<Workspace>`.
|
||||
- Set `WindowOptions.window_background = cx.theme().window_background` after `gpui_native_kit::init` (import `gpui_native_kit::theme::ActiveTheme`).
|
||||
- Delete `apply_native_theme`, `ThemeWatcher`, `start_theme_watcher`, `native_theme`/`native_theme_gpui` imports, and the two dependencies from `desktop/Cargo.toml`. Delete the same from `web/Cargo.toml` and `web/src/lib.rs`.
|
||||
- Delete `apply_web_fonts`; the kit paints `.SystemUIFont` and has no UI-font token.
|
||||
|
||||
2. Appearance (`desktop`, `web`, `settings`):
|
||||
- `AppearanceMode::System` → `gpui_native_kit::theme::follow_system(cx)`.
|
||||
- `Light`/`Dark` → `gpui_native_kit::theme::set_mode_override(Some(gpui_native_kit::theme::ThemeVariant::Light/Dark), cx)`.
|
||||
- Mirror the mode into the old theme for speech: `gpui_kit::component::Theme::change(gpui_kit::component::ThemeMode::Light/Dark, None, cx)`.
|
||||
- In `crates/workspace/src/lib.rs`, replace the `observe_window_appearance` body with `gpui_native_kit::theme::Theme::sync_from_window(window, cx)`; remove `AppSettings::apply_theme_style(cx)` and the old `Theme` import.
|
||||
|
||||
3. Remove theme tuning from `settings`:
|
||||
- Keep `AppearanceMode`; delete `ThemeSettings`, `theme` accessor, `apply_theme_style`, `apply_theme`, and `persisted_theme`. `settings` keeps its `gpui` dependency (it uses `App`/`Context`) but drops `gpui-kit`.
|
||||
- In `crates/workspace/src/dialogs/settings.rs`, delete `font_size_input`, `radius_input`, `radius_lg_input`, `wire_number_input`, the font-size/radius/focus-ring rows, `RADIUS_RANGE`, `FONT_SIZE_RANGE`, and the `ThemeSettings` reads. Keep appearance, wallet, speech, and other non-theme rows.
|
||||
- The kit owns radii, focus chrome, and metrics; there is no public override in `gpui-native_theme`. If preserving these settings is required later, add a token-override API to the kit, not app-side mutation.
|
||||
|
||||
Checkpoint: `cargo check -p coop -p coop_web -p settings`.
|
||||
|
||||
## Step 2 — icons and assets
|
||||
|
||||
1. `crates/assets/src/lib.rs`: replace every `gpui_kit::assets::Assets` with `gpui_native_kit::icons::IconAssets` (keep the wasm `OnceLock` caching). Add `gpui-native-kit = { workspace = true }` to `crates/assets/Cargo.toml`; remove `gpui-kit`.
|
||||
2. Merge the two icon enums into one `ui::IconName` in `crates/ui/src/icon.rs`.
|
||||
- Keep all current local variants and add every old-kit variant used by coop: `ArrowRight, Info, Plus, Link, X, CircleCheck, Compass, SquarePen, LogOut, LogIn, Wallet, Coins, UserRoundPlus, CircleX, Ban, Reply, Ellipsis, ChevronDown, MessageSquare, TriangleAlert, Check, PanelLeft, PanelLeftOpen, PanelRight, PanelRightOpen, PanelBottom, PanelBottomOpen, Close, WindowMinimize, WindowRestore, WindowMaximize, WindowClose, Book, Inbox`.
|
||||
- Implement `gpui_native_icons::IconNamed`; new variants use `icons/<kebab-case>.svg` (e.g. `CircleCheck -> "icons/circle-check.svg"`, `X`/`WindowClose -> "icons/x.svg"`, `TriangleAlert -> "icons/triangle-alert.svg"`). Existing local variants keep their current string paths (`Close -> "icons/close.svg"`, `CheckCircle -> "icons/check-circle.svg"`).
|
||||
- Delete the `gpui_kit::assets::IconNamed` import.
|
||||
3. Replace every `gpui_kit::assets::IconName::Variant` with `ui::IconName::Variant` in `chat_ui`, `nip29_ui`, `nip60_ui`, `nip4e`, `workspace`.
|
||||
4. Replace `Icon::new(x)` with `Icon::named(x)`; `.small()`/`.xsmall()` on icons become `.size(cx.theme().sizes.small.icon_size)` / `.size(cx.theme().sizes.small.dense_icon_size)` (or keep `Sizable` where the type implements it; `Icon` does not). `.text_color(color)` becomes `.color(color)`.
|
||||
5. `CombinedAssets` falls back to `IconAssets`, which serves the one missing kit glyph (`square.svg`); no new vendored files are required.
|
||||
|
||||
Checkpoint: `cargo check -p assets -p ui`.
|
||||
|
||||
## Step 3 — theme token sweep (all UI files)
|
||||
|
||||
Import `gpui_native_kit::common::{h_flex, v_flex, StyledExt, Sizable}` and `gpui_native_kit::theme::{ActiveTheme, ThemeVariant}`. The old `StyledExt` font helpers do not exist: `.font_semibold()` → `.font_weight(FontWeight::SEMIBOLD)`, `.font_medium()` → `.font_weight(FontWeight::MEDIUM)`, `.font_normal()` → `.font_weight(FontWeight::NORMAL)`.
|
||||
|
||||
Apply this exact field mapping to every `cx.theme()` access (48 files):
|
||||
|
||||
| Old (`gpui-kit` Theme) | New (`gpui_native_theme::Theme`) |
|
||||
|---|---|
|
||||
| `.background` | `.colors.background` |
|
||||
| `.foreground` | `.colors.foreground` |
|
||||
| `.border` | `.colors.border` |
|
||||
| `.primary` | `.ramps.primary.base` |
|
||||
| `.secondary`, `.secondary_foreground` | `.colors.secondary`, `.colors.secondary_foreground` |
|
||||
| `.secondary_hover`, `.secondary_active` | `.ramps.secondary.hover`, `.ramps.secondary.pressed` |
|
||||
| `.danger`, `.danger_foreground`, `.danger_active` | `.colors.destructive`, `.colors.destructive_foreground`, `.ramps.destructive.pressed` |
|
||||
| `.warning`, `.success` | `.colors.warning`, `.colors.success` |
|
||||
| `.muted`, `.muted_foreground` | `.colors.muted`, `.colors.muted_foreground` |
|
||||
| `.input`, `.input_background()` | `.colors.input` |
|
||||
| `.list_hover`, `.list_active` | `.colors.muted_hover`, `.colors.selection` |
|
||||
| `.sidebar`, `.sidebar_foreground`, `.sidebar_border` | `.colors.surface`, `.colors.foreground`, `.colors.border` |
|
||||
| `.tab_foreground`, `.tab_active`, `.tab_active_foreground` | `.colors.muted_foreground`, `.colors.surface`, `.colors.foreground` |
|
||||
| `.tokens.tab_bar`, `.tokens.background`, `.tokens.tab_active`, `.tokens.drop_target` | `.colors.background`, `.colors.background`, `.colors.surface`, `.colors.selection` |
|
||||
| `.group_box`, `.group_box_foreground` | `.colors.surface`, `.colors.surface_foreground` |
|
||||
| `.drag_border`, `.popover` | `.colors.accent`, `.colors.surface` |
|
||||
| `.radius`, `.radius_lg` | `.radius.control`, `.radius.overlay` |
|
||||
| `.font_family` | delete (use `.SystemUIFont` default) |
|
||||
| `.motion_tokens().duration_normal` / `.easing_move` | `.motion.overlay` / `gpui::ease_out_quint()` |
|
||||
| `.is_dark()` | `.variant == ThemeVariant::Dark` |
|
||||
| `.shadow` bool | delete the conditional; use `.elevations.popover.shadows()` where a shadow is needed |
|
||||
|
||||
`Div::rounded(cx.theme().radius)` etc. become `rounded(cx.theme().radius.control)`.
|
||||
|
||||
Checkpoint: `cargo check --workspace` (expect old-component type errors only).
|
||||
|
||||
## Step 4 — leaf controls
|
||||
|
||||
| Old | New |
|
||||
|---|---|
|
||||
| `Button::new(id)` | `Button::new(id)` |
|
||||
| `.label/.icon/.on_click/.disabled/.loading/.tab_stop` | same; `.icon` takes `Icon` (`Icon::named(app_icon)`) |
|
||||
| `.ghost()` | `.variant(ButtonVariant::Ghost)` |
|
||||
| `.primary()` / `.secondary()` | `.variant(ButtonVariant::Primary)` / `.variant(ButtonVariant::Secondary)` |
|
||||
| `.danger()` / `ButtonVariant::Danger` | `.variant(ButtonVariant::Destructive)` |
|
||||
| `.warning()` | `.variant(ButtonVariant::Destructive)` (no warning variant; noted gap) |
|
||||
| `.outline()` | `.variant(ButtonVariant::Secondary)` |
|
||||
| `.text()` | `.variant(ButtonVariant::Ghost)` (padding differs) |
|
||||
| `.xsmall()` | `.small()` |
|
||||
| `.size(impl Into<Size>)` | `.with_size(size)`; `.size(rems(2.))` becomes `.w(rems(2.)).h(rems(2.))` (styled) |
|
||||
| `.selected(bool)` | no equivalent; use `if selected { Ghost } else { Secondary }` or the owner's explicit style |
|
||||
| `.tooltip("x")` | `.tooltip(Tooltip::text("x"))` |
|
||||
| `.dropdown_menu(...)`, `.dropdown_caret(...)`, `.dropdown_menu_with_anchor(...)` | use `DropdownMenu` (action menus) or `Menu` with a `trigger` (arbitrary trigger element); see Step 5 |
|
||||
| `Switch::new(id).checked(v).on_click(\|&v,..\|)` | `.checked(v).on_change(\|v,..\|)` |
|
||||
| `Separator::horizontal()` | `Separator::horizontal()` (same) |
|
||||
| `Spinner::new().small()` | `Spinner::new(id).small()` (id required) |
|
||||
| `Progress::new(id).value(v)` | `Progress::new(id, v)` |
|
||||
| `ProgressCircle::new(id).value(v)` | `Spinner::new(id).value(v).small()` |
|
||||
| `Clipboard::new(id).value(v).tooltip(t)` | `ClipboardButton::new(id, v).tooltip(Tooltip::text(t))` |
|
||||
| `Tooltip::new(t).build(window,cx)` | `Tooltip::text(t)` (closure is the tooltip builder) |
|
||||
| `Avatar` (`crates/ui`) | `gpui_native_kit::avatar::Avatar::new(id).name(name).picture(url).fully_rounded(true)` |
|
||||
| local empty/notice views | `EmptyState::new(headline).hint(...).icon(...).action(...)` |
|
||||
| local chips | `Badge::new(label)` / `StatusDot::new(id).variant(...)` |
|
||||
|
||||
Avatar specifics: old `Avatar::new(picture).seed(seed)` becomes `Avatar::new(seed).picture(picture).fully_rounded(true)`. The kit `Avatar` is not a `DropdownMenu`; for menus see Step 5. Delete `crates/ui/src/avatar.rs`, and replace all `ui::Avatar` imports with `gpui_native_kit::avatar::Avatar`.
|
||||
|
||||
`Separator`, `Spinner`, `Progress`, `ClipboardButton`, `Tooltip`, `Switch`, `EmptyState`, `Badge`, `StatusDot` come from `gpui_native_kit` root (`gpui_native_core::*` re-export).
|
||||
|
||||
Checkpoint: `cargo check -p chat_ui -p nip29_ui -p nip60_ui -p workspace`.
|
||||
|
||||
## Step 5 — overlays, menus, dialogs, notifications
|
||||
|
||||
### Menus
|
||||
|
||||
The kit has no extension traits. `Menu`/`DropdownMenu` are controlled elements: pass `open` and update it from `on_change`. Add one `bool` per menu to its owning view.
|
||||
|
||||
- `Button::dropdown_menu(|menu, window, cx| ...)` → `DropdownMenu::new(id).variant(ButtonVariant::Ghost).icon(...).open(self.field).on_open_change(cx.listener(|this, open, _, cx| { this.field = open; cx.notify(); })).items(move |window, cx| vec![...])`.
|
||||
- `PopupMenu` builder → `Vec<MenuItem>`:
|
||||
- `.menu(label, Box::new(action))` → `MenuItem::new(label).handler(move |window, cx| window.dispatch_action(action.clone(), cx))`
|
||||
- `.menu_with_check(label, checked, action)` → `.checked(checked).handler(...)`
|
||||
- `.label(text)` → `MenuItem::new(text).disabled(true)`
|
||||
- `.separator()` → `MenuItem::separator()`
|
||||
- `.item(PopupMenuItem::new(label).on_click(...))` → `MenuItem::new(label).handler(move |window, cx| ...)`
|
||||
- `.item(PopupMenuItem::element(...))` (logout row) → `MenuItem::new("Log out").destructive(true).handler(...)`
|
||||
- `.submenu("React", ...)` (one site, `nip29_ui/message.rs`) → flatten the emoji into sibling `MenuItem`s (label the first `"React"` as a disabled header if desired); the kit has no submenu.
|
||||
- `.context_menu(...)` on a row → wrap the row in `ContextMenu::new(id).target(move |window, cx| row_builder(window, cx)).items(move |_, _| vec![...])`. `ContextMenu` owns its open state; no owner field.
|
||||
- Avatar menu (`sidebar/header.rs`, `chat_ui` avatar): wrap the Avatar in `Menu::new(("avatar-menu", id)).open(self.avatar_menu == Some(id)).on_change(...).trigger(move |window, cx| avatar_element).items(...)`. Add `avatar_menu: Option<EntityId>` to `Sidebar`/`ChatPanel`/`GroupPanel`. For `chat_ui` message avatars, use a single `avatar_menu: Option<EventId>` on `ChatPanel`.
|
||||
- `Popover` (`sidebar/header.rs`): new `Popover::new(id).open(self.encryption_open).on_change(...).trigger(...).content(...)`. The old `Anchor::TopRight` and `popover.update(...).dismiss(...)` are gone; the kit anchors below the trigger and closes on outside click/Escape. Add `encryption_open: bool` to `Sidebar`.
|
||||
|
||||
### Dialogs
|
||||
|
||||
The kit `Dialog` is an element, and its overlay is absolute within its parent; it must be mounted at the workspace root or it is clipped by panels. Add a host in `crates/common`:
|
||||
|
||||
```rust
|
||||
// crates/common/src/dialogs.rs
|
||||
pub struct Dialogs { views: Vec<AnyView> }
|
||||
impl Dialogs {
|
||||
pub fn open(&mut self, view: AnyView, cx: &mut Context<Self>) -> EntityId;
|
||||
pub fn close(&mut self, id: EntityId, cx: &mut Context<Self>);
|
||||
pub fn clear(&mut self, cx: &mut Context<Self>);
|
||||
}
|
||||
impl Render for Dialogs {
|
||||
// An absolute, window-covering layer. Every dialog view inside renders its own
|
||||
// `Dialog` element; the host only stacks them.
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().absolute().inset_0().children(self.views.iter().cloned())
|
||||
}
|
||||
}
|
||||
pub fn init(cx: &mut App); // installs the global Entity<Dialogs>
|
||||
pub fn open(view: AnyView, cx: &mut App); // pushes
|
||||
pub fn close(view: EntityId, cx: &mut App); // removes one
|
||||
pub fn clear(cx: &mut App); // close_all_dialogs
|
||||
```
|
||||
|
||||
`crates/common/src/toasts.rs` wraps `gpui_native_kit::toast::{Toasts, Toast}`:
|
||||
|
||||
```rust
|
||||
pub fn init(cx: &mut App);
|
||||
pub fn notify(toast: Toast, cx: &mut App);
|
||||
pub fn clear(cx: &mut App); // remove_notification/clear
|
||||
```
|
||||
|
||||
`Workspace::render` mounts both at the root, outside the dock and sidebar:
|
||||
|
||||
```rust
|
||||
deferred(div().absolute().inset_0()
|
||||
.child(common::dialogs::global(cx).clone())
|
||||
.child(ToastsView::new("toasts", common::toasts::global(cx))))
|
||||
```
|
||||
|
||||
(Observe the two entities in `Workspace::new` with `cx.observe(&entity, |_,_,cx| cx.notify())`.)
|
||||
|
||||
Each dialog view becomes an overlay view:
|
||||
- In its `Render`, wrap the existing body in `Dialog::new(("dialog", self.id)).open(true).title(self.title()).on_change(move |open, window, cx| if !open { common::dialogs::close(self_id, cx) }).content(move |window,cx| body).actions(...)`. `Dialog` centers itself over the host. Capture `self_id = cx.entity().entity_id()` before the closures.
|
||||
- `window.open_dialog(cx, builder)` → create the entity (`cx.new(...)`), then `common::dialogs::open(view.clone().into(), cx)`. The view stores no host state.
|
||||
- `window.close_dialog(cx)` → `common::dialogs::close(cx.entity().entity_id(), cx)` from inside the view; for child views that close a parent dialog, pass the parent dialog `EntityId` in the constructor or capture the parent's `WeakEntity` and call `close(parent.entity_id(), cx)`.
|
||||
- `window.close_all_dialogs(cx)` → `common::dialogs::clear(cx)`.
|
||||
- `AlertDialog` (5 sites) → `Dialog::new(id).alert(true).title(...).content(...).actions(move |window,cx| vec![cancel_button, confirm_button])`; explicit `Button`s replace `DialogButtonProps`. Default cancel/ok labels are written at each site (`Cancel`, `OK`, `Ignore`, `Response`, etc.). `Dialog::on_change` closes the host entry; `alert(true)` blocks Escape/backdrop close.
|
||||
- The kit `Dialog` has no width builder and no title-bar close button. Preserve the old widths (380/420/460/650) by wrapping the content in `div().w(px(...))`; Escape and scrim click close, and alert dialogs require their action buttons.
|
||||
- `DialogHeader`/`DialogTitle`/`DialogDescription`/`DialogFooter`/`DialogAction` (import dialog) → plain `v_flex()` with `.text_size(...)`/`.font_weight(SEMIBOLD)` and the kit `Separator`; no component.
|
||||
|
||||
Notification → toast mapping:
|
||||
- `window.push_notification(Notification::error(msg), cx)` → `common::toasts::notify(Toast::new(msg).icon(IconRole::Warning), cx)`.
|
||||
- `.warning(msg)` → `.icon(IconRole::Warning)`; `.success(msg)` → `.icon(IconRole::Check)`; `NotificationType::Info` → `.icon(IconRole::Info)`.
|
||||
- `.autohide(false)` → `.duration(std::time::Duration::ZERO)`.
|
||||
- `.action(|...| Button...)` → `.action(ToastAction::new("Retry", move |cx| { ... }))`.
|
||||
- `.title(t).message(m)` → `Toast::new(format!("{t} · {m}"))`; the kit toast has no title/body/content slot.
|
||||
- `.id::<T>()` and `window.remove_notification::<T>()` → `common::toasts::clear(cx)`; the kit store holds one toast at a time.
|
||||
- `nip4e`'s rich content closure (`crates/nip4e/src/lib.rs:1006-1087`) → a one-line toast containing the code; avatar/rich body are a noted gap.
|
||||
|
||||
Checkpoint: `cargo check -p common -p workspace -p chat_ui -p nip29_ui -p nip60_ui -p nip4e`.
|
||||
|
||||
## Step 6 — inputs and forms
|
||||
|
||||
| Old | New |
|
||||
|---|---|
|
||||
| `InputState::new(window, cx)` | `InputState::new(cx)` (or `new_with_text(text, cx)` for `.default_value`) |
|
||||
| `InputState::new(...).placeholder(p)` | `Input::new(id, state).placeholder(p)` |
|
||||
| `.masked(true)` | `state.update(cx, \|s,cx\| s.set_password(true, cx))`; add an `IconRole::Eye`/`EyeOff` ghost `Button` suffix toggling `set_revealed` for the three password fields |
|
||||
| `.clean_on_escape()` | delete; the kit blurs on Escape |
|
||||
| `.clearable(true)` / `.cleanable(true)` | no equivalent; for the two search inputs add a `Button` suffix with `IconRole::Close` that calls `set_value("", window, cx)`; drop the flag elsewhere |
|
||||
| `.appearance(false)` | delete; the kit field always paints its chrome |
|
||||
| `TextareaState::new` + `.auto_grow(min,max)` | `InputState::new(cx)` + `set_multiline(true, cx)`; render `Input::new(id, state).rows(1)`; no auto-grow (noted gap) |
|
||||
| `Textarea::new(&state)` | `Input::new(id, state.clone())` (multiline state) |
|
||||
| `NumberInput::new(&state)` | `Input::new(id, state)`; parse/clamp in the `TextChanged` handler; no stepper buttons |
|
||||
| `InputEvent::PressEnter { .. }` | `InputStateEvent::Submitted` |
|
||||
| `InputEvent::Change` | `InputStateEvent::TextChanged` |
|
||||
| `state.read(cx).insert(text, window, cx)` | `let mut value = state.read(cx).value().to_owned(); value.push_str(text); state.update(cx, \|s,cx\| s.set_value(value, window, cx));` |
|
||||
| `state.update(cx, \|s,cx\| s.focus(window,cx))` | `window.focus(&state.read(cx).focus_handle(), cx)` |
|
||||
| `.set_loading(bool, ...)` | delete (no loading state); show a `Spinner` prefix/suffix where the old spinner mattered |
|
||||
| `Input::new(&state)` | `Input::new(id, state.clone())` (id required) |
|
||||
|
||||
Forms:
|
||||
- `settings_row(label, desc, control, cx)` → `Field::new(id, label).description(desc).variant(FieldVariant::Row).child(control)`.
|
||||
- `settings_block(...)` → `Field::new(id, label).description(desc).child(control)` (default `Block`).
|
||||
- Call sites that need the old fixed control column: wrap the control in `h_flex().w_40().justify_end().child(control)`; otherwise let the kit field lay it out.
|
||||
- Delete `crates/ui/src/form.rs`.
|
||||
- `GroupBox` (`new_group.rs` two sites) → inline `v_flex().p_3().gap_2().rounded(cx.theme().radius.overlay).border_1().border_color(cx.theme().colors.border).bg(cx.theme().colors.surface)` with a title row; noted gap (no kit GroupBox).
|
||||
- `Kbd` (`settings.rs` dictation shortcut) → inline shortcut label: `if cfg!(target_os="macos") { "⌘D" } else { "Ctrl+D" }` in a muted `div().px_1().rounded(cx.theme().radius.sm).bg(cx.theme().colors.muted)`; noted gap.
|
||||
- `gpui_base::Select`/`Popup` in `new_group.rs`: replace with kit `Select<T>` (`Select::new(id).options(...).selected(value).on_change(...)`) and `Popover` for the content.
|
||||
- `form::{field, v_form}` in `dialogs/import.rs` → kit `Field`.
|
||||
|
||||
Checkpoint: `cargo check -p workspace -p chat_ui -p nip29_ui -p nip60_ui`.
|
||||
|
||||
## Step 7 — data views and text
|
||||
|
||||
1. `ScrollableElement::overflow_y_scrollbar()` (4 sites) → wrap the content in `ScrollArea::new(id).axes(ScrollAxes::Vertical)`.
|
||||
2. `Scrollbar::vertical(&list_state)` → `Scrollbar::vertical(("chat-scroll", id), list_state.clone())`; the kit trait is implemented for `ListState`/`UniformListScrollHandle`.
|
||||
3. `gpui_base::{VirtualListScrollHandle, v_virtual_list}` (`panels/browse.rs`) → gpui `ListState::new(rows, ListAlignment::Top, px(100.))` + `list(state, move |this, ix, window, cx| ...)`; remove the per-item size vector and `track_scroll`. `scroll_to_item(0, Top)` → `ListState::scroll_to(ListOffset { item_ix: 0, offset_in_item: px(0.) })`. Noted gap: the kit has no generic virtual list (its checklist lists it as future work).
|
||||
4. `TabBar` in `dialogs/relays.rs` → `Tabs::new(id).variant(TabsVariant::Pill).selected(self.selected).on_change(...).tabs(move |_,_| vec![Tab::new(key, label)...])`.
|
||||
5. `Presence`/`Transition` progress pill (`sidebar/mod.rs`) → render the pill unconditionally; drop the enter/exit animation. Noted gap: no transition helper.
|
||||
6. `ui::text`:
|
||||
- Keep `InlineReplacement` and `rendered_text`.
|
||||
- Store `Entity<MarkdownViewState>` per message: replace `BTreeMap<EventId, RenderedText>` with `BTreeMap<EventId, Entity<MarkdownViewState>>`. On first render of a message, create `cx.new(|_| MarkdownViewState::new(("message", message.id), source))`; render the entity. `MarkdownViewState` is immutable, so creation happens once per message.
|
||||
- Preprocess `nostr:nevent1...` destinations into `https://njump.me/<bech32>` inside `rendered_text`/`InlineReplacement`; the kit's markdown view calls `cx.open_url` itself and exposes no link callback. Delete `is_web_url`/`nevent_bech32` if unused.
|
||||
- NIP-30 custom emoji images become their alt text (kit markdown renders images as alt text). Noted gap.
|
||||
- `RevealText` stays as-is (plain text, no kit dependency).
|
||||
7. `WelcomeMessage` → `EmptyState::new(title).hint(message).icon(icon)` where the caller can supply a headline; otherwise keep the product view. Delete only if the mapping is lossless.
|
||||
|
||||
Checkpoint: `cargo check -p chat_ui -p nip29_ui -p workspace`.
|
||||
|
||||
## Step 8 — dock, shell, panels
|
||||
|
||||
Delete `crates/workspace/src/dock/{mod.rs, skin.rs, tab_panel.rs, invalid_panel.rs, window_controls.rs}` and replace with `crates/workspace/src/dock.rs`.
|
||||
|
||||
1. `Workspace` owns `dock: Entity<gpui_native_kit::layout::dock::DockState>` and `sidebar: Entity<Sidebar>`.
|
||||
2. Root layout (manual, because `Shell::left_sidebar` only accepts the data `Sidebar`, not an arbitrary view):
|
||||
|
||||
```rust
|
||||
div().flex().flex_row().size_full()
|
||||
.when(self.sidebar_visible, |row| row
|
||||
.child(div().w(self.sidebar_width).h_full().child(self.sidebar.clone()))
|
||||
.child(ResizeHandle::new("sidebar-resize").on_resize(...)))
|
||||
.child(div().flex_1().min_w_0().h_full().child(
|
||||
Dock::new("coop-dock", self.dock.clone())
|
||||
.window_controls(!cfg!(target_os = "macos"))
|
||||
.traffic_light_pad(cfg!(target_os = "macos"))))
|
||||
```
|
||||
|
||||
Add `sidebar_visible: bool` (initial `true`), `sidebar_width: Pixels` (default 240; clamp 160..480) to `Workspace`. Add `workspace::ToggleSidebar` action and a `Button` in the sidebar header with `IconRole::PanelLeft`; handle it in `Workspace`.
|
||||
|
||||
Initial content: create the dock state, then in `cx.defer_in` open `greeter::init(dock.downgrade(), window, cx)` into the dock exactly as the old center region did. Do not register every panel at startup; panels register on first open through `open_panel`.
|
||||
3. Register/open panels with a helper:
|
||||
|
||||
```rust
|
||||
pub fn open_panel<P: Render + 'static>(
|
||||
dock: &WeakEntity<DockState>,
|
||||
title: impl Into<SharedString>,
|
||||
view: Entity<P>,
|
||||
split: Option<SplitDirection>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let id = SharedString::from(view.entity_id().to_string());
|
||||
dock.update(cx, |dock, cx| {
|
||||
let view = view.clone();
|
||||
dock.register_panel(PanelDescriptor::new(id.clone(), title, move |_, _| view.clone().into()));
|
||||
dock.open_panel(&id, window, cx);
|
||||
if let Some(direction) = split { dock.split_panel(&id, direction, cx); }
|
||||
}).ok();
|
||||
}
|
||||
```
|
||||
|
||||
Call sites: `sidebar/rows.rs::open_room`, `sidebar/inbox.rs`, `dialogs/new_chat.rs`, `dialogs/new_group.rs`, `panels/browse.rs`, `workspace/src/lib.rs`. Replace `WeakEntity<DockArea>` with `WeakEntity<DockState>` in those signatures.
|
||||
4. Remove `Panel`/`BasePanel`/`PanelEvent` impls from `ChatPanel`, `GroupPanel`, `WalletPanel`, `BackupPanel`, `BrowsePanel`, `ContactListPanel`, `GreeterPanel`, `InboxPanel`, `Sidebar`. Remove `EventEmitter<PanelEvent>`, `panel_name`, `title`, `toolbar_buttons`, `set_active`, `on_removed`, `closable`, `zoomable`. Panel titles/icons move to `PanelDescriptor` at registration (static; dynamic rename is a noted gap). Delete the now-unused `nip29_ui::{ShowGroupMeta, HideGroupMeta}` actions and their handlers in `crates/workspace/src/lib.rs`.
|
||||
5. Replace `TAB_BAR_HEIGHT` with `cx.theme().titlebar_height`; use it for the sidebar header height and the window traffic-light position (`point(px(9.), px(theme.titlebar_height / 2. - 14. / 2.))`). Use `gpui_native_kit::layout::window_controls::{titlebar_strip, TRAFFIC_LIGHT_PAD}` in the sidebar header; delete `ui::title_bar_drag_handlers` and `crates/ui/src/window_drag.rs`.
|
||||
6. Remove `ClosePanel`/`ToggleZoom` dispatch and handlers. The new dock's own close/zoom controls own those actions. In `screening.rs`, the `Ignore` button now only closes the dialog (no panel close).
|
||||
7. `Sidebar` is no longer a dock panel; its `BasePanel::set_active` group-view dispatch is removed. Row clicks already call `Sidebar::show_group`; keep that path. Behavior change: activating an already-open group panel no longer opens the sidebar's group view, and closing a group panel no longer hides it; the back button still hides it.
|
||||
|
||||
Checkpoint: `cargo check -p workspace -p chat_ui -p nip29_ui -p nip60_ui`.
|
||||
|
||||
## Step 9 — remove the old design system
|
||||
|
||||
1. `crates/ui`: delete `avatar.rs`, `form.rs`, `window_drag.rs`. Keep `icon.rs`, `message.rs`, `text.rs`. Update `lib.rs`. Remove `gpui-base`, `gpui-kit`, and dead deps (`uuid`, `web-time`, `log`); add `gpui-native-kit`.
|
||||
2. `crates/speech` is the only crate that keeps `gpui-kit`. Make it re-export the speech UI types so `chat_ui`/`nip29_ui` do not import `gpui_kit`:
|
||||
|
||||
```rust
|
||||
pub use gpui_kit::component::speech::{SpeechButton, SpeechEvent, SpeechState, SpeechWaveform};
|
||||
```
|
||||
|
||||
Change `chat_ui`/`nip29_ui` imports to `speech::{...}`. `speech::init_compat` calls `gpui_kit::init(cx)`; keep the old-theme mode mirror from Step 1. Remove `gpui-kit` from `chat_ui`, `nip29_ui`, `nip60_ui`, `nip4e`, `settings`, `assets`, `workspace`, `ui`, `desktop`, `web`.
|
||||
3. Workspace `Cargo.toml`: remove `gpui-base`, `native-theme`, `native-theme-gpui`; keep `gpui-kit` (speech only). Remove `gpui-base` from all crate manifests.
|
||||
4. Delete `crates/workspace/src/dock/{skin,tab_panel,invalid_panel,window_controls}.rs`; delete `CoopDockSkin`, `CoopTabGroupSkin`, `DragPanelPreview`, `DockResizeTracker`, `ResizePanel`, `ControlIcon`.
|
||||
5. No crate other than `crates/speech` imports `gpui_kit`. `crates/speech` keeps the old speech types, `ActiveTheme`, `Subscription`, and `init_compat`.
|
||||
|
||||
Checkpoint: `cargo check --workspace`; `rg "gpui_kit::|gpui_base::" crates desktop web` must report only `crates/speech` (`init_compat` and the speech re-exports).
|
||||
|
||||
## Kit gaps (explicitly noted, no new components invented)
|
||||
|
||||
| Missing in `gpui-native-kit` | Handling in this plan |
|
||||
|---|---|
|
||||
| Window-level notifications with severity, title, rich content, ids, system delivery | Kit `Toast` only: one toast at a time, one line, optional icon/action/duration. Hosted in `common`; rich `nip4e` body reduced to text. |
|
||||
| Speech (`SpeechState`, `SpeechButton`, `SpeechWaveform`, `SpeechRecognizer`, `AudioInput`) | Kept on old `gpui-kit` in `crates/speech`; the only old-kit dependency left. |
|
||||
| Markdown inline images (NIP-30 emoji) and a link-click callback | Kit `MarkdownViewState` renders images as alt text and opens links through `cx.open_url`; `rendered_text` preprocesses `nostr:nevent` to a njump URL. |
|
||||
| Menu submenus, arbitrary-element rows, non-action label rows, anchor override | Flatten the single React submenu; use `MenuItem::new(label).disabled(true)` for labels; `MenuItem::disabled(true)`/`destructive(true)` for custom rows; anchor below trigger. |
|
||||
| Button text variant, child content, attached dropdown, selected state, warning variant, xsmall | Map to Ghost/Secondary/Destructive variants and `DropdownMenu`/`Menu`; avatar-as-trigger uses `Menu::trigger`. |
|
||||
| Input clear button, mask toggle, loading state, auto-grow, number stepper, borderless appearance, escape-clear, `insert`/`focus` | Compose suffix buttons where needed; otherwise drop the affordance and note it. Fixed rows replace auto-grow. |
|
||||
| `GroupBox`, `Kbd`, `NumberInput`, `ProgressCircle`, `Presence`/`Transition`, `Lerp`/easing helpers | Inline box/label compositions; `ProgressCircle` → determinate `Spinner`; transitions dropped. |
|
||||
| Generic virtual list | Use gpui `list`/`uniform_list` with `ListState`; no kit component. |
|
||||
| Window-level dialog/notification managers; dialog width/close button | `common::dialogs` host + explicit action buttons; width on content; no title-bar X. |
|
||||
| Old theme tokens (`danger`, `list_*`, `sidebar*`, `tab_*`, `tokens.*`, `group_box`, `font_family`) and token overrides | Mapped in Step 3; font-size/radius/focus-ring settings removed (kit owns tokens). |
|
||||
| Dock placements (left/right/bottom), panel lifecycle/toolbar/actions, dynamic tab titles | One pane tree; left panels open as tabs or split left; lifecycle hooks removed; titles static. |
|
||||
| Generic rows with arbitrary prefix/suffix | `Nav` stays as product-specific code, rebased on kit tokens; kit `List`/`Sidebar` cannot express it. |
|
||||
| Facade `editor` re-export | Added in Step 0; otherwise depend on `gpui-native-editor` directly. |
|
||||
| `IconRole` coverage of app glyphs | App `ui::IconName` implements `IconNamed`; kit `IconAssets` supplies missing role glyphs. |
|
||||
|
||||
## Critical files
|
||||
|
||||
- `desktop/src/main.rs` and `web/src/lib.rs` — startup, theme, window.
|
||||
- `crates/workspace/src/lib.rs` — dock root, sidebar layout, dialogs/toasts host, actions.
|
||||
- `crates/workspace/src/dock.rs` (new) — `open_panel` helper; replaces the old dock module.
|
||||
- `crates/ui/src/message.rs` — largest product-view rebase (buttons, menus, theme tokens).
|
||||
- `crates/common/src/dialogs.rs`, `crates/common/src/toasts.rs` (new) — kit lacks window-level managers.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `cargo check --workspace` in `/Users/reya/Documents/Git/coop`.
|
||||
2. `cargo build -p coop` (desktop).
|
||||
3. Run `cargo run -p coop -- --sec <throwaway nsec>` and check:
|
||||
- Window opens with the kit titlebar; no old dock tab bar or double titlebar.
|
||||
- Sidebar renders avatars, icons, hover/selected rows; sidebar resize and toggle work.
|
||||
- User avatar menu opens and shows Profile/Relays/Log out; encryption popover opens and closes.
|
||||
- `Cmd/Ctrl+,` Preferences opens; appearance Light/Dark/System changes the whole UI immediately; the speech shortcut row shows the platform shortcut.
|
||||
- Open a chat from the sidebar; send `hello`; the message renders; right-click the message opens the context menu; the emoji menu opens; a toast appears for an empty send.
|
||||
- A `nevent1...` mention opens `njump.me` on click.
|
||||
- Drag a dock tab to an edge; the pane splits; close/close-others work.
|
||||
- Open Browse; scrolling uses the list; open New Group and New Chat dialogs; cancel closes them.
|
||||
4. `cargo check -p coop_web --target wasm32-unknown-unknown` (requires the wasm target and `gpui_web`); verify the web bundle opens a window and settings appearance works.
|
||||
5. `rg -n "gpui_kit::|gpui_base::|native_theme" crates desktop web` — only `crates/speech`, the compat `gpui_kit::init` lines, and the speech imports should remain.
|
||||
|
||||
## Assumptions and contingencies
|
||||
|
||||
- The kit's `gpui` dependency moves to `gpui-pre` 0.3.10 as a committed prerequisite. If the kit does not compile against 0.3.10, bump coop's `gpui`, `gpui_platform`, `gpui_web`, and `gpui-kit` to the newest 0.3.x that compiles both, together; never keep two `gpui` crates.
|
||||
- The kit repo is modifiable. If it must stay untouched, vendor it under `vendor/gpui-native-kit` and apply the same two manifest edits there.
|
||||
- Theme tuning (font size, focus ring, radii) is removed because the new theme has no public override. If it must be preserved, add a token-override API to the kit; do not mutate the global.
|
||||
- Speech is retained through the old kit. If old theme/keybinding initialization conflicts with the new kit, replace `gpui_kit::init(cx)` with the minimal old-component theme init (`gpui_kit::component::Theme::change` after new theme init) and verify `SpeechButton`/`SpeechWaveform` render.
|
||||
- Dock placements are not preserved 1:1: the new dock has one pane tree. Left-placed panels open as tabs, optionally split left via `split_panel`; bottom/right placements are not used by coop. Panel tab titles are static after registration.
|
||||
+2
-1
@@ -22,7 +22,8 @@ person = { path = "../crates/person" }
|
||||
gpui.workspace = true
|
||||
gpui_platform.workspace = true
|
||||
gpui_web.workspace = true
|
||||
gpui-kit.workspace = true
|
||||
gpui-native-kit.workspace = true
|
||||
speech = { path = "../crates/speech" }
|
||||
|
||||
log.workspace = true
|
||||
instant = { workspace = true, features = ["wasm-bindgen"] }
|
||||
|
||||
+18
-34
@@ -1,10 +1,10 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::*;
|
||||
use gpui_native_kit::theme::ActiveTheme;
|
||||
use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -83,16 +83,15 @@ pub fn run() -> Result<(), JsValue> {
|
||||
KeyBinding::new("ctrl-d", chat_ui::ToggleDictation, None),
|
||||
]);
|
||||
|
||||
gpui_kit::init(cx);
|
||||
gpui_native_kit::init(cx);
|
||||
speech::init_compat(cx);
|
||||
|
||||
load_embedded_fonts(cx);
|
||||
apply_web_fonts(cx);
|
||||
|
||||
// Initialize settings
|
||||
settings::init(cx);
|
||||
|
||||
apply_appearance(settings::persisted_appearance_mode(), cx);
|
||||
settings::AppSettings::apply_theme_style(cx);
|
||||
workspace::apply_appearance_mode(settings::persisted_appearance_mode(), cx);
|
||||
|
||||
// Initialize the nostr client
|
||||
state::init(cx, None);
|
||||
@@ -111,7 +110,20 @@ pub fn run() -> Result<(), JsValue> {
|
||||
// Initialize groups registry
|
||||
nip29::init(cx);
|
||||
|
||||
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
|
||||
let opts = WindowOptions {
|
||||
window_background: cx.theme().window_background,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
cx.open_window(opts, |window, cx| {
|
||||
gpui_native_kit::theme::Theme::sync_from_window(window, cx);
|
||||
gpui_native_kit::theme::Theme::sync_window_rem_size(window, cx);
|
||||
window
|
||||
.observe_window_appearance(|window, cx| {
|
||||
gpui_native_kit::theme::Theme::sync_from_window(window, cx);
|
||||
cx.refresh_windows();
|
||||
})
|
||||
.detach();
|
||||
workspace::init(window, cx)
|
||||
})
|
||||
.expect("Failed to open window. Please restart the application.");
|
||||
@@ -139,31 +151,3 @@ fn load_embedded_fonts(cx: &mut App) {
|
||||
.add_fonts(fonts)
|
||||
.expect("Failed to load bundled fonts");
|
||||
}
|
||||
|
||||
fn apply_web_fonts(cx: &mut App) {
|
||||
use gpui_kit::component::Theme;
|
||||
|
||||
const UI_FONT: &str = "Inter";
|
||||
|
||||
Theme::update(cx, |theme| {
|
||||
theme.font_family = UI_FONT.into();
|
||||
theme.mono_font_family = UI_FONT.into();
|
||||
|
||||
for config in [&mut theme.light_theme, &mut theme.dark_theme] {
|
||||
let config = Rc::make_mut(config);
|
||||
config.font_family = Some(UI_FONT.into());
|
||||
config.mono_font_family = Some(UI_FONT.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Honor the appearance mode against the default palettes
|
||||
fn apply_appearance(appearance: settings::AppearanceMode, cx: &mut App) {
|
||||
use gpui_kit::component::{Theme, ThemeMode};
|
||||
|
||||
match appearance {
|
||||
settings::AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||
settings::AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
||||
settings::AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user