chore: revamp theme

This commit is contained in:
2025-05-07 14:12:31 +07:00
parent 97e66fbeb7
commit 2f83b5091e
57 changed files with 922 additions and 1494 deletions

117
Cargo.lock generated
View File

@@ -499,9 +499,9 @@ dependencies = [
[[package]]
name = "backtrace"
version = "0.3.74"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
@@ -1136,7 +1136,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"indexmap",
"rustc-hash 2.1.1",
@@ -1240,6 +1240,7 @@ dependencies = [
"serde_json",
"smallvec",
"smol",
"theme",
"tracing-subscriber",
"ui",
"webbrowser",
@@ -1526,7 +1527,7 @@ dependencies = [
[[package]]
name = "derive_refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"proc-macro2",
"quote",
@@ -2317,7 +2318,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"anyhow",
"as-raw-xcb-connection",
@@ -2409,7 +2410,7 @@ dependencies = [
[[package]]
name = "gpui_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"proc-macro2",
"quote",
@@ -2425,9 +2426,9 @@ checksum = "d196ffc1627db18a531359249b2bf8416178d84b729f3cebeb278f285fb9b58c"
[[package]]
name = "h2"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
dependencies = [
"atomic-waker",
"bytes",
@@ -2633,7 +2634,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"anyhow",
"bytes",
@@ -2650,7 +2651,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"rustls",
"rustls-platform-verifier",
@@ -2699,7 +2700,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
"webpki-roots 0.26.11",
]
[[package]]
@@ -3214,9 +3215,9 @@ dependencies = [
[[package]]
name = "libm"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25169bd5913a4b437588a7e3d127cd6e90127b60e0ffbd834a38f1599e016b8"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
@@ -3391,7 +3392,7 @@ dependencies = [
[[package]]
name = "media"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"anyhow",
"bindgen 0.71.1",
@@ -3589,7 +3590,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr"
version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr?branch=nip46#63609d9c7e62b16c26fa59b255284f3a292bea24"
source = "git+https://github.com/rust-nostr/nostr#677cb7fb242fd59f861561203cd7b9502e222642"
dependencies = [
"aes",
"base64",
@@ -3614,7 +3615,7 @@ dependencies = [
[[package]]
name = "nostr-connect"
version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr?branch=nip46#63609d9c7e62b16c26fa59b255284f3a292bea24"
source = "git+https://github.com/rust-nostr/nostr#677cb7fb242fd59f861561203cd7b9502e222642"
dependencies = [
"async-utility",
"nostr",
@@ -3626,7 +3627,7 @@ dependencies = [
[[package]]
name = "nostr-database"
version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr?branch=nip46#63609d9c7e62b16c26fa59b255284f3a292bea24"
source = "git+https://github.com/rust-nostr/nostr#677cb7fb242fd59f861561203cd7b9502e222642"
dependencies = [
"flatbuffers",
"lru",
@@ -3637,7 +3638,7 @@ dependencies = [
[[package]]
name = "nostr-lmdb"
version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr?branch=nip46#63609d9c7e62b16c26fa59b255284f3a292bea24"
source = "git+https://github.com/rust-nostr/nostr#677cb7fb242fd59f861561203cd7b9502e222642"
dependencies = [
"async-utility",
"heed",
@@ -3650,7 +3651,7 @@ dependencies = [
[[package]]
name = "nostr-relay-pool"
version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr?branch=nip46#63609d9c7e62b16c26fa59b255284f3a292bea24"
source = "git+https://github.com/rust-nostr/nostr#677cb7fb242fd59f861561203cd7b9502e222642"
dependencies = [
"async-utility",
"async-wsocket",
@@ -3666,7 +3667,7 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr?branch=nip46#63609d9c7e62b16c26fa59b255284f3a292bea24"
source = "git+https://github.com/rust-nostr/nostr#677cb7fb242fd59f861561203cd7b9502e222642"
dependencies = [
"async-utility",
"nostr",
@@ -4842,7 +4843,7 @@ dependencies = [
[[package]]
name = "refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"derive_refineable",
"workspace-hack",
@@ -4926,7 +4927,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
"webpki-roots 0.26.11",
"windows-registry 0.4.0",
]
@@ -4981,7 +4982,7 @@ dependencies = [
[[package]]
name = "reqwest_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"anyhow",
"bytes",
@@ -5042,9 +5043,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rust-embed"
version = "8.7.0"
version = "8.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d"
checksum = "60e425e204264b144d4c929d126d0de524b40a961686414bab5040f7465c71be"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@@ -5130,9 +5131,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.26"
version = "0.23.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
dependencies = [
"aws-lc-rs",
"log",
@@ -5176,9 +5177,9 @@ dependencies = [
[[package]]
name = "rustls-platform-verifier"
version = "0.5.2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4937d110d34408e9e5ad30ba0b0ca3b6a8a390f8db3636db60144ac4fa792750"
checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
dependencies = [
"core-foundation 0.10.0",
"core-foundation-sys",
@@ -5191,7 +5192,7 @@ dependencies = [
"rustls-webpki",
"security-framework 3.2.0",
"security-framework-sys",
"webpki-root-certs",
"webpki-root-certs 0.26.11",
"windows-sys 0.59.0",
]
@@ -5203,9 +5204,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.1"
version = "0.103.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437"
dependencies = [
"aws-lc-rs",
"ring",
@@ -5451,7 +5452,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]]
name = "semantic_version"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"anyhow",
"serde",
@@ -5774,7 +5775,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"arrayvec",
"log",
@@ -6065,6 +6066,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "theme"
version = "0.1.5"
dependencies = [
"anyhow",
"gpui",
"log",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -6199,9 +6209,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.44.2"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",
@@ -6269,7 +6279,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots",
"webpki-roots 0.26.11",
]
[[package]]
@@ -6499,6 +6509,7 @@ dependencies = [
"serde_json",
"smallvec",
"smol",
"theme",
"unicode-segmentation",
"uuid",
]
@@ -6684,7 +6695,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#45fe158bc9179d1357617f9fa2cc6c3c203daa6b"
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
dependencies = [
"anyhow",
"async-fs",
@@ -7064,18 +7075,36 @@ dependencies = [
[[package]]
name = "webpki-root-certs"
version = "0.26.10"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c99403924bc5f23afefc319b8ac67ed0e50669f6e52a413314cccb1fdbc93ba0"
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
dependencies = [
"webpki-root-certs 1.0.0",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "0.26.10"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.0",
]
[[package]]
name = "webpki-roots"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
dependencies = [
"rustls-pki-types",
]
@@ -7620,9 +7649,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.9"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]

View File

@@ -16,10 +16,10 @@ gpui = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr
nostr = { git = "https://github.com/rust-nostr/nostr", branch = "nip46", features = ["parser"] }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", branch = "nip46", features = ["lmdb", "nip96", "nip59", "nip49", "nip44", "nip05"] }
nostr-connect = { git = "https://github.com/rust-nostr/nostr", branch = "nip46" }
nostr-keyring = { git = "https://github.com/rust-nostr/nostr", branch = "nip46" }
nostr = { git = "https://github.com/rust-nostr/nostr", features = ["parser"] }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb", "nip96", "nip59", "nip49", "nip44", "nip05"] }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-keyring = { git = "https://github.com/rust-nostr/nostr" }
# Others
emojis = "0.6.4"

View File

@@ -10,6 +10,7 @@ path = "src/main.rs"
[dependencies]
ui = { path = "../ui" }
theme = { path = "../theme" }
common = { path = "../common" }
global = { path = "../global" }
chats = { path = "../chats" }

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use account::Account;
use anyhow::Error;
use global::get_client;
@@ -9,11 +11,10 @@ use gpui::{
use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use std::sync::Arc;
use theme::{ActiveTheme, Theme, ThemeMode};
use ui::{
button::{Button, ButtonVariants},
dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem},
theme::{ActiveTheme, Appearance, Theme},
ContextModal, IconName, Root, Sizable, TitleBar,
};
@@ -324,7 +325,7 @@ impl Render for ChatSpace {
.xsmall()
.ghost()
.map(|this| {
if cx.theme().appearance.is_dark() {
if cx.theme().mode.is_dark() {
this.icon(IconName::Sun)
} else {
this.icon(IconName::Moon)
@@ -332,15 +333,15 @@ impl Render for ChatSpace {
})
.on_click(cx.listener(
|_, _, window, cx| {
if cx.theme().appearance.is_dark() {
if cx.theme().mode.is_dark() {
Theme::change(
Appearance::Light,
ThemeMode::Light,
Some(window),
cx,
);
} else {
Theme::change(
Appearance::Dark,
ThemeMode::Dark,
Some(window),
cx,
);

View File

@@ -27,7 +27,8 @@ use nostr_sdk::{
};
use smol::Timer;
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
use ui::{theme::Theme, Root};
use theme::Theme;
use ui::Root;
pub(crate) mod asset;
pub(crate) mod chatspace;
@@ -286,7 +287,6 @@ fn main() {
// Open a window with default options
cx.open_window(opts, |window, cx| {
// Automatically sync theme with system appearance
#[cfg(not(target_os = "linux"))]
window
.observe_window_appearance(|window, cx| {
Theme::sync_system_appearance(Some(window), cx);

View File

@@ -1,3 +1,5 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::{anyhow, Error};
use async_utility::task::spawn;
use chats::{
@@ -8,7 +10,7 @@ use chats::{
use common::{nip96_upload, profile::SharedProfile};
use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{
div, img, impl_internal_actions, list, prelude::FluentBuilder, px, relative, svg, white,
div, img, impl_internal_actions, list, prelude::FluentBuilder, px, red, relative, svg, white,
AnyElement, App, AppContext, Context, Element, Empty, Entity, EventEmitter, Flatten,
FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit,
ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement, Styled,
@@ -18,7 +20,7 @@ use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::fs;
use std::{collections::HashMap, sync::Arc};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
@@ -27,7 +29,6 @@ use ui::{
notification::Notification,
popup_menu::PopupMenu,
text::RichText,
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, ContextModal, Disableable, Icon, IconName, Sizable, Size, StyledExt,
};
@@ -388,7 +389,7 @@ impl Chat {
.entry(item.id)
.or_insert_with(|| RichText::new(item.content.to_owned(), &item.mentions));
this.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
this.hover(|this| this.bg(cx.theme().surface_background))
.text_sm()
.child(
div()
@@ -397,10 +398,8 @@ impl Chat {
.top_0()
.w(px(2.))
.h_full()
.bg(cx.theme().transparent)
.group_hover("", |this| {
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
}),
.bg(cx.theme().border_transparent)
.group_hover("", |this| this.bg(cx.theme().element_active)),
)
.child(img(item.author.shared_avatar()).size_8().flex_shrink_0())
.child(
@@ -419,9 +418,7 @@ impl Chat {
)
.child(
div()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::NINE),
)
.text_color(cx.theme().text_placeholder)
.child(item.ago()),
),
)
@@ -437,12 +434,12 @@ impl Chat {
.top_0()
.w(px(2.))
.h_full()
.bg(cx.theme().transparent)
.group_hover("", |this| this.bg(cx.theme().danger)),
.bg(cx.theme().border_transparent)
.group_hover("", |this| this.bg(red())),
)
.child(img("brand/avatar.png").size_8().flex_shrink_0())
.text_sm()
.text_color(cx.theme().danger)
.text_color(red())
.child(content.clone()),
RoomMessage::Announcement => this
.w_full()
@@ -453,13 +450,13 @@ impl Chat {
.justify_center()
.text_center()
.text_xs()
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
.text_color(cx.theme().text_placeholder)
.line_height(relative(1.3))
.child(
svg()
.path("brand/coop.svg")
.size_10()
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
.text_color(cx.theme().elevated_surface_background),
)
.child(DESC),
})
@@ -492,10 +489,12 @@ impl Panel for Chat {
.items_center()
.size_5()
.rounded_full()
.bg(cx.theme().accent.step(cx, ColorScaleStep::THREE))
.child(Icon::new(IconName::UsersThreeFill).xsmall().text_color(
cx.theme().accent.step(cx, ColorScaleStep::TWELVE),
)),
.bg(cx.theme().element_disabled)
.child(
Icon::new(IconName::UsersThreeFill)
.xsmall()
.text_color(cx.theme().icon_accent),
),
)
}
})
@@ -567,7 +566,7 @@ impl Render for Chat {
))
.size_16()
.shadow_lg()
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.object_fit(ObjectFit::ScaleDown),
)
.child(
@@ -580,7 +579,7 @@ impl Render for Chat {
.items_center()
.justify_center()
.rounded_full()
.bg(cx.theme().danger)
.bg(red())
.child(
Icon::new(IconName::Close)
.size_2()
@@ -603,9 +602,7 @@ impl Render for Chat {
.flex()
.items_center()
.gap_1()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
.text_color(cx.theme().text_muted)
.child(
Button::new("upload")
.icon(Icon::new(IconName::Upload))

View File

@@ -1,9 +1,14 @@
use std::{
collections::{BTreeSet, HashSet},
time::Duration,
};
use anyhow::Error;
use chats::ChatRegistry;
use common::{profile::SharedProfile, random_name};
use global::get_client;
use gpui::{
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
div, img, impl_internal_actions, prelude::FluentBuilder, px, red, relative, uniform_list, App,
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign,
Window,
@@ -12,15 +17,11 @@ use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::Timer;
use std::{
collections::{BTreeSet, HashSet},
time::Duration,
};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::dock::DockPlacement,
input::{InputEvent, TextInput},
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, Icon, IconName, Sizable, Size, StyledExt,
};
@@ -335,24 +336,18 @@ impl Render for Compose {
div()
.px_3()
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_color(cx.theme().text_muted)
.child(DESCRIPTION),
)
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
this.child(
div()
.px_3()
.text_xs()
.text_color(cx.theme().danger)
.child(msg.clone()),
)
this.child(div().px_3().text_xs().text_color(red()).child(msg.clone()))
})
.child(
div().px_3().flex().flex_col().child(
div()
.h_10()
.border_b_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
.border_color(cx.theme().border)
.flex()
.items_center()
.gap_1()
@@ -399,9 +394,7 @@ impl Render for Compose {
.child(
div()
.text_xs()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
.text_color(cx.theme().text_muted)
.child("Your recently contacts will appear here."),
),
)
@@ -445,19 +438,13 @@ impl Render for Compose {
this.child(
Icon::new(IconName::CheckCircleFill)
.small()
.text_color(
cx.theme().accent.step(
cx,
ColorScaleStep::NINE,
),
),
.text_color(cx.theme().icon_accent),
)
})
.hover(|this| {
this.bg(cx
.theme()
.base
.step(cx, ColorScaleStep::THREE))
.elevated_surface_background)
})
.on_click(move |_, window, cx| {
window.dispatch_action(

View File

@@ -4,19 +4,19 @@ use account::Account;
use common::create_qr;
use global::get_client_keys;
use gpui::{
div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity,
div, img, prelude::FluentBuilder, red, relative, AnyElement, App, AppContext, Context, Entity,
EventEmitter, FocusHandle, Focusable, Image, IntoElement, ParentElement, Render, SharedString,
Styled, Subscription, Window,
};
use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
input::{InputEvent, TextInput},
notification::Notification,
popup_menu::PopupMenu,
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, Sizable, Size, StyledExt,
};
@@ -307,9 +307,7 @@ impl Render for Login {
)
.child(
div()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
.text_color(cx.theme().text_muted)
.child("Continue with Private Key or Bunker"),
),
)
@@ -334,7 +332,7 @@ impl Render for Login {
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger)
.text_color(red())
.child(error),
)
}),
@@ -348,7 +346,7 @@ impl Render for Login {
.flex()
.items_center()
.justify_center()
.bg(cx.theme().base.step(cx, ColorScaleStep::TWO))
.bg(cx.theme().surface_background)
.child(
div()
.flex()
@@ -364,17 +362,13 @@ impl Render for Login {
div()
.font_semibold()
.line_height(relative(1.2))
.text_color(
cx.theme().base.step(cx, ColorScaleStep::TWELVE),
)
.text_color(cx.theme().text)
.child("Continue with Nostr Connect"),
)
.child(
div()
.text_sm()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
.text_color(cx.theme().text_muted)
.child("Use Nostr Connect apps to scan the code"),
),
)
@@ -391,10 +385,10 @@ impl Render for Login {
.gap_2()
.rounded_2xl()
.shadow_md()
.when(cx.theme().appearance.is_dark(), |this| {
this.shadow_none().border_1().border_color(
cx.theme().base.step(cx, ColorScaleStep::SIX),
)
.when(cx.theme().mode.is_dark(), |this| {
this.shadow_none()
.border_1()
.border_color(cx.theme().border)
})
.bg(cx.theme().background)
.child(img(qr).h_64()),

View File

@@ -1,3 +1,5 @@
use std::str::FromStr;
use account::Account;
use async_utility::task::spawn;
use common::nip96_upload;
@@ -9,13 +11,12 @@ use gpui::{
};
use nostr_sdk::prelude::*;
use smol::fs;
use std::str::FromStr;
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
input::TextInput,
popup_menu::PopupMenu,
theme::{scale::ColorScaleStep, ActiveTheme},
Disableable, Icon, IconName, Sizable, Size, StyledExt,
};
@@ -290,7 +291,7 @@ impl Render for NewAccount {
.my_2()
.w_full()
.h_px()
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)),
.bg(cx.theme().elevated_surface_background),
)
.child(
Button::new("submit")

View File

@@ -2,11 +2,11 @@ use gpui::{
div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
popup_menu::PopupMenu,
theme::{scale::ColorScaleStep, ActiveTheme},
Icon, IconName, StyledExt,
};
@@ -91,7 +91,7 @@ impl Render for Onboarding {
svg()
.path("brand/coop.svg")
.size_16()
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
.text_color(cx.theme().elevated_surface_background),
)
.child(
div()
@@ -103,11 +103,7 @@ impl Render for Onboarding {
.line_height(relative(1.3))
.child(TITLE),
)
.child(
div()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(SUBTITLE),
),
.child(div().text_color(cx.theme().text_muted).child(SUBTITLE)),
),
)
.child(

View File

@@ -2,16 +2,16 @@ use async_utility::task::spawn;
use common::nip96_upload;
use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{
div, img, prelude::FluentBuilder, px, App, AppContext, Context, Entity, Flatten, IntoElement,
div, img, prelude::FluentBuilder, App, AppContext, Context, Entity, Flatten, IntoElement,
ParentElement, PathPromptOptions, Render, Styled, Task, Window,
};
use nostr_sdk::prelude::*;
use smol::fs;
use std::{str::FromStr, time::Duration};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
input::TextInput,
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, IconName, Sizable, Size,
};
@@ -249,8 +249,8 @@ impl Render for Profile {
div()
.w_full()
.h_32()
.bg(cx.theme().base.step(cx, ColorScaleStep::TWO))
.rounded(px(cx.theme().radius))
.bg(cx.theme().surface_background)
.rounded(cx.theme().radius)
.flex()
.flex_col()
.items_center()

View File

@@ -7,10 +7,10 @@ use gpui::{
};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
input::{InputEvent, TextInput},
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, IconName, Sizable,
};
@@ -241,8 +241,8 @@ impl Relays {
.flex()
.items_center()
.justify_between()
.rounded(px(cx.theme().radius))
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.text_xs()
.child(item)
.child(
@@ -298,7 +298,7 @@ impl Render for Relays {
.child(
div()
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_color(cx.theme().text_muted)
.child(MESSAGE),
)
.child(
@@ -320,7 +320,7 @@ impl Render for Relays {
.label("Add")
.small()
.ghost()
.rounded(px(cx.theme().radius))
.rounded_md()
.on_click(cx.listener(|this, _, window, cx| {
this.add(window, cx)
})),

View File

@@ -6,19 +6,19 @@ use chats::ChatRegistry;
use common::profile::SharedProfile;
use global::{constants::SEARCH_RELAYS, get_client};
use gpui::{
div, img, prelude::FluentBuilder, px, relative, uniform_list, App, AppContext, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, Window,
div, img, prelude::FluentBuilder, px, red, relative, uniform_list, App, AppContext, Context,
Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, Window,
};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::dock::DockPlacement,
indicator::Indicator,
input::{InputEvent, TextInput},
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, IconName, Sizable,
};
@@ -202,8 +202,6 @@ impl Search {
impl Render for Search {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let mute_color = cx.theme().base.step(cx, ColorScaleStep::NINE);
div()
.size_full()
.flex()
@@ -233,7 +231,7 @@ impl Render for Search {
div()
.px_3()
.text_xs()
.text_color(cx.theme().danger)
.text_color(red())
.child(error.clone()),
)
})
@@ -254,7 +252,7 @@ impl Render for Search {
.items_center()
.justify_center()
.text_sm()
.text_color(mute_color)
.text_color(cx.theme().text_muted)
.child("No one with that query could be found.")
} else {
this.child(
@@ -278,7 +276,7 @@ impl Render for Search {
.flex()
.items_center()
.justify_between()
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.child(
div()
.flex()
@@ -305,7 +303,10 @@ impl Render for Search {
this.child(
div()
.text_xs()
.text_color(mute_color)
.text_color(
cx.theme()
.text_muted,
)
.child(nip05),
)
},
@@ -335,10 +336,7 @@ impl Render for Search {
),
)
.hover(|this| {
this.bg(cx
.theme()
.base
.step(cx, ColorScaleStep::THREE))
this.bg(cx.theme().elevated_surface_background)
}),
);
}

View File

@@ -1,13 +1,11 @@
use std::rc::Rc;
use gpui::{
div, prelude::FluentBuilder, px, App, ClickEvent, Div, InteractiveElement, IntoElement,
div, prelude::FluentBuilder, App, ClickEvent, Div, InteractiveElement, IntoElement,
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
};
use ui::{
theme::{scale::ColorScaleStep, ActiveTheme},
Icon,
};
use theme::ActiveTheme;
use ui::Icon;
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
@@ -49,16 +47,12 @@ impl RenderOnce for SidebarButton {
self.base
.id(self.label.clone())
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.when_some(self.icon, |this, icon| {
this.child(
div()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(icon),
)
this.child(div().text_color(cx.theme().text_muted).child(icon))
})
.child(self.label.clone())
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.hover(|this| this.bg(cx.theme().elevated_surface_background))
.on_click(move |ev, window, cx| handler(ev, window, cx))
}
}

View File

@@ -1,15 +1,12 @@
use std::rc::Rc;
use gpui::{
div, percentage, prelude::FluentBuilder, px, App, ClickEvent, Div, Img, InteractiveElement,
div, percentage, prelude::FluentBuilder, App, ClickEvent, Div, Img, InteractiveElement,
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
Window,
};
use ui::{
theme::{scale::ColorScaleStep, ActiveTheme},
tooltip::Tooltip,
Collapsible, Icon, IconName, Sizable, StyledExt,
};
use theme::ActiveTheme;
use ui::{tooltip::Tooltip, Collapsible, Icon, IconName, Sizable, StyledExt};
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
@@ -96,9 +93,9 @@ impl RenderOnce for Parent {
.gap_2()
.px_2()
.h_8()
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_color(cx.theme().text_muted)
.font_medium()
.child(
Icon::new(IconName::CaretDown)
@@ -118,7 +115,7 @@ impl RenderOnce for Parent {
Tooltip::new(tooltip.clone(), window, cx).into()
})
})
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.hover(|this| this.bg(cx.theme().elevated_surface_background))
.on_click(move |ev, window, cx| handler(ev, window, cx)),
)
.when(!self.collapsed, |this| {
@@ -204,9 +201,9 @@ impl RenderOnce for Folder {
.gap_2()
.px_2()
.h_8()
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_color(cx.theme().text_muted)
.font_medium()
.child(
Icon::new(IconName::CaretDown)
@@ -226,7 +223,7 @@ impl RenderOnce for Folder {
Tooltip::new(tooltip.clone(), window, cx).into()
})
})
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.hover(|this| this.bg(cx.theme().elevated_surface_background))
.on_click(move |ev, window, cx| handler(ev, window, cx)),
)
.when(!self.collapsed, |this| {
@@ -291,7 +288,7 @@ impl RenderOnce for FolderItem {
.items_center()
.justify_between()
.text_sm()
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.child(
div()
.flex_1()
@@ -312,11 +309,11 @@ impl RenderOnce for FolderItem {
.items_center()
.size_5()
.rounded_full()
.bg(cx.theme().accent.step(cx, ColorScaleStep::THREE))
.bg(cx.theme().element_disabled)
.child(
Icon::new(IconName::UsersThreeFill).xsmall().text_color(
cx.theme().accent.step(cx, ColorScaleStep::TWELVE),
),
Icon::new(IconName::UsersThreeFill)
.xsmall()
.text_color(cx.theme().text_accent),
),
)
}
@@ -328,11 +325,11 @@ impl RenderOnce for FolderItem {
div()
.flex_shrink_0()
.text_xs()
.text_color(cx.theme().base.step(cx, ColorScaleStep::TEN))
.text_color(cx.theme().text_placeholder)
.child(description),
)
})
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.hover(|this| this.bg(cx.theme().elevated_surface_background))
.on_click(move |ev, window, cx| handler(ev, window, cx))
}
}

View File

@@ -15,6 +15,7 @@ use gpui::{
ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Task, Window,
};
use itertools::Itertools;
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonCustomVariant, ButtonVariants},
dock_area::{
@@ -23,7 +24,6 @@ use ui::{
},
popup_menu::{PopupMenu, PopupMenuExt},
skeleton::Skeleton,
theme::{scale::ColorScaleStep, ActiveTheme},
IconName, Sizable, StyledExt,
};
@@ -305,7 +305,7 @@ impl Render for Sidebar {
.items_center()
.text_xs()
.font_semibold()
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
.text_color(cx.theme().text_placeholder)
.child("Messages")
.child(
Button::new("menu")
@@ -320,13 +320,10 @@ impl Render for Sidebar {
.small()
.custom(
ButtonCustomVariant::new(window, cx)
.foreground(
cx.theme().base.step(cx, ColorScaleStep::NINE),
)
.color(cx.theme().transparent)
.hover(cx.theme().transparent)
.active(cx.theme().transparent)
.border(cx.theme().transparent),
.foreground(cx.theme().text_placeholder)
.color(cx.theme().ghost_element_background)
.hover(cx.theme().ghost_element_background)
.active(cx.theme().ghost_element_background),
)
.on_click(cx.listener(move |this, _, _, cx| {
this.split_into_folders(cx);

View File

@@ -3,10 +3,10 @@ use gpui::{
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
ParentElement, Render, Styled, Window,
};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
input::TextInput,
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Size,
};
@@ -90,7 +90,7 @@ impl Render for Subject {
.child(
div()
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_color(cx.theme().text_muted)
.child("Subject:"),
)
.child(self.input.clone())
@@ -98,7 +98,7 @@ impl Render for Subject {
div()
.text_xs()
.italic()
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
.text_color(cx.theme().text_placeholder)
.child(HELP_TEXT),
),
)

View File

@@ -2,11 +2,11 @@ use gpui::{
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use theme::ActiveTheme;
use ui::{
button::Button,
dock_area::panel::{Panel, PanelEvent},
popup_menu::PopupMenu,
theme::{scale::ColorScaleStep, ActiveTheme},
StyledExt,
};
@@ -87,12 +87,12 @@ impl Render for Welcome {
svg()
.path("brand/coop.svg")
.size_12()
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
.text_color(cx.theme().elevated_surface_background),
)
.child(
div()
.child("coop on nostr.")
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
.text_color(cx.theme().text_placeholder)
.font_semibold()
.text_sm(),
),

10
crates/theme/Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name = "theme"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
gpui.workspace = true
anyhow.workspace = true
log.workspace = true

1
crates/theme/LICENSE Normal file
View File

@@ -0,0 +1 @@
../../LICENSE

1
crates/theme/README.md Normal file
View File

@@ -0,0 +1 @@
CREDITS: [zed/theme](https://github.com/zed-industries/zed/tree/main/crates/theme)

View File

@@ -1,7 +1,15 @@
use super::scale::{ColorScaleSet, ColorScales};
use crate::theme::scale::ColorScale;
use gpui::{hsla, Hsla, Rgba};
use crate::scale::{ColorScale, ColorScaleSet, ColorScales};
pub(crate) fn neutral() -> ColorScaleSet {
gray()
}
pub(crate) fn brand() -> ColorScaleSet {
yellow()
}
/// Make a [gpui::Hsla] color.
///
/// - h: 0..360.0

375
crates/theme/src/lib.rs Normal file
View File

@@ -0,0 +1,375 @@
use std::ops::{Deref, DerefMut};
use colors::{brand, hsl, neutral};
use gpui::{black, px, white, App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance};
mod colors;
mod scale;
pub fn init(cx: &mut App) {
Theme::sync_system_appearance(None, cx);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ThemeColor {
/// Border color. Used for most borders, is usually a high contrast color.
pub border: Hsla,
/// Border color. Used for deemphasized borders, like a visual divider between two sections
pub border_variant: Hsla,
/// Border color. Used for focused elements, like keyboard focused list item.
pub border_focused: Hsla,
/// Border color. Used for selected elements, like an active search filter or selected checkbox.
pub border_selected: Hsla,
/// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change.
pub border_transparent: Hsla,
/// Border color. Used for disabled elements, like a disabled input or button.
pub border_disabled: Hsla,
/// Background color. Used for elevated surfaces, like a context menu, popup, or dialog.
pub elevated_surface_background: Hsla,
/// Background color. Used for grounded surfaces like a panel or tab.
pub surface_background: Hsla,
/// Background color. Used for the app background and blank panels or windows.
pub background: Hsla,
/// Text color. Used for the foreground of an element.
pub element_foreground: Hsla,
/// Background color. Used for the background of an element that should have a different background than the surface it's on.
///
/// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons...
///
/// For an element that should have the same background as the surface it's on, use `ghost_element_background`.
pub element_background: Hsla,
/// Background color. Used for the hover state of an element that should have a different background than the surface it's on.
///
/// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen.
pub element_hover: Hsla,
/// Background color. Used for the active state of an element that should have a different background than the surface it's on.
///
/// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed.
pub element_active: Hsla,
/// Background color. Used for the selected state of an element that should have a different background than the surface it's on.
///
/// Selected states are triggered by the element being selected (or "activated") by the user.
///
/// This could include a selected checkbox, a toggleable button that is toggled on, etc.
pub element_selected: Hsla,
/// Background Color. Used for the disabled state of a element that should have a different background than the surface it's on.
///
/// Disabled states are shown when a user cannot interact with an element, like a disabled button or input.
pub element_disabled: Hsla,
/// Background color. Used for the area that shows where a dragged element will be dropped.
pub drop_target_background: Hsla,
/// Used for the background of a ghost element that should have the same background as the surface it's on.
///
/// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons...
///
/// For an element that should have a different background than the surface it's on, use `element_background`.
pub ghost_element_background: Hsla,
/// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on.
///
/// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen.
pub ghost_element_hover: Hsla,
/// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on.
///
/// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed.
pub ghost_element_active: Hsla,
/// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on.
///
/// Selected states are triggered by the element being selected (or "activated") by the user.
///
/// This could include a selected checkbox, a toggleable button that is toggled on, etc.
pub ghost_element_selected: Hsla,
/// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on.
///
/// Disabled states are shown when a user cannot interact with an element, like a disabled button or input.
pub ghost_element_disabled: Hsla,
/// Text color. Default text color used for most text.
pub text: Hsla,
/// Text color. Color of muted or deemphasized text. It is a subdued version of the standard text color.
pub text_muted: Hsla,
/// Text color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data.
pub text_placeholder: Hsla,
/// Text color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search.
pub text_accent: Hsla,
/// Fill color. Used for the default fill color of an icon.
pub icon: Hsla,
/// Fill color. Used for the muted or deemphasized fill color of an icon.
///
/// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight.
pub icon_muted: Hsla,
/// Fill color. Used for the accent fill color of an icon.
///
/// This might be used to show when a toggleable icon button is selected.
pub icon_accent: Hsla,
/// The color of the scrollbar thumb.
pub scrollbar_thumb_background: Hsla,
/// The color of the scrollbar thumb when hovered over.
pub scrollbar_thumb_hover_background: Hsla,
/// The border color of the scrollbar thumb.
pub scrollbar_thumb_border: Hsla,
/// The background color of the scrollbar track.
pub scrollbar_track_background: Hsla,
/// The border color of the scrollbar track.
pub scrollbar_track_border: Hsla,
/// Background color. Used for the background of a panel
pub panel_background: Hsla,
/// Border color. Used for outline border.
pub ring: Hsla,
/// Background color. Used for inactive tab.
pub tab_inactive_background: Hsla,
/// Background color. Used for hovered tab.
pub tab_hover_background: Hsla,
/// Background color. Used for active tab.
pub tab_active_background: Hsla,
/// Background color. Used for Title Bar.
pub title_bar: Hsla,
/// Border color. Used for Title Bar.
pub title_bar_border: Hsla,
/// Background color. Used for modal's overlay.
pub overlay: Hsla,
/// Window border color.
///
/// # Platform specific:
///
/// This is only works on Linux, other platforms we can't change the window border color.
pub window_border: Hsla,
}
/// The default colors for the theme.
///
/// Themes that do not specify all colors are refined off of these defaults.
impl ThemeColor {
/// Returns the default colors for light themes.
///
/// Themes that do not specify all colors are refined off of these defaults.
pub fn light() -> Self {
Self {
border: neutral().light().step_6(),
border_variant: neutral().light().step_5(),
border_focused: brand().light().step_7(),
border_selected: brand().light().step_7(),
border_transparent: gpui::transparent_black(),
border_disabled: neutral().light().step_3(),
elevated_surface_background: neutral().light().step_3(),
surface_background: neutral().light().step_2(),
background: neutral().light().step_1(),
element_foreground: brand().light().step_12(),
element_background: brand().light().step_9(),
element_hover: brand().light_alpha().step_10(),
element_active: brand().light().step_10(),
element_selected: brand().light().step_10(),
element_disabled: brand().light_alpha().step_3(),
drop_target_background: brand().light_alpha().step_2(),
ghost_element_background: gpui::transparent_black(),
ghost_element_hover: neutral().light_alpha().step_3(),
ghost_element_active: neutral().light_alpha().step_4(),
ghost_element_selected: neutral().light_alpha().step_5(),
ghost_element_disabled: neutral().light_alpha().step_3(),
text: neutral().light().step_12(),
text_muted: neutral().light().step_11(),
text_placeholder: neutral().light().step_10(),
text_accent: brand().light().step_11(),
icon: neutral().light().step_11(),
icon_muted: neutral().light().step_10(),
icon_accent: brand().light().step_11(),
scrollbar_thumb_background: neutral().light_alpha().step_3(),
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
scrollbar_thumb_border: gpui::transparent_black(),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: neutral().light().step_5(),
panel_background: white(),
ring: brand().light().step_8(),
tab_active_background: neutral().light().step_5(),
tab_hover_background: neutral().light().step_4(),
tab_inactive_background: neutral().light().step_3(),
title_bar: gpui::transparent_black(),
title_bar_border: gpui::transparent_black(),
overlay: neutral().light_alpha().step_3(),
window_border: hsl(240.0, 5.9, 78.0),
}
}
/// Returns the default colors for dark themes.
///
/// Themes that do not specify all colors are refined off of these defaults.
pub fn dark() -> Self {
Self {
border: neutral().dark().step_6(),
border_variant: neutral().dark().step_5(),
border_focused: brand().dark().step_7(),
border_selected: brand().dark().step_7(),
border_transparent: gpui::transparent_black(),
border_disabled: neutral().light().step_3(),
elevated_surface_background: neutral().dark().step_3(),
surface_background: neutral().dark().step_2(),
background: neutral().dark().step_1(),
element_foreground: brand().dark().step_12(),
element_background: brand().dark().step_9(),
element_hover: brand().dark_alpha().step_10(),
element_active: brand().dark().step_10(),
element_selected: brand().dark().step_10(),
element_disabled: brand().dark_alpha().step_3(),
drop_target_background: brand().dark_alpha().step_2(),
ghost_element_background: gpui::transparent_black(),
ghost_element_hover: neutral().dark_alpha().step_3(),
ghost_element_active: neutral().dark_alpha().step_4(),
ghost_element_selected: neutral().dark_alpha().step_5(),
ghost_element_disabled: neutral().dark_alpha().step_3(),
text: neutral().dark().step_12(),
text_muted: neutral().dark().step_11(),
text_placeholder: neutral().dark().step_10(),
text_accent: brand().dark().step_11(),
icon: neutral().dark().step_11(),
icon_muted: neutral().dark().step_10(),
icon_accent: brand().dark().step_11(),
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
scrollbar_thumb_border: gpui::transparent_black(),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: neutral().dark().step_5(),
panel_background: black(),
ring: brand().dark().step_8(),
tab_active_background: neutral().dark().step_5(),
tab_hover_background: neutral().dark().step_4(),
tab_inactive_background: neutral().dark().step_3(),
title_bar: gpui::transparent_black(),
title_bar_border: gpui::transparent_black(),
overlay: neutral().dark_alpha().step_3(),
window_border: hsl(240.0, 3.7, 28.0),
}
}
}
pub trait ActiveTheme {
fn theme(&self) -> &Theme;
}
impl ActiveTheme for App {
#[inline(always)]
fn theme(&self) -> &Theme {
Theme::global(self)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Hash)]
pub enum ThemeMode {
Light,
#[default]
Dark,
}
impl ThemeMode {
pub fn is_dark(&self) -> bool {
matches!(self, Self::Dark)
}
/// Return lower_case theme name: `light`, `dark`.
pub fn name(&self) -> &'static str {
match self {
ThemeMode::Light => "light",
ThemeMode::Dark => "dark",
}
}
}
impl From<WindowAppearance> for ThemeMode {
fn from(appearance: WindowAppearance) -> Self {
match appearance {
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
}
}
}
#[derive(Debug, Clone)]
pub struct Theme {
pub colors: ThemeColor,
pub mode: ThemeMode,
pub font_family: SharedString,
pub font_size: Pixels,
pub radius: Pixels,
}
impl Deref for Theme {
type Target = ThemeColor;
fn deref(&self) -> &Self::Target {
&self.colors
}
}
impl DerefMut for Theme {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.colors
}
}
impl Global for Theme {}
impl Theme {
/// Returns the global theme reference
pub fn global(cx: &App) -> &Theme {
cx.global::<Theme>()
}
/// Returns the global theme mutable reference
pub fn global_mut(cx: &mut App) -> &mut Theme {
cx.global_mut::<Theme>()
}
/// Returns true if the theme is dark.
pub fn is_dark(&self) -> bool {
self.mode.is_dark()
}
/// Sync the theme with the system appearance
pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
let appearance = window
.as_ref()
.map(|window| window.appearance())
.unwrap_or_else(|| cx.window_appearance());
Self::change(appearance, window, cx);
}
pub fn change(mode: impl Into<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
let mode = mode.into();
let colors = match mode {
ThemeMode::Light => ThemeColor::light(),
ThemeMode::Dark => ThemeColor::dark(),
};
if !cx.has_global::<Theme>() {
let theme = Theme::from(colors);
cx.set_global(theme);
}
let theme = cx.global_mut::<Theme>();
theme.mode = mode;
theme.colors = colors;
if let Some(window) = window {
window.refresh();
}
}
}
impl From<ThemeColor> for Theme {
fn from(colors: ThemeColor) -> Self {
let mode = ThemeMode::default();
Theme {
font_size: px(15.),
font_family: if cfg!(target_os = "macos") {
".SystemUIFont".into()
} else if cfg!(target_os = "windows") {
"Segoe UI".into()
} else {
"FreeMono".into()
},
radius: px(5.),
mode,
colors,
}
}
}

View File

@@ -1,5 +1,4 @@
use crate::theme::{ActiveTheme, Appearance};
use gpui::{App, Hsla, SharedString};
use gpui::{Hsla, SharedString};
/// A collection of colors that are used to style the UI.
///
@@ -278,25 +277,4 @@ impl ColorScaleSet {
pub fn dark_alpha(&self) -> &ColorScale {
&self.dark_alpha
}
pub fn step(&self, cx: &App, step: ColorScaleStep) -> Hsla {
match cx.theme().appearance {
Appearance::Light => self.light().step(step),
Appearance::Dark => self.dark().step(step),
}
}
pub fn step_alpha(&self, cx: &App, step: ColorScaleStep) -> Hsla {
match cx.theme().appearance {
Appearance::Light => self.light_alpha.step(step),
Appearance::Dark => self.dark_alpha.step(step),
}
}
pub fn darken(&self, cx: &App) -> Hsla {
match cx.theme().appearance {
Appearance::Light => self.light.step_12(),
Appearance::Dark => self.dark.step_1(),
}
}
}

View File

@@ -6,6 +6,7 @@ publish.workspace = true
[dependencies]
common = { path = "../common" }
theme = { path = "../theme" }
nostr-sdk.workspace = true
gpui.workspace = true

View File

@@ -1,35 +1,23 @@
use crate::{
indicator::Indicator,
theme::{scale::ColorScaleStep, ActiveTheme},
tooltip::Tooltip,
Disableable, Icon, Selectable, Sizable, Size, StyledExt,
};
use gpui::{
div, prelude::FluentBuilder as _, px, relative, AnyElement, App, ClickEvent, Corners, Div,
Edges, ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, Window,
div, prelude::FluentBuilder as _, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla,
InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString,
StatefulInteractiveElement as _, Styled, Window,
};
use theme::ActiveTheme;
use crate::{
indicator::Indicator, tooltip::Tooltip, Disableable, Icon, Selectable, Sizable, Size, StyledExt,
};
pub enum ButtonRounded {
None,
Small,
Medium,
Large,
Size(Pixels),
}
impl From<Pixels> for ButtonRounded {
fn from(px: Pixels) -> Self {
ButtonRounded::Size(px)
}
Normal,
Full,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ButtonCustomVariant {
color: Hsla,
foreground: Hsla,
border: Hsla,
shadow: bool,
hover: Hsla,
active: Hsla,
}
@@ -66,12 +54,10 @@ pub trait ButtonVariants: Sized {
impl ButtonCustomVariant {
pub fn new(_window: &Window, cx: &App) -> Self {
Self {
color: cx.theme().accent.step(cx, ColorScaleStep::NINE),
foreground: cx.theme().accent.step(cx, ColorScaleStep::ONE),
border: cx.theme().accent.step(cx, ColorScaleStep::TEN),
hover: cx.theme().accent.step(cx, ColorScaleStep::TEN),
active: cx.theme().accent.step(cx, ColorScaleStep::ELEVEN),
shadow: true,
color: cx.theme().element_background,
foreground: cx.theme().element_foreground,
hover: cx.theme().element_hover,
active: cx.theme().element_active,
}
}
@@ -85,11 +71,6 @@ impl ButtonCustomVariant {
self
}
pub fn border(mut self, color: Hsla) -> Self {
self.border = color;
self
}
pub fn hover(mut self, color: Hsla) -> Self {
self.hover = color;
self
@@ -99,11 +80,6 @@ impl ButtonCustomVariant {
self.active = color;
self
}
pub fn shadow(mut self, shadow: bool) -> Self {
self.shadow = shadow;
self
}
}
/// The variant of the Button.
@@ -149,12 +125,9 @@ pub struct Button {
disabled: bool,
variant: ButtonVariant,
rounded: ButtonRounded,
border_corners: Corners<bool>,
border_edges: Edges<bool>,
size: Size,
reverse: bool,
bold: bool,
centered: bool,
tooltip: Option<SharedString>,
on_click: OnClick,
loading: bool,
@@ -179,9 +152,7 @@ impl Button {
disabled: false,
selected: false,
variant: ButtonVariant::default(),
rounded: ButtonRounded::Medium,
border_corners: Corners::all(true),
border_edges: Edges::all(true),
rounded: ButtonRounded::Normal,
size: Size::Medium,
tooltip: None,
on_click: None,
@@ -189,7 +160,6 @@ impl Button {
loading: false,
reverse: false,
bold: false,
centered: true,
children: Vec::new(),
loading_icon: None,
}
@@ -201,18 +171,6 @@ impl Button {
self
}
/// Set the border corners side of the Button.
pub(crate) fn border_corners(mut self, corners: impl Into<Corners<bool>>) -> Self {
self.border_corners = corners.into();
self
}
/// Set the border edges of the Button.
pub(crate) fn border_edges(mut self, edges: impl Into<Edges<bool>>) -> Self {
self.border_edges = edges.into();
self
}
/// Set label to the Button, if no label is set, the button will be in Icon Button mode.
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
@@ -243,11 +201,6 @@ impl Button {
self
}
pub fn not_centered(mut self) -> Self {
self.centered = false;
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
@@ -335,11 +288,12 @@ impl RenderOnce for Button {
.id(self.id)
.flex()
.items_center()
.when(self.centered, |this| this.justify_center())
.justify_center()
.cursor_pointer()
.overflow_hidden()
.when(cx.theme().shadow && normal_style.shadow, |this| {
this.shadow_sm()
.map(|this| match self.rounded {
ButtonRounded::Normal => this.rounded(cx.theme().radius),
ButtonRounded::Full => this.rounded_full(),
})
.when(!style.no_padding(), |this| {
if self.label.is_none() && self.children.is_empty() {
@@ -361,50 +315,20 @@ impl RenderOnce for Button {
}
}
})
.when(
self.border_corners.top_left && self.border_corners.bottom_left,
|this| match self.rounded {
ButtonRounded::Small => this.rounded_l(px(cx.theme().radius * 0.5)),
ButtonRounded::Medium => this.rounded_l(px(cx.theme().radius)),
ButtonRounded::Large => this.rounded_l(px(cx.theme().radius * 1.6)),
ButtonRounded::Size(px) => this.rounded_l(px),
ButtonRounded::None => this.rounded_none(),
},
)
.when(
self.border_corners.top_right && self.border_corners.bottom_right,
|this| match self.rounded {
ButtonRounded::Small => this.rounded_r(px(cx.theme().radius * 0.5)),
ButtonRounded::Medium => this.rounded_r(px(cx.theme().radius)),
ButtonRounded::Large => this.rounded_r(px(cx.theme().radius * 1.6)),
ButtonRounded::Size(px) => this.rounded_r(px),
ButtonRounded::None => this.rounded_none(),
},
)
.when(self.border_edges.left, |this| this.border_l_1())
.when(self.border_edges.right, |this| this.border_r_1())
.when(self.border_edges.top, |this| this.border_t_1())
.when(self.border_edges.bottom, |this| this.border_b_1())
.text_color(normal_style.fg)
.when(self.selected, |this| {
let selected_style = style.selected(window, cx);
this.bg(selected_style.bg)
.border_color(selected_style.border)
.text_color(selected_style.fg)
this.bg(selected_style.bg).text_color(selected_style.fg)
})
.when(!self.disabled && !self.selected, |this| {
this.border_color(normal_style.border)
.bg(normal_style.bg)
this.bg(normal_style.bg)
.when(normal_style.underline, |this| this.text_decoration_1())
.hover(|this| {
let hover_style = style.hovered(window, cx);
this.bg(hover_style.bg).border_color(hover_style.border)
this.bg(hover_style.bg)
})
.active(|this| {
let active_style = style.active(window, cx);
this.bg(active_style.bg)
.border_color(active_style.border)
.text_color(active_style.fg)
this.bg(active_style.bg).text_color(active_style.fg)
})
})
.when_some(
@@ -427,9 +351,9 @@ impl RenderOnce for Button {
this.cursor_not_allowed()
.bg(disabled_style.bg)
.text_color(disabled_style.fg)
.border_color(disabled_style.border)
.shadow_none()
})
.text_color(normal_style.fg)
.child({
div()
.flex()
@@ -474,45 +398,26 @@ impl RenderOnce for Button {
struct ButtonVariantStyle {
bg: Hsla,
border: Hsla,
fg: Hsla,
underline: bool,
shadow: bool,
}
impl ButtonVariant {
fn bg_color(&self, _window: &Window, cx: &App) -> Hsla {
match self {
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::NINE),
ButtonVariant::Primary => cx.theme().element_background,
ButtonVariant::Custom(colors) => colors.color,
_ => cx.theme().transparent,
_ => cx.theme().ghost_element_background,
}
}
fn text_color(&self, _window: &Window, cx: &App) -> Hsla {
match self {
ButtonVariant::Primary => match cx.theme().accent.name().to_string().as_str() {
"Sky" => cx.theme().base.darken(cx),
"Mint" => cx.theme().base.darken(cx),
"Lime" => cx.theme().base.darken(cx),
"Amber" => cx.theme().base.darken(cx),
"Yellow" => cx.theme().base.darken(cx),
_ => cx.theme().accent.step(cx, ColorScaleStep::ONE),
},
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::NINE),
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::TWELVE),
ButtonVariant::Primary => cx.theme().element_foreground,
ButtonVariant::Link => cx.theme().text_accent,
ButtonVariant::Ghost => cx.theme().text_muted,
ButtonVariant::Custom(colors) => colors.foreground,
_ => cx.theme().base.step(cx, ColorScaleStep::TWELVE),
}
}
fn border_color(&self, _window: &Window, cx: &App) -> Hsla {
match self {
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::NINE),
ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
cx.theme().transparent
}
ButtonVariant::Custom(colors) => colors.border,
_ => cx.theme().text,
}
}
@@ -520,142 +425,79 @@ impl ButtonVariant {
matches!(self, ButtonVariant::Link)
}
fn shadow(&self, _window: &Window, _cx: &App) -> bool {
match self {
ButtonVariant::Primary => true,
ButtonVariant::Custom(c) => c.shadow,
_ => false,
}
}
fn normal(&self, window: &Window, cx: &App) -> ButtonVariantStyle {
let bg = self.bg_color(window, cx);
let border = self.border_color(window, cx);
let fg = self.text_color(window, cx);
let underline = self.underline(window, cx);
let shadow = self.shadow(window, cx);
ButtonVariantStyle {
bg,
border,
fg,
underline,
shadow,
}
ButtonVariantStyle { bg, fg, underline }
}
fn hovered(&self, window: &Window, cx: &App) -> ButtonVariantStyle {
let bg = match self {
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::FOUR),
ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Primary => cx.theme().element_hover,
ButtonVariant::Ghost => cx.theme().ghost_element_hover,
ButtonVariant::Link => cx.theme().ghost_element_background,
ButtonVariant::Text => cx.theme().ghost_element_background,
ButtonVariant::Custom(colors) => colors.hover,
};
let border = self.border_color(window, cx);
let fg = match self {
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::TWELVE),
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Ghost => cx.theme().text,
ButtonVariant::Link => cx.theme().text_accent,
_ => self.text_color(window, cx),
};
let underline = self.underline(window, cx);
let shadow = self.shadow(window, cx);
ButtonVariantStyle {
bg,
border,
fg,
underline,
shadow,
}
ButtonVariantStyle { bg, fg, underline }
}
fn active(&self, window: &Window, cx: &App) -> ButtonVariantStyle {
let bg = match self {
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::THREE),
ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Primary => cx.theme().element_active,
ButtonVariant::Ghost => cx.theme().ghost_element_active,
ButtonVariant::Custom(colors) => colors.active,
_ => cx.theme().ghost_element_background,
};
let fg = match self {
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::NINE),
ButtonVariant::Text => cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
ButtonVariant::Link => cx.theme().text_accent,
ButtonVariant::Text => cx.theme().text,
_ => self.text_color(window, cx),
};
let border = self.border_color(window, cx);
let underline = self.underline(window, cx);
let shadow = self.shadow(window, cx);
ButtonVariantStyle {
bg,
border,
fg,
underline,
shadow,
}
ButtonVariantStyle { bg, fg, underline }
}
fn selected(&self, window: &Window, cx: &App) -> ButtonVariantStyle {
let bg = match self {
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::THREE),
ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Primary => cx.theme().element_selected,
ButtonVariant::Ghost => cx.theme().ghost_element_selected,
ButtonVariant::Custom(colors) => colors.active,
_ => cx.theme().ghost_element_background,
};
let fg = match self {
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Text => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Link => cx.theme().text_accent,
ButtonVariant::Text => cx.theme().text,
_ => self.text_color(window, cx),
};
let border = self.border_color(window, cx);
let underline = self.underline(window, cx);
let shadow = self.shadow(window, cx);
ButtonVariantStyle {
bg,
border,
fg,
underline,
shadow,
}
ButtonVariantStyle { bg, fg, underline }
}
fn disabled(&self, window: &Window, cx: &App) -> ButtonVariantStyle {
let bg = match self {
ButtonVariant::Link | ButtonVariant::Ghost | ButtonVariant::Text => {
cx.theme().transparent
cx.theme().ghost_element_disabled
}
_ => cx.theme().base.step(cx, ColorScaleStep::THREE),
_ => cx.theme().element_disabled,
};
let fg = match self {
ButtonVariant::Primary => match cx.theme().accent.name().to_string().as_str() {
"Sky" => cx.theme().base.darken(cx),
"Mint" => cx.theme().base.darken(cx),
"Lime" => cx.theme().base.darken(cx),
"Amber" => cx.theme().base.darken(cx),
"Yellow" => cx.theme().base.darken(cx),
_ => cx.theme().accent.step(cx, ColorScaleStep::ONE),
},
_ => cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
ButtonVariant::Primary => cx.theme().text_muted, // TODO: use a different color?
_ => cx.theme().text_muted,
};
let border = bg;
let underline = self.underline(window, cx);
let shadow = false;
ButtonVariantStyle {
bg,
border,
fg,
underline,
shadow,
}
ButtonVariantStyle { bg, fg, underline }
}
}

View File

@@ -1,201 +0,0 @@
use gpui::{
div, prelude::FluentBuilder as _, App, Corners, Div, Edges, ElementId, InteractiveElement,
IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement as _, Styled, Window,
};
use std::{cell::Cell, rc::Rc};
use crate::{
button::{Button, ButtonVariant, ButtonVariants},
Disableable, Sizable, Size,
};
type OnClick = Option<Box<dyn Fn(&Vec<usize>, &mut Window, &mut App) + 'static>>;
/// A ButtonGroup element, to wrap multiple buttons in a group.
#[derive(IntoElement)]
pub struct ButtonGroup {
pub base: Div,
id: ElementId,
children: Vec<Button>,
multiple: bool,
disabled: bool,
// The button props
compact: Option<bool>,
variant: Option<ButtonVariant>,
size: Option<Size>,
on_click: OnClick,
}
impl Disableable for ButtonGroup {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl ButtonGroup {
/// Creates a new ButtonGroup.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
base: div(),
children: Vec::new(),
id: id.into(),
variant: None,
size: None,
compact: None,
multiple: false,
disabled: false,
on_click: None,
}
}
/// Adds a button as a child to the ButtonGroup.
pub fn child(mut self, child: Button) -> Self {
self.children.push(child.disabled(self.disabled));
self
}
/// With the multiple selection mode.
pub fn multiple(mut self, multiple: bool) -> Self {
self.multiple = multiple;
self
}
/// With the compact mode for the ButtonGroup.
pub fn compact(mut self) -> Self {
self.compact = Some(true);
self
}
/// Sets the on_click handler for the ButtonGroup.
///
/// The handler first argument is a vector of the selected button indices.
pub fn on_click(
mut self,
handler: impl Fn(&Vec<usize>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Box::new(handler));
self
}
}
impl Sizable for ButtonGroup {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = Some(size.into());
self
}
}
impl Styled for ButtonGroup {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl ButtonVariants for ButtonGroup {
fn with_variant(mut self, variant: ButtonVariant) -> Self {
self.variant = Some(variant);
self
}
}
impl RenderOnce for ButtonGroup {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let children_len = self.children.len();
let mut selected_ixs: Vec<usize> = Vec::new();
let state = Rc::new(Cell::new(None));
for (ix, child) in self.children.iter().enumerate() {
if child.selected {
selected_ixs.push(ix);
}
}
self.base
.id(self.id)
.flex()
.items_center()
.children(
self.children
.into_iter()
.enumerate()
.map(|(child_index, child)| {
let state = Rc::clone(&state);
if children_len == 1 {
child
} else if child_index == 0 {
// First
child
.border_corners(Corners {
top_left: true,
top_right: false,
bottom_left: true,
bottom_right: false,
})
.border_edges(Edges {
left: true,
top: true,
right: true,
bottom: true,
})
} else if child_index == children_len - 1 {
// Last
child
.border_edges(Edges {
left: false,
top: true,
right: true,
bottom: true,
})
.border_corners(Corners {
top_left: false,
top_right: true,
bottom_left: false,
bottom_right: true,
})
} else {
// Middle
child
.border_corners(Corners::all(false))
.border_edges(Edges {
left: false,
top: true,
right: true,
bottom: true,
})
}
.stop_propagation(false)
.when_some(self.size, |this, size| this.with_size(size))
.when_some(self.variant, |this, variant| this.with_variant(variant))
.on_click(move |_, _, _| {
state.set(Some(child_index));
})
}),
)
.when_some(
self.on_click.filter(|_| !self.disabled),
move |this, on_click| {
this.on_click(move |_, window, cx| {
let mut selected_ixs = selected_ixs.clone();
if let Some(ix) = state.get() {
if self.multiple {
if let Some(pos) = selected_ixs.iter().position(|&i| i == ix) {
selected_ixs.remove(pos);
} else {
selected_ixs.push(ix);
}
} else {
selected_ixs.clear();
selected_ixs.push(ix);
}
}
on_click(&selected_ixs, window, cx);
})
},
)
}
}

View File

@@ -1,13 +1,11 @@
use crate::{
h_flex,
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, Disableable, IconName, Selectable,
};
use gpui::{
div, prelude::FluentBuilder as _, relative, svg, App, ElementId, InteractiveElement,
IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _,
Styled as _, Window,
};
use theme::ActiveTheme;
use crate::{h_flex, v_flex, Disableable, IconName, Selectable};
type OnClick = Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>;
@@ -68,15 +66,9 @@ impl Selectable for Checkbox {
impl RenderOnce for Checkbox {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (color, icon_color) = if self.disabled {
(
cx.theme().base.step(cx, ColorScaleStep::THREE),
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
(cx.theme().ghost_element_disabled, cx.theme().text_muted)
} else {
(
cx.theme().accent.step(cx, ColorScaleStep::NINE),
cx.theme().accent.step(cx, ColorScaleStep::ONE),
)
(cx.theme().text_accent, cx.theme().surface_background)
};
h_flex()
@@ -93,7 +85,7 @@ impl RenderOnce for Checkbox {
.size_4()
.flex_shrink_0()
.map(|this| match self.checked {
false => this.bg(cx.theme().transparent),
false => this.bg(cx.theme().ghost_element_background),
_ => this.bg(color),
})
.child(
@@ -111,8 +103,7 @@ impl RenderOnce for Checkbox {
)
.map(|this| {
if let Some(label) = self.label {
this.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(
this.text_color(cx.theme().text_muted).child(
div()
.w_full()
.overflow_x_hidden()
@@ -126,7 +117,7 @@ impl RenderOnce for Checkbox {
})
.when(self.disabled, |this| {
this.cursor_not_allowed()
.text_color(cx.theme().base.step(cx, ColorScaleStep::TEN))
.text_color(cx.theme().text_placeholder)
})
.when_some(
self.on_click.filter(|_| !self.disabled),

View File

@@ -1,158 +0,0 @@
use gpui::{
prelude::FluentBuilder, AnyElement, App, ClipboardItem, Element, ElementId, GlobalElementId,
IntoElement, LayoutId, ParentElement, SharedString, Styled, Window,
};
use std::{cell::RefCell, rc::Rc, time::Duration};
use crate::{
button::{Button, ButtonVariants as _},
h_flex, IconName, Sizable as _,
};
type ContentBuilder = Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement>>;
type CopiedCallback = Option<Rc<dyn Fn(SharedString, &mut Window, &mut App)>>;
pub struct Clipboard {
id: ElementId,
value: SharedString,
content_builder: ContentBuilder,
copied_callback: CopiedCallback,
}
impl Clipboard {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
value: "".into(),
content_builder: None,
copied_callback: None,
}
}
pub fn value(mut self, value: impl Into<SharedString>) -> Self {
self.value = value.into();
self
}
pub fn content<E, F>(mut self, builder: F) -> Self
where
E: IntoElement,
F: Fn(&mut Window, &mut App) -> E + 'static,
{
self.content_builder = Some(Box::new(move |window, cx| {
builder(window, cx).into_any_element()
}));
self
}
pub fn on_copied<F>(mut self, handler: F) -> Self
where
F: Fn(SharedString, &mut Window, &mut App) + 'static,
{
self.copied_callback = Some(Rc::new(handler));
self
}
}
impl IntoElement for Clipboard {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
#[derive(Default)]
pub struct ClipboardState {
copied: Rc<RefCell<bool>>,
}
impl Element for Clipboard {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
window.with_element_state::<ClipboardState, _>(global_id.unwrap(), |state, window| {
let state = state.unwrap_or_default();
let content_element = self
.content_builder
.as_ref()
.map(|builder| builder(window, cx).into_any_element());
let value = self.value.clone();
let clipboard_id = self.id.clone();
let copied_callback = self.copied_callback.as_ref().map(|c| c.clone());
let copied = state.copied.clone();
let copide_value = *copied.borrow();
let mut element = h_flex()
.gap_1()
.items_center()
.when_some(content_element, |this, element| this.child(element))
.child(
Button::new(clipboard_id)
.icon(if copide_value {
IconName::Check
} else {
IconName::Copy
})
.ghost()
.xsmall()
.when(!copide_value, |this| {
this.on_click(move |_, window, cx| {
cx.stop_propagation();
cx.write_to_clipboard(ClipboardItem::new_string(value.to_string()));
*copied.borrow_mut() = true;
let copied = copied.clone();
cx.spawn(async move |cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
*copied.borrow_mut() = false;
})
.detach();
if let Some(callback) = &copied_callback {
callback(value.clone(), window, cx);
}
})
}),
)
.into_any_element();
((element.request_layout(window, cx), element), state)
})
}
fn prepaint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: gpui::Bounds<gpui::Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) {
element.prepaint(window, cx);
}
fn paint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: gpui::Bounds<gpui::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx)
}
}

View File

@@ -2,8 +2,7 @@ use gpui::{
div, prelude::FluentBuilder as _, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce,
SharedString, Styled,
};
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
use theme::ActiveTheme;
/// A divider that can be either vertical or horizontal.
#[derive(IntoElement)]
@@ -65,9 +64,7 @@ impl RenderOnce for Divider {
Axis::Vertical => this.w(px(2.)).h_full(),
Axis::Horizontal => this.h(px(2.)).w_full(),
})
.bg(self
.color
.unwrap_or(cx.theme().base.step(cx, ColorScaleStep::FIVE))),
.bg(self.color.unwrap_or(cx.theme().border_variant)),
)
.when_some(self.label, |this, label| {
this.child(

View File

@@ -1,16 +1,17 @@
use std::sync::Arc;
use gpui::{
div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Entity,
InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use theme::ActiveTheme;
use super::{DockArea, DockItem};
use crate::{
dock_area::{panel::PanelView, tab_panel::TabPanel},
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE},
theme::{scale::ColorScaleStep, ActiveTheme as _},
AxisExt as _, StyledExt,
};
@@ -268,7 +269,7 @@ impl Dock {
.child(
div()
.rounded_full()
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::SIX)))
.hover(|this| this.bg(cx.theme().border_variant))
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
)

View File

@@ -1,3 +1,12 @@
use gpui::{
div, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength,
DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
};
use std::sync::Arc;
use theme::ActiveTheme;
use super::{
panel::PanelView, stack_panel::StackPanel, ClosePanel, DockArea, PanelEvent, PanelStyle,
ToggleZoom,
@@ -8,16 +17,8 @@ use crate::{
h_flex,
popup_menu::{PopupMenu, PopupMenuExt},
tab::{tab_bar::TabBar, Tab},
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt,
};
use gpui::{
div, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength,
DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
};
use std::sync::Arc;
#[derive(Clone)]
struct TabState {
@@ -53,11 +54,11 @@ impl Render for DragPanel {
.justify_center()
.overflow_hidden()
.whitespace_nowrap()
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.text_xs()
.shadow_lg()
.bg(cx.theme().background)
.text_color(cx.theme().accent.step(cx, ColorScaleStep::TWELVE))
.text_color(cx.theme().text_accent)
.child(self.panel.title(cx))
}
}
@@ -639,9 +640,7 @@ impl TabPanel {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(
cx.theme().base.step(cx, ColorScaleStep::FIVE),
)
.border_color(cx.theme().border)
})
.on_drop(cx.listener(
move |this, drag: &DragPanel, window, cx| {
@@ -660,10 +659,10 @@ impl TabPanel {
.h_full()
.flex_grow()
.min_w_16()
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.when(state.droppable, |this| {
this.drag_over::<DragPanel>(|this, _, _, cx| {
this.bg(cx.theme().base.step(cx, ColorScaleStep::TWO))
this.bg(cx.theme().surface_background)
})
.on_drop(cx.listener(
move |this, drag: &DragPanel, window, cx| {
@@ -718,8 +717,8 @@ impl TabPanel {
.size_full()
.rounded_lg()
.shadow_sm()
.when(cx.theme().appearance.is_dark(), |this| this.shadow_lg())
.bg(cx.theme().background)
.when(cx.theme().mode.is_dark(), |this| this.shadow_lg())
.bg(cx.theme().panel_background)
.overflow_hidden()
.child(
active_panel
@@ -738,8 +737,8 @@ impl TabPanel {
div()
.rounded_lg()
.border_1()
.border_color(cx.theme().accent.step(cx, ColorScaleStep::FOUR))
.bg(cx.theme().accent.step_alpha(cx, ColorScaleStep::THREE))
.border_color(cx.theme().element_disabled)
.bg(cx.theme().drop_target_background)
.size_full(),
)
.map(|this| match self.will_split_placement {

View File

@@ -5,11 +5,11 @@ use gpui::{
Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
WeakEntity, Window,
};
use theme::ActiveTheme;
use crate::{
h_flex,
list::{self, List, ListDelegate, ListItem},
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, Icon, IconName, Sizable, Size, StyleSized, StyledExt,
};
@@ -216,7 +216,7 @@ where
h_flex()
.justify_center()
.py_6()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_color(cx.theme().text_muted)
.child(Icon::new(IconName::Inbox).size(px(28.)))
.into_any_element()
}
@@ -545,9 +545,7 @@ where
.when_some(self.title_prefix.clone(), |this, prefix| this.child(prefix))
.child(title.clone())
} else {
div()
.text_color(cx.theme().accent.step(cx, ColorScaleStep::ELEVEN))
.child(
div().text_color(cx.theme().text_accent).child(
self.placeholder
.clone()
.unwrap_or_else(|| "Please select".into()),
@@ -555,8 +553,7 @@ where
};
title.when(self.disabled, |this| {
this.cursor_not_allowed()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
this.cursor_not_allowed().text_color(cx.theme().text_muted)
})
}
}
@@ -623,9 +620,9 @@ where
.justify_between()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::SIX))
.rounded(px(cx.theme().radius))
.when(cx.theme().shadow, |this| this.shadow_sm())
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.shadow_sm()
.map(|this| {
if self.disabled {
this.cursor_not_allowed()
@@ -672,10 +669,8 @@ where
Icon::new(icon)
.xsmall()
.text_color(match self.disabled {
true => cx.theme().base.step(cx, ColorScaleStep::TEN),
false => {
cx.theme().base.step(cx, ColorScaleStep::ELEVEN)
}
true => cx.theme().icon_muted,
false => cx.theme().icon,
})
.when(self.disabled, |this| this.cursor_not_allowed()),
)
@@ -706,10 +701,8 @@ where
.mt_1p5()
.bg(cx.theme().background)
.border_1()
.border_color(
cx.theme().base.step(cx, ColorScaleStep::SEVEN),
)
.rounded(px(cx.theme().radius))
.border_color(cx.theme().border_focused)
.rounded(cx.theme().radius)
.shadow_md()
.on_mouse_down_out(|_, _, cx| {
cx.dispatch_action(&Escape);

View File

@@ -6,12 +6,12 @@ use gpui::{
StatefulInteractiveElement, Styled, WeakEntity, Window,
};
use serde::Deserialize;
use theme::ActiveTheme;
use crate::{
button::{Button, ButtonVariants},
input::TextInput,
popover::{Popover, PopoverContent},
theme::{scale::ColorScaleStep, ActiveTheme},
Icon,
};
@@ -99,11 +99,9 @@ impl RenderOnce for EmojiPicker {
.flex()
.items_center()
.justify_center()
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.child(e.clone())
.hover(|this| {
this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
})
.hover(|this| this.bg(cx.theme().ghost_element_hover))
.on_click({
let item = e.clone();
let input = input.upgrade();

View File

@@ -1,12 +1,11 @@
use crate::{
theme::{scale::ColorScaleStep, ActiveTheme},
Sizable, Size,
};
use gpui::{
prelude::FluentBuilder as _, svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement,
Radians, Render, RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation,
Window,
};
use theme::ActiveTheme;
use crate::{Sizable, Size};
#[derive(IntoElement, Clone)]
pub enum IconName {
@@ -295,9 +294,7 @@ impl Render for Icon {
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let text_color = self
.text_color
.unwrap_or_else(|| cx.theme().base.step(cx, ColorScaleStep::ELEVEN));
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
svg()
.flex_none()

View File

@@ -1,11 +1,12 @@
use super::TextInput;
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
use gpui::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path,
Pixels, Point, Style, TextRun, UnderlineStyle, Window, WrappedLine,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use super::TextInput;
const RIGHT_MARGIN: Pixels = px(5.);
const BOTTOM_MARGIN: Pixels = px(20.);
@@ -149,7 +150,7 @@ impl TextElement {
),
size(px(1.), cursor_height),
),
cx.theme().accent.step(cx, ColorScaleStep::TEN),
cx.theme().element_active,
))
};
}
@@ -342,17 +343,11 @@ impl Element for TextElement {
let mut bounds = bounds;
let (display_text, text_color) = if text.is_empty() {
(
placeholder,
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
(placeholder, cx.theme().text_muted)
} else if input.masked {
(
"*".repeat(text.chars().count()).into(),
cx.theme().base.step(cx, ColorScaleStep::TWELVE),
)
("*".repeat(text.chars().count()).into(), cx.theme().text)
} else {
(text, cx.theme().base.step(cx, ColorScaleStep::TWELVE))
(text, cx.theme().text)
};
let run = TextRun {
@@ -471,7 +466,7 @@ impl Element for TextElement {
// Paint selections
if let Some(path) = prepaint.selection_path.take() {
window.paint_path(path, cx.theme().accent.step(cx, ColorScaleStep::FOUR));
window.paint_path(path, cx.theme().element_disabled);
}
// Paint multi line text

View File

@@ -1,3 +1,5 @@
use std::{cell::Cell, ops::Range, rc::Rc};
use gpui::{
actions, div, point, prelude::FluentBuilder as _, px, AnyElement, App, AppContext, Bounds,
ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable,
@@ -6,7 +8,7 @@ use gpui::{
ScrollWheelEvent, SharedString, Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
};
use smallvec::SmallVec;
use std::{cell::Cell, ops::Range, rc::Rc};
use theme::ActiveTheme;
use unicode_segmentation::*;
use super::{blink_cursor::BlinkCursor, change::Change, element::TextElement};
@@ -14,7 +16,6 @@ use crate::{
history::History,
indicator::Indicator,
scroll::{Scrollbar, ScrollbarAxis, ScrollbarState},
theme::{scale::ColorScaleStep, ActiveTheme},
Sizable, Size, StyleSized, StyledExt,
};
@@ -1624,9 +1625,8 @@ impl Render for TextInput {
.cursor_text()
.when(self.multi_line, |this| this.h_auto())
.when(self.appearance, |this| {
this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.rounded(px(cx.theme().radius))
.when(cx.theme().shadow, |this| this.shadow_sm())
this.bg(cx.theme().elevated_surface_background)
.rounded(cx.theme().radius)
.when(focused, |this| this.outline(window, cx))
.when(prefix.is_none(), |this| this.input_pl(self.size))
.when(suffix.is_none(), |this| this.input_pr(self.size))
@@ -1642,7 +1642,7 @@ impl Render for TextInput {
.child(TextElement::new(cx.entity().clone())),
)
.when(self.loading, |this| {
this.child(Indicator::new().color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)))
this.child(Indicator::new().color(cx.theme().text_muted))
})
.children(suffix)
.when(self.is_multi_line(), |this| {

View File

@@ -1,8 +1,24 @@
pub use event::InteractiveElementExt;
pub use focusable::FocusableCycle;
pub use icon::*;
pub use root::{ContextModal, Root};
pub use styled::*;
pub use title_bar::*;
pub use window_border::{window_border, WindowBorder};
pub use crate::Disableable;
mod event;
mod focusable;
mod icon;
mod root;
mod styled;
mod title_bar;
mod window_border;
pub mod animation;
pub mod button;
pub mod button_group;
pub mod checkbox;
pub mod clipboard;
pub mod context_menu;
pub mod divider;
pub mod dock_area;
@@ -16,34 +32,14 @@ pub mod modal;
pub mod notification;
pub mod popover;
pub mod popup_menu;
pub mod progress;
pub mod radio;
pub mod resizable;
pub mod scroll;
pub mod skeleton;
pub mod switch;
pub mod tab;
pub mod text;
pub mod theme;
pub mod tooltip;
pub use crate::Disableable;
pub use event::InteractiveElementExt;
pub use focusable::FocusableCycle;
pub use icon::*;
pub use root::{ContextModal, Root};
pub use styled::*;
pub use title_bar::*;
pub use window_border::{window_border, WindowBorder};
mod event;
mod focusable;
mod icon;
mod root;
mod styled;
mod title_bar;
mod window_border;
/// Initialize the UI module.
///
/// This must be called before using any of the UI components.

View File

@@ -1,9 +1,5 @@
use crate::{
input::{InputEvent, TextInput},
scroll::{Scrollbar, ScrollbarState},
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, Icon, IconName, Size,
};
use std::{cell::Cell, rc::Rc, time::Duration};
use gpui::{
actions, div, prelude::FluentBuilder, px, uniform_list, AnyElement, App, AppContext, Context,
Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length,
@@ -11,7 +7,13 @@ use gpui::{
Subscription, Task, UniformListScrollHandle, Window,
};
use smol::Timer;
use std::{cell::Cell, rc::Rc, time::Duration};
use theme::ActiveTheme;
use crate::{
input::{InputEvent, TextInput},
scroll::{Scrollbar, ScrollbarState},
v_flex, Icon, IconName, Size,
};
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
@@ -122,10 +124,7 @@ where
let query_input = cx.new(|cx| {
TextInput::new(window, cx)
.appearance(false)
.prefix(|_window, cx| {
Icon::new(IconName::Search)
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
})
.prefix(|_window, cx| Icon::new(IconName::Search).text_color(cx.theme().text_muted))
.placeholder("Search...")
.cleanable()
});
@@ -379,9 +378,9 @@ where
.left(px(0.))
.right(px(0.))
.bottom(px(0.))
.bg(cx.theme().accent.step(cx, ColorScaleStep::SIX))
.bg(cx.theme().element_background)
.border_1()
.border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)),
.border_color(cx.theme().border_selected),
)
})
})
@@ -394,7 +393,7 @@ where
.right(px(0.))
.bottom(px(0.))
.border_1()
.border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)),
.border_color(cx.theme().element_active),
)
})
.on_mouse_down(
@@ -471,7 +470,7 @@ where
_ => this.py_1().px_2(),
})
.border_b_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::THREE))
.border_color(cx.theme().border)
.child(input),
)
})

View File

@@ -1,14 +1,12 @@
use crate::{
h_flex,
theme::{scale::ColorScaleStep, ActiveTheme},
Disableable, Icon, IconName, Selectable, Sizable as _,
};
use gpui::{
div, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, Div, ElementId,
InteractiveElement, IntoElement, MouseButton, MouseMoveEvent, ParentElement, RenderOnce,
Stateful, StatefulInteractiveElement as _, Styled, Window,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable as _};
type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>;
type OnMouseEnter = Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>;
@@ -132,7 +130,7 @@ impl RenderOnce for ListItem {
let is_active = self.selected || self.confirmed;
self.base
.text_color(cx.theme().base.step(cx, ColorScaleStep::TWELVE))
.text_color(cx.theme().text_muted)
.relative()
.items_center()
.justify_between()
@@ -147,11 +145,9 @@ impl RenderOnce for ListItem {
this
}
})
.when(is_active, |this| {
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
})
.when(is_active, |this| this.bg(cx.theme().element_active))
.when(!is_active && !self.disabled, |this| {
this.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::TWO)))
this.hover(|this| this.bg(cx.theme().surface_background))
})
// Mouse enter
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
@@ -169,16 +165,15 @@ impl RenderOnce for ListItem {
.gap_x_1()
.child(div().w_full().children(self.children))
.when_some(self.check_icon, |this, icon| {
this.child(div().w_5().items_center().justify_center().when(
self.confirmed,
|this| {
this.child(
icon.small().text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
),
div()
.w_5()
.items_center()
.justify_center()
.when(self.confirmed, |this| {
this.child(icon.small().text_color(cx.theme().text_muted))
}),
)
},
))
}),
)
.when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))

View File

@@ -6,11 +6,11 @@ use gpui::{
IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString,
Styled, Window,
};
use theme::ActiveTheme;
use crate::{
animation::cubic_bezier,
button::{Button, ButtonCustomVariant, ButtonVariants as _},
theme::{scale::ColorScaleStep, ActiveTheme as _},
v_flex, ContextModal, IconName, StyledExt,
};
@@ -47,7 +47,7 @@ impl Modal {
let base = v_flex()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::SIX))
.border_color(cx.theme().border)
.rounded_xl()
.shadow_md();
@@ -168,9 +168,7 @@ impl RenderOnce for Modal {
.occlude()
.w(view_size.width)
.h(view_size.height)
.when(self.overlay, |this| {
this.bg(cx.theme().base.step_alpha(cx, ColorScaleStep::TWO))
})
.when(self.overlay, |this| this.bg(cx.theme().overlay))
.when(self.keyboard, |this| {
this.on_mouse_down(MouseButton::Left, {
let on_close = self.on_close.clone();
@@ -201,7 +199,7 @@ impl RenderOnce for Modal {
.items_center()
.font_semibold()
.border_b_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::SIX))
.border_color(cx.theme().border)
.line_height(relative(1.))
.child(title),
)
@@ -217,13 +215,10 @@ impl RenderOnce for Modal {
.right_2()
.custom(
ButtonCustomVariant::new(window, cx)
.foreground(
cx.theme().base.step(cx, ColorScaleStep::NINE),
)
.color(cx.theme().transparent)
.hover(cx.theme().transparent)
.active(cx.theme().transparent)
.border(cx.theme().transparent),
.foreground(cx.theme().icon_muted)
.color(cx.theme().ghost_element_background)
.hover(cx.theme().ghost_element_background)
.active(cx.theme().ghost_element_background),
)
.on_click(
move |_, window, cx| {

View File

@@ -1,21 +1,3 @@
use crate::{
animation::cubic_bezier,
button::{Button, ButtonVariants as _},
h_flex,
theme::{
colors::{blue, green, red, yellow},
scale::ColorScaleStep,
ActiveTheme as _,
},
v_flex, Icon, IconName, Sizable as _, StyledExt,
};
use gpui::{
div, prelude::FluentBuilder, px, Animation, AnimationExt, App, AppContext, ClickEvent, Context,
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
Window,
};
use smol::Timer;
use std::{
any::TypeId,
collections::{HashMap, VecDeque},
@@ -23,6 +5,21 @@ use std::{
time::Duration,
};
use gpui::{
blue, div, green, prelude::FluentBuilder, px, red, yellow, Animation, AnimationExt, App,
AppContext, ClickEvent, Context, DismissEvent, ElementId, Entity, EventEmitter,
InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Window,
};
use smol::Timer;
use theme::ActiveTheme;
use crate::{
animation::cubic_bezier,
button::{Button, ButtonVariants as _},
h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt,
};
pub enum NotificationType {
Info,
Success,
@@ -57,7 +54,7 @@ pub struct Notification {
///
/// None means the notification will be added to the end of the list.
id: NotificationId,
type_: NotificationType,
kind: NotificationType,
title: Option<SharedString>,
message: SharedString,
icon: Option<Icon>,
@@ -110,7 +107,7 @@ impl Notification {
id: id.into(),
title: None,
message: message.into(),
type_: NotificationType::Info,
kind: NotificationType::Info,
icon: None,
autohide: true,
on_click: None,
@@ -169,7 +166,7 @@ impl Notification {
/// Set the type of the notification, default is NotificationType::Info.
pub fn with_type(mut self, type_: NotificationType) -> Self {
self.type_ = type_;
self.kind = type_;
self
}
@@ -217,16 +214,13 @@ impl Render for Notification {
let closing = self.closing;
let icon = match self.icon.clone() {
Some(icon) => icon,
None => match self.type_ {
NotificationType::Info => {
Icon::new(IconName::Info).text_color(blue().step(cx, ColorScaleStep::NINE))
None => match self.kind {
NotificationType::Info => Icon::new(IconName::Info).text_color(blue()),
NotificationType::Error => Icon::new(IconName::CloseCircle).text_color(red()),
NotificationType::Success => Icon::new(IconName::CheckCircle).text_color(green()),
NotificationType::Warning => {
Icon::new(IconName::TriangleAlert).text_color(yellow())
}
NotificationType::Error => Icon::new(IconName::CloseCircle)
.text_color(red().step(cx, ColorScaleStep::NINE)),
NotificationType::Success => Icon::new(IconName::CheckCircle)
.text_color(green().step(cx, ColorScaleStep::NINE)),
NotificationType::Warning => Icon::new(IconName::TriangleAlert)
.text_color(yellow().step(cx, ColorScaleStep::NINE)),
},
};
@@ -237,9 +231,9 @@ impl Render for Notification {
.relative()
.w_72()
.border_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
.bg(cx.theme().background)
.rounded(px(cx.theme().radius))
.border_color(cx.theme().border)
.bg(cx.theme().surface_background)
.rounded(cx.theme().radius)
.shadow_md()
.p_2()
.gap_3()

View File

@@ -1,12 +1,5 @@
use crate::{
button::Button,
h_flex,
list::ListItem,
popover::Popover,
scroll::{Scrollbar, ScrollbarState},
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt,
};
use std::{cell::Cell, ops::Deref, rc::Rc};
use gpui::{
actions, anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App,
AppContext, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle,
@@ -14,7 +7,16 @@ use gpui::{
Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription,
WeakEntity, Window,
};
use std::{cell::Cell, ops::Deref, rc::Rc};
use theme::ActiveTheme;
use crate::{
button::Button,
h_flex,
list::ListItem,
popover::Popover,
scroll::{Scrollbar, ScrollbarState},
v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt,
};
actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]);
@@ -456,9 +458,7 @@ impl PopupMenu {
) -> Option<impl IntoElement> {
if let Some(action) = action {
if let Some(keybinding) = window.bindings_for_action(action.deref()).first() {
let el = div()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.children(
let el = div().text_color(cx.theme().text_muted).children(
keybinding
.keystrokes()
.iter()
@@ -590,10 +590,7 @@ impl Render for PopupMenu {
.h(px(1.))
.mx_neg_1()
.my_0p5()
.bg(cx
.theme()
.base
.step(cx, ColorScaleStep::TWO)),
.bg(cx.theme().border_variant),
)
}
PopupMenuItem::ElementItem { render, .. } => this

View File

@@ -1,62 +0,0 @@
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
use gpui::{
div, prelude::FluentBuilder, px, relative, App, IntoElement, ParentElement, RenderOnce, Styled,
Window,
};
/// A Progress bar element.
#[derive(IntoElement)]
pub struct Progress {
value: f32,
height: f32,
}
impl Progress {
pub fn new() -> Self {
Progress {
value: Default::default(),
height: 8.,
}
}
pub fn value(mut self, value: f32) -> Self {
self.value = value;
self
}
}
impl Default for Progress {
fn default() -> Self {
Self::new()
}
}
impl RenderOnce for Progress {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let rounded = px(self.height / 2.);
let relative_w = relative(match self.value {
v if v < 0. => 0.,
v if v > 100. => 1.,
v => v / 100.,
});
div()
.relative()
.h(px(self.height))
.rounded(rounded)
.bg(cx.theme().accent.step(cx, ColorScaleStep::THREE))
.child(
div()
.absolute()
.top_0()
.left_0()
.h_full()
.w(relative_w)
.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
.map(|this| match self.value {
v if v >= 100. => this.rounded(rounded),
_ => this.rounded_l(rounded),
}),
)
}
}

View File

@@ -1,111 +0,0 @@
use crate::{
h_flex,
theme::{scale::ColorScaleStep, ActiveTheme},
IconName,
};
use gpui::{
div, prelude::FluentBuilder, relative, svg, App, ElementId, InteractiveElement, IntoElement,
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
};
type OnClick = Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>;
/// A Radio element.
///
/// This is not included the Radio group implementation, you can manage the group by yourself.
#[derive(IntoElement)]
pub struct Radio {
id: ElementId,
label: Option<SharedString>,
checked: bool,
disabled: bool,
on_click: OnClick,
}
impl Radio {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
label: None,
checked: false,
disabled: false,
on_click: None,
}
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Box::new(handler));
self
}
}
impl RenderOnce for Radio {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let color = if self.disabled {
cx.theme().accent.step(cx, ColorScaleStep::FIVE)
} else {
cx.theme().accent.step(cx, ColorScaleStep::NINE)
};
h_flex()
.id(self.id)
.gap_x_2()
.items_center()
.line_height(relative(1.))
.child(
div()
.relative()
.size_4()
.flex_shrink_0()
.rounded_full()
.border_1()
.border_color(color)
.when(self.checked, |this| this.bg(color))
.child(
svg()
.absolute()
.top_px()
.left_px()
.size_3()
.text_color(color)
.map(|this| match self.checked {
true => this.path(IconName::Check.path()),
false => this,
}),
),
)
.when_some(self.label, |this, label| {
this.child(
div()
.size_full()
.overflow_x_hidden()
.text_ellipsis()
.line_height(relative(1.))
.child(label),
)
})
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| {
this.on_click(move |_event, window, cx| {
on_click(&!self.checked, window, cx);
})
},
)
}
}

View File

@@ -1,12 +1,11 @@
use crate::{
theme::{scale::ColorScaleStep, ActiveTheme as _},
AxisExt as _,
};
use gpui::{
div, prelude::FluentBuilder as _, px, App, Axis, Div, ElementId, InteractiveElement,
IntoElement, ParentElement as _, Pixels, RenderOnce, Stateful, StatefulInteractiveElement,
Styled as _, Window,
};
use theme::ActiveTheme;
use crate::AxisExt as _;
pub(crate) const HANDLE_PADDING: Pixels = px(8.);
pub(crate) const HANDLE_SIZE: Pixels = px(2.);
@@ -65,7 +64,7 @@ impl RenderOnce for ResizeHandle {
.child(
div()
.rounded_full()
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::SIX)))
.hover(|this| this.bg(cx.theme().border_variant))
.when(self.axis.is_horizontal(), |this| {
this.h_full().w(HANDLE_SIZE)
})

View File

@@ -1,14 +1,16 @@
use crate::{
modal::Modal,
notification::{Notification, NotificationList},
theme::{scale::ColorScaleStep, ActiveTheme},
window_border,
};
use std::rc::Rc;
use gpui::{
div, AnyView, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
ParentElement as _, Render, Styled, Window,
};
use std::rc::Rc;
use theme::ActiveTheme;
use crate::{
modal::Modal,
notification::{Notification, NotificationList},
window_border,
};
/// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality.
pub trait ContextModal: Sized {
@@ -230,8 +232,8 @@ impl Render for Root {
.relative()
.size_full()
.font_family(".SystemUIFont")
.bg(cx.theme().base.step(cx, ColorScaleStep::ONE))
.text_color(cx.theme().base.step(cx, ColorScaleStep::TWELVE))
.bg(cx.theme().background)
.text_color(cx.theme().text)
.child(self.view.clone()),
)
}

View File

@@ -1,38 +1,18 @@
use gpui::{
fill, point, px, relative, App, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element,
EntityId, Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, UniformListScrollHandle, Window,
};
use serde::{Deserialize, Serialize};
use std::{
cell::Cell,
rc::Rc,
time::{Duration, Instant},
};
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
/// Scrollbar show mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
pub enum ScrollbarShow {
#[default]
Scrolling,
Hover,
Always,
}
impl ScrollbarShow {
fn is_hover(&self) -> bool {
matches!(self, Self::Hover)
}
fn is_always(&self) -> bool {
matches!(self, Self::Always)
}
}
use gpui::{
fill, point, px, relative, App, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element,
EntityId, Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, UniformListScrollHandle, Window,
};
use theme::ActiveTheme;
const WIDTH: Pixels = px(12.);
const BORDER_WIDTH: Pixels = px(0.);
pub(crate) const WIDTH: Pixels = px(12.);
const MIN_THUMB_SIZE: f32 = 80.;
const THUMB_RADIUS: Pixels = Pixels(4.0);
const THUMB_INSET: Pixels = Pixels(3.);
@@ -336,9 +316,9 @@ impl Scrollbar {
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
(
cx.theme().scrollbar_thumb_hover,
cx.theme().scrollbar,
cx.theme().base.step(cx, ColorScaleStep::SEVEN),
cx.theme().scrollbar_thumb_hover_background,
cx.theme().scrollbar_thumb_background,
cx.theme().scrollbar_thumb_border,
THUMB_INSET - px(1.),
THUMB_RADIUS,
)
@@ -346,24 +326,20 @@ impl Scrollbar {
fn style_for_hovered_thumb(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
(
cx.theme().scrollbar_thumb_hover,
cx.theme().scrollbar,
cx.theme().base.step(cx, ColorScaleStep::SIX),
cx.theme().scrollbar_thumb_hover_background,
cx.theme().scrollbar_thumb_background,
cx.theme().scrollbar_thumb_border,
THUMB_INSET - px(1.),
THUMB_RADIUS,
)
}
fn style_for_hovered_bar(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
let (inset, radius) = if cx.theme().scrollbar_show.is_hover() {
(THUMB_INSET, THUMB_RADIUS - px(1.))
} else {
(THUMB_INSET - px(1.), THUMB_RADIUS)
};
let (inset, radius) = (THUMB_INSET - px(1.), THUMB_RADIUS);
(
cx.theme().scrollbar_thumb,
cx.theme().scrollbar,
cx.theme().scrollbar_thumb_background,
cx.theme().scrollbar_thumb_border,
gpui::transparent_black(),
inset,
radius,
@@ -514,22 +490,12 @@ impl Element for Scrollbar {
};
let state = self.state.clone();
let is_always_to_show = cx.theme().scrollbar_show.is_always();
let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
let (thumb_bg, bar_bg, bar_border, inset, radius) =
if state.get().dragged_axis == Some(axis) {
Self::style_for_active(cx)
} else if (is_hover_to_show || is_always_to_show)
&& (is_hovered_on_bar || is_hovered_on_thumb)
{
if is_hovered_on_thumb {
Self::style_for_hovered_thumb(cx)
} else {
Self::style_for_hovered_bar(cx)
}
} else {
let mut idle_state = Self::style_for_idle(cx);
// Delay 2s to fade out the scrollbar thumb (in 1s)
@@ -545,11 +511,12 @@ impl Element for Scrollbar {
};
} else {
if elapsed < FADE_OUT_DELAY {
idle_state.0 = cx.theme().scrollbar_thumb;
idle_state.0 = cx.theme().scrollbar_thumb_background;
} else {
// opacity = 1 - (x - 2)^10
let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
idle_state.0 = cx.theme().scrollbar_thumb.opacity(opacity);
idle_state.0 =
cx.theme().scrollbar_thumb_background.opacity(opacity);
};
window.request_animation_frame();
@@ -626,11 +593,10 @@ impl Element for Scrollbar {
_: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
_cx: &mut App,
) {
let hitbox_bounds = prepaint.hitbox.bounds;
let is_visible = self.state.get().is_scrollbar_visible();
let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
// Update last_scroll_time when offset is changed.
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
@@ -711,7 +677,7 @@ impl Element for Scrollbar {
let safe_range = (-scroll_area_size + container_size)..px(0.);
if is_hover_to_show || is_visible {
if is_visible {
window.on_mouse_event({
let state = self.state.clone();
let view_id = self.view_id;
@@ -770,7 +736,7 @@ impl Element for Scrollbar {
let mut notify = false;
// When is hover to show mode or it was visible,
// we need to update the hovered state and increase the last_scroll_time.
let need_hover_to_update = is_hover_to_show || is_visible;
let need_hover_to_update = is_visible;
// Update hovered state for scrollbar
if bounds.contains(&event.position) && need_hover_to_update {
state.set(state.get().with_hovered(Some(axis)));

View File

@@ -1,9 +1,10 @@
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
use std::time::Duration;
use gpui::{
bounce, div, ease_in_out, Animation, AnimationExt, Div, IntoElement, ParentElement as _,
RenderOnce, Styled,
};
use std::time::Duration;
use theme::ActiveTheme;
#[derive(IntoElement)]
pub struct Skeleton {
@@ -34,7 +35,7 @@ impl RenderOnce for Skeleton {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
div().child(
self.base
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.bg(cx.theme().ghost_element_disabled)
.with_animation(
"skeleton",
Animation::new(Duration::from_secs(2))

View File

@@ -1,10 +1,10 @@
use crate::{
scroll::{Scrollable, ScrollbarAxis},
theme::{scale::ColorScaleStep, ActiveTheme},
};
use std::fmt::{self, Display, Formatter};
use gpui::{div, px, App, Axis, Div, Element, ElementId, EntityId, Pixels, Styled, Window};
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
use theme::ActiveTheme;
use crate::scroll::{Scrollable, ScrollbarAxis};
/// Returns a `Div` as horizontal flex layout.
pub fn h_flex() -> Div {
@@ -39,7 +39,7 @@ pub trait StyledExt: Styled + Sized {
/// Render a border with a width of 1px, color ring color
fn outline(self, _window: &Window, cx: &App) -> Self {
self.border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE))
self.border_color(cx.theme().ring)
}
/// Wraps the element in a ScrollView.
@@ -66,7 +66,7 @@ pub trait StyledExt: Styled + Sized {
fn popover_style(self, cx: &mut App) -> Self {
self.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::SIX))
.border_color(cx.theme().border)
.shadow_lg()
.rounded_lg()
}

View File

@@ -1,14 +1,13 @@
use crate::{
h_flex,
theme::{scale::ColorScaleStep, ActiveTheme},
Disableable, Side, Sizable, Size,
};
use std::{cell::RefCell, rc::Rc, time::Duration};
use gpui::{
div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, App, Element,
ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _,
SharedString, Styled as _, Window,
};
use std::{cell::RefCell, rc::Rc, time::Duration};
use theme::ActiveTheme;
use crate::{h_flex, Disableable, Side, Sizable, Size};
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
@@ -110,11 +109,8 @@ impl Element for Switch {
let on_click = self.on_click.clone();
let (bg, toggle_bg) = match self.checked {
true => (
theme.accent.step(cx, ColorScaleStep::NINE),
theme.background,
),
false => (theme.base.step(cx, ColorScaleStep::THREE), theme.background),
true => (theme.icon_accent, theme.background),
false => (theme.element_background, theme.background),
};
let (bg, toggle_bg) = match self.disabled {
@@ -126,10 +122,12 @@ impl Element for Switch {
Size::XSmall | Size::Small => (px(28.), px(16.)),
_ => (px(36.), px(20.)),
};
let bar_width = match self.size {
Size::XSmall | Size::Small => px(12.),
_ => px(16.),
};
let inset = px(2.);
let mut element = div()
@@ -150,7 +148,7 @@ impl Element for Switch {
.flex()
.items_center()
.border(inset)
.border_color(theme.transparent)
.border_color(theme.border_transparent)
.bg(bg)
.when(!self.disabled, |this| this.cursor_pointer())
.child(

View File

@@ -1,8 +1,10 @@
use crate::theme::scale::ColorScaleStep;
use crate::theme::ActiveTheme;
use gpui::{
div, prelude::FluentBuilder, px, AnyElement, App, Div, ElementId, InteractiveElement,
IntoElement, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window,
};
use theme::ActiveTheme;
use crate::Selectable;
use gpui::prelude::FluentBuilder;
use gpui::*;
pub mod tab_bar;
@@ -80,25 +82,24 @@ impl RenderOnce for Tab {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (text_color, bg_color, hover_bg_color) = match (self.selected, self.disabled) {
(true, false) => (
cx.theme().base.step(cx, ColorScaleStep::TWELVE),
cx.theme().base.step(cx, ColorScaleStep::FIVE),
cx.theme().base.step(cx, ColorScaleStep::FOUR),
cx.theme().text,
cx.theme().tab_active_background,
cx.theme().tab_hover_background,
),
(false, false) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().transparent,
cx.theme().base.step(cx, ColorScaleStep::FOUR),
cx.theme().text_muted,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
),
// disabled
(true, true) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().transparent,
cx.theme().base.step(cx, ColorScaleStep::FOUR),
cx.theme().text_muted,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
),
(false, true) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().transparent,
cx.theme().base.step(cx, ColorScaleStep::FOUR),
cx.theme().text_muted,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
),
};
@@ -115,7 +116,7 @@ impl RenderOnce for Tab {
.text_ellipsis()
.text_color(text_color)
.bg(bg_color)
.rounded(px(cx.theme().radius))
.rounded(cx.theme().radius)
.hover(|this| this.bg(hover_bg_color))
.when_some(self.prefix, |this, prefix| {
this.child(prefix).text_color(text_color)

View File

@@ -8,8 +8,7 @@ use nostr_sdk::prelude::*;
use once_cell::sync::Lazy;
use regex::Regex;
use std::{collections::HashMap, ops::Range, sync::Arc};
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
use theme::ActiveTheme;
static NOSTR_URI_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"nostr:(npub|note|nprofile|nevent|naddr)[a-zA-Z0-9]+").unwrap());
@@ -78,7 +77,7 @@ impl RichText {
}
pub fn element(&self, id: ElementId, window: &mut Window, cx: &App) -> AnyElement {
let link_color = cx.theme().accent.step(cx, ColorScaleStep::ELEVEN);
let link_color = cx.theme().text_accent;
InteractiveText::new(
id,

View File

@@ -1,166 +0,0 @@
use crate::scroll::ScrollbarShow;
use colors::{default_color_scales, hsl};
use gpui::{App, Global, Hsla, SharedString, Window, WindowAppearance};
use scale::ColorScaleSet;
use std::ops::{Deref, DerefMut};
pub mod colors;
pub mod scale;
pub fn init(cx: &mut App) {
Theme::sync_system_appearance(None, cx)
}
pub trait ActiveTheme {
fn theme(&self) -> &Theme;
}
impl ActiveTheme for App {
#[inline]
fn theme(&self) -> &Theme {
Theme::global(self)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemColors {
pub background: Hsla,
pub transparent: Hsla,
pub scrollbar: Hsla,
pub scrollbar_thumb: Hsla,
pub scrollbar_thumb_hover: Hsla,
pub window_border: Hsla,
pub danger: Hsla,
}
impl SystemColors {
pub fn light() -> Self {
Self {
background: hsl(0.0, 0.0, 100.),
transparent: Hsla::transparent_black(),
window_border: hsl(240.0, 5.9, 78.0),
scrollbar: hsl(0., 0., 97.).opacity(0.75),
scrollbar_thumb: hsl(0., 0., 69.).opacity(0.9),
scrollbar_thumb_hover: hsl(0., 0., 59.),
danger: hsl(0.0, 84.2, 60.2),
}
}
pub fn dark() -> Self {
Self {
background: hsl(0.0, 0.0, 8.0),
transparent: Hsla::transparent_black(),
window_border: hsl(240.0, 3.7, 28.0),
scrollbar: hsl(240., 1., 15.).opacity(0.75),
scrollbar_thumb: hsl(0., 0., 48.).opacity(0.9),
scrollbar_thumb_hover: hsl(0., 0., 68.),
danger: hsl(0.0, 62.8, 30.6),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq)]
pub enum Appearance {
#[default]
Light,
Dark,
}
impl Appearance {
pub fn is_dark(&self) -> bool {
matches!(self, Self::Dark)
}
}
pub struct Theme {
colors: SystemColors,
/// Base colors.
pub base: ColorScaleSet,
/// Accent colors.
pub accent: ColorScaleSet,
/// Window appearances.
pub appearance: Appearance,
pub font_family: SharedString,
pub font_size: f32,
pub radius: f32,
pub shadow: bool,
/// Show the scrollbar mode, default: Scrolling
pub scrollbar_show: ScrollbarShow,
}
impl Deref for Theme {
type Target = SystemColors;
fn deref(&self) -> &Self::Target {
&self.colors
}
}
impl DerefMut for Theme {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.colors
}
}
impl Global for Theme {}
impl Theme {
/// Returns the global theme reference
pub fn global(cx: &App) -> &Theme {
cx.global::<Theme>()
}
/// Returns the global theme mutable reference
pub fn global_mut(cx: &mut App) -> &mut Theme {
cx.global_mut::<Theme>()
}
/// Sync the theme with the system appearance
pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
match cx.window_appearance() {
WindowAppearance::Dark | WindowAppearance::VibrantDark => {
Self::change(Appearance::Dark, window, cx)
}
WindowAppearance::Light | WindowAppearance::VibrantLight => {
Self::change(Appearance::Light, window, cx)
}
}
}
pub fn change(mode: Appearance, window: Option<&mut Window>, cx: &mut App) {
let theme = Theme::new(mode);
cx.set_global(theme);
if let Some(window) = window {
window.refresh();
}
}
}
impl Theme {
fn new(appearance: Appearance) -> Self {
let color_scales = default_color_scales();
let colors = match appearance {
Appearance::Light => SystemColors::light(),
Appearance::Dark => SystemColors::dark(),
};
Theme {
base: color_scales.gray,
accent: color_scales.yellow,
font_size: 15.0,
font_family: if cfg!(target_os = "macos") {
".SystemUIFont".into()
} else if cfg!(target_os = "windows") {
"Segoe UI".into()
} else {
"FreeMono".into()
},
radius: 5.0,
shadow: false,
scrollbar_show: ScrollbarShow::default(),
appearance,
colors,
}
}
}

View File

@@ -1,15 +1,20 @@
use crate::{h_flex, theme::ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _};
use std::rc::Rc;
use gpui::{
black, div, prelude::FluentBuilder as _, px, relative, white, AnyElement, App, ClickEvent, Div,
Element, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
RenderOnce, Rgba, Stateful, StatefulInteractiveElement as _, Style, Styled, Window,
};
use std::rc::Rc;
use theme::ActiveTheme;
use crate::{h_flex, Icon, IconName, InteractiveElementExt as _, Sizable as _};
const HEIGHT: Pixels = px(34.);
const TITLE_BAR_HEIGHT: Pixels = px(34.);
#[cfg(target_os = "macos")]
const TITLE_BAR_LEFT_PADDING: Pixels = px(80.);
#[cfg(not(target_os = "macos"))]
const TITLE_BAR_LEFT_PADDING: Pixels = px(12.);
@@ -105,7 +110,7 @@ impl Control {
}
fn fg(&self, _window: &Window, cx: &App) -> Hsla {
if cx.theme().appearance.is_dark() {
if cx.theme().mode.is_dark() {
white()
} else {
black()
@@ -113,7 +118,7 @@ impl Control {
}
fn hover_fg(&self, _window: &Window, cx: &App) -> Hsla {
if self.is_close() || cx.theme().appearance.is_dark() {
if self.is_close() || cx.theme().mode.is_dark() {
white()
} else {
black()
@@ -128,7 +133,7 @@ impl Control {
b: 32.0 / 255.0,
a: 1.0,
}
} else if cx.theme().appearance.is_dark() {
} else if cx.theme().mode.is_dark() {
Rgba {
r: 0.9,
g: 0.9,
@@ -247,7 +252,7 @@ impl RenderOnce for TitleBar {
.items_center()
.justify_between()
.h(HEIGHT)
.bg(cx.theme().transparent)
.bg(cx.theme().title_bar)
.when(window.is_fullscreen(), |this| this.pl(px(12.)))
.on_double_click(|_, window, _cx| window.zoom_window())
.child(

View File

@@ -2,8 +2,7 @@ use gpui::{
div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render,
SharedString, Styled, Window,
};
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
use theme::ActiveTheme;
pub struct Tooltip {
text: SharedString,
@@ -18,18 +17,17 @@ impl Tooltip {
impl Render for Tooltip {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().child(
// Wrap in a child, to ensure the left margin is applied to the tooltip
div()
.font_family(".SystemUIFont")
.m_3()
.p_2()
.border_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::SIX))
.bg(cx.theme().base.step(cx, ColorScaleStep::TWO))
.border_color(cx.theme().border)
.bg(cx.theme().surface_background)
.shadow_lg()
.rounded_lg()
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_color(cx.theme().text_muted)
.line_height(relative(1.25))
.child(self.text.clone()),
)

View File

@@ -1,14 +1,16 @@
use crate::theme::ActiveTheme;
use gpui::{
canvas, div, point, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, CursorStyle,
Decorations, Edges, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement,
Pixels, Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
};
use theme::ActiveTheme;
pub(crate) const BORDER_SIZE: Pixels = Pixels(1.0);
pub(crate) const BORDER_RADIUS: Pixels = Pixels(0.0);
#[cfg(not(target_os = "linux"))]
pub(crate) const SHADOW_SIZE: Pixels = Pixels(0.0);
#[cfg(target_os = "linux")]
pub(crate) const SHADOW_SIZE: Pixels = Pixels(12.0);