scaffold
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "gpui-native-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "The GPUI components every application needs, one cargo feature each"
|
||||
|
||||
[dependencies]
|
||||
gpui = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["full"]
|
||||
full = [
|
||||
"button",
|
||||
"checkbox",
|
||||
"switch",
|
||||
"radio",
|
||||
"toggle",
|
||||
"link",
|
||||
"slider",
|
||||
"progress",
|
||||
"avatar",
|
||||
"input",
|
||||
"popover",
|
||||
"tooltip",
|
||||
"menu",
|
||||
"dialog",
|
||||
"sheet",
|
||||
"toast",
|
||||
]
|
||||
button = []
|
||||
checkbox = []
|
||||
switch = []
|
||||
radio = []
|
||||
toggle = []
|
||||
link = []
|
||||
slider = []
|
||||
progress = []
|
||||
avatar = []
|
||||
input = []
|
||||
popover = []
|
||||
tooltip = []
|
||||
menu = []
|
||||
dialog = []
|
||||
sheet = []
|
||||
toast = []
|
||||
@@ -0,0 +1,8 @@
|
||||
//! The components every application needs, one cargo feature each.
|
||||
//!
|
||||
//! Components land one per feature: `button`, `checkbox`, `input`, …
|
||||
//! `default-features = false` plus the features you want for a lean build;
|
||||
//! `full` (the default) for everything. Features gate whole modules and are
|
||||
//! additive only; each component adds its `#[cfg(feature = "…")] pub mod`
|
||||
//! declaration together with its file. Shared helpers promoted per the
|
||||
//! third-use rule live in the ungated `common` module once one exists.
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "gpui-native-icons"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "System icons for GPUI: semantic roles resolved per platform, with a bundled Lucide fallback"
|
||||
|
||||
[dependencies]
|
||||
gpui = { workspace = true }
|
||||
@@ -0,0 +1,9 @@
|
||||
# Licenses
|
||||
|
||||
## Bundled icons (`assets/*.svg`)
|
||||
|
||||
Lucide, ISC License, © Lucide Contributors. Fetched from https://github.com/lucide-icons/lucide (main) on 2026-10-28. These are the offline fallback glyphs, see https://lucide.dev/license.
|
||||
|
||||
## Platform glyph sets (not bundled)
|
||||
|
||||
SF Symbols (macOS) and Segoe Fluent Icons (Windows) are resolved through the operating system at runtime and are never extracted, modified, or redistributed. Freedesktop icons are loaded from the user's installed icon themes.
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 237 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m18 15-6-6-6 6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 342 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4" />
|
||||
<path d="M12 8h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -0,0 +1,166 @@
|
||||
//! System icons for GPUI: components and apps reference semantic roles,
|
||||
//! and the crate resolves each role to a glyph for the current platform.
|
||||
//!
|
||||
//! Today every role resolves to the bundled Lucide fallback. Platform
|
||||
//! resolvers land with their phases: SF Symbols (macOS), Segoe Fluent
|
||||
//! Icons (Windows), freedesktop icon themes (Linux/Omarchy).
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AssetSource, Hsla, IntoElement, ParentElement as _, RenderOnce, SharedString, Styled as _,
|
||||
Window, div, px, svg,
|
||||
};
|
||||
|
||||
/// The semantic glyphs UI components need. Grows one role at a time as
|
||||
/// components call for it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum IconRole {
|
||||
Check,
|
||||
Close,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
Plus,
|
||||
Minus,
|
||||
Search,
|
||||
Folder,
|
||||
Warning,
|
||||
Info,
|
||||
}
|
||||
|
||||
impl IconRole {
|
||||
pub fn all() -> &'static [Self] {
|
||||
&[
|
||||
Self::Check,
|
||||
Self::Close,
|
||||
Self::ChevronDown,
|
||||
Self::ChevronLeft,
|
||||
Self::ChevronRight,
|
||||
Self::ChevronUp,
|
||||
Self::Plus,
|
||||
Self::Minus,
|
||||
Self::Search,
|
||||
Self::Folder,
|
||||
Self::Warning,
|
||||
Self::Info,
|
||||
]
|
||||
}
|
||||
|
||||
fn asset_path(self) -> &'static str {
|
||||
match self {
|
||||
Self::Check => "icons/check.svg",
|
||||
Self::Close => "icons/x.svg",
|
||||
Self::ChevronDown => "icons/chevron-down.svg",
|
||||
Self::ChevronLeft => "icons/chevron-left.svg",
|
||||
Self::ChevronRight => "icons/chevron-right.svg",
|
||||
Self::ChevronUp => "icons/chevron-up.svg",
|
||||
Self::Plus => "icons/plus.svg",
|
||||
Self::Minus => "icons/minus.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
Self::Folder => "icons/folder.svg",
|
||||
Self::Warning => "icons/triangle-alert.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An icon element. Unpainted unless a color is set (gpui paints SVGs from
|
||||
/// the element's text color only).
|
||||
#[derive(IntoElement)]
|
||||
pub struct Icon {
|
||||
role: IconRole,
|
||||
size: f32,
|
||||
color: Option<Hsla>,
|
||||
}
|
||||
|
||||
/// Creates an icon for the role at the default size of 16px.
|
||||
pub fn icon(role: IconRole) -> Icon {
|
||||
Icon {
|
||||
role,
|
||||
size: 16.0,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn size(mut self, size: f32) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Hsla) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement {
|
||||
div().flex_none().size(px(self.size)).child(
|
||||
svg()
|
||||
.size_full()
|
||||
.path(self.role.asset_path())
|
||||
.when_some(self.color, |el, color| el.text_color(color)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The asset source serving this crate's bundled glyphs. Register it on the
|
||||
/// application: `application().with_assets(IconAssets)`.
|
||||
pub struct IconAssets;
|
||||
|
||||
fn asset_bytes(path: &str) -> Option<&'static [u8]> {
|
||||
match path {
|
||||
"icons/check.svg" => Some(include_bytes!("../assets/check.svg")),
|
||||
"icons/x.svg" => Some(include_bytes!("../assets/x.svg")),
|
||||
"icons/chevron-down.svg" => Some(include_bytes!("../assets/chevron-down.svg")),
|
||||
"icons/chevron-left.svg" => Some(include_bytes!("../assets/chevron-left.svg")),
|
||||
"icons/chevron-right.svg" => Some(include_bytes!("../assets/chevron-right.svg")),
|
||||
"icons/chevron-up.svg" => Some(include_bytes!("../assets/chevron-up.svg")),
|
||||
"icons/plus.svg" => Some(include_bytes!("../assets/plus.svg")),
|
||||
"icons/minus.svg" => Some(include_bytes!("../assets/minus.svg")),
|
||||
"icons/search.svg" => Some(include_bytes!("../assets/search.svg")),
|
||||
"icons/folder.svg" => Some(include_bytes!("../assets/folder.svg")),
|
||||
"icons/triangle-alert.svg" => Some(include_bytes!("../assets/triangle-alert.svg")),
|
||||
"icons/info.svg" => Some(include_bytes!("../assets/info.svg")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetSource for IconAssets {
|
||||
fn load(&self, path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
Ok(asset_bytes(path).map(std::borrow::Cow::Borrowed))
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> gpui::Result<Vec<SharedString>> {
|
||||
if path.is_empty() {
|
||||
Ok(IconRole::all()
|
||||
.iter()
|
||||
.map(|role| SharedString::from(role.asset_path()))
|
||||
.collect())
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_role_resolves_to_a_bundled_asset() {
|
||||
for role in IconRole::all() {
|
||||
assert!(
|
||||
asset_bytes(role.asset_path()).is_some(),
|
||||
"missing bundled asset for {role:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_assets_report_none() {
|
||||
assert_eq!(asset_bytes("icons/nope.svg"), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "gpui-native-theme"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Native theme engine: reads the OS theme and resolves it into semantic tokens"
|
||||
|
||||
[dependencies]
|
||||
gpui = { workspace = true }
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Semantic color roles. This module is the only place in the workspace
|
||||
//! allowed to hold color literals.
|
||||
|
||||
use gpui::{Hsla, rgb};
|
||||
|
||||
fn hex(hex: u32) -> Hsla {
|
||||
rgb(hex).into()
|
||||
}
|
||||
|
||||
/// The semantic color roles components paint from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ColorTokens {
|
||||
pub background: Hsla,
|
||||
pub foreground: Hsla,
|
||||
pub surface: Hsla,
|
||||
pub surface_foreground: Hsla,
|
||||
pub primary: Hsla,
|
||||
pub primary_foreground: Hsla,
|
||||
pub secondary: Hsla,
|
||||
pub secondary_foreground: Hsla,
|
||||
pub muted: Hsla,
|
||||
pub muted_foreground: Hsla,
|
||||
pub accent: Hsla,
|
||||
pub accent_foreground: Hsla,
|
||||
pub destructive: Hsla,
|
||||
pub destructive_foreground: Hsla,
|
||||
pub border: Hsla,
|
||||
pub input: Hsla,
|
||||
pub ring: Hsla,
|
||||
pub selection: Hsla,
|
||||
}
|
||||
|
||||
impl ColorTokens {
|
||||
/// Neutral light palette; the fallback when no system theme applies.
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
background: hex(0xffffff),
|
||||
foreground: hex(0x18181b),
|
||||
surface: hex(0xfafafa),
|
||||
surface_foreground: hex(0x18181b),
|
||||
primary: hex(0x2563eb),
|
||||
primary_foreground: hex(0xffffff),
|
||||
secondary: hex(0xf4f4f5),
|
||||
secondary_foreground: hex(0x18181b),
|
||||
muted: hex(0xf4f4f5),
|
||||
muted_foreground: hex(0x71717a),
|
||||
accent: hex(0x2563eb),
|
||||
accent_foreground: hex(0xffffff),
|
||||
destructive: hex(0xdc2626),
|
||||
destructive_foreground: hex(0xffffff),
|
||||
border: hex(0xe4e4e7),
|
||||
input: hex(0xe4e4e7),
|
||||
ring: hex(0x93c5fd),
|
||||
selection: hex(0xdbeafe),
|
||||
}
|
||||
}
|
||||
|
||||
/// Neutral dark palette; the fallback when no system theme applies.
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
background: hex(0x09090b),
|
||||
foreground: hex(0xfafafa),
|
||||
surface: hex(0x18181b),
|
||||
surface_foreground: hex(0xfafafa),
|
||||
primary: hex(0x3b82f6),
|
||||
primary_foreground: hex(0xffffff),
|
||||
secondary: hex(0x27272a),
|
||||
secondary_foreground: hex(0xfafafa),
|
||||
muted: hex(0x27272a),
|
||||
muted_foreground: hex(0xa1a1aa),
|
||||
accent: hex(0x3b82f6),
|
||||
accent_foreground: hex(0xffffff),
|
||||
destructive: hex(0xef4444),
|
||||
destructive_foreground: hex(0xffffff),
|
||||
border: hex(0x27272a),
|
||||
input: hex(0x27272a),
|
||||
ring: hex(0x1d4ed8),
|
||||
selection: hex(0x1e3a8a),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! The theme engine: reads the running system theme and resolves it into the
|
||||
//! semantic tokens every component paints from.
|
||||
//!
|
||||
//! Phase B replaces the bundled light/dark presets with platform adapters
|
||||
//! (`NSColor` / `UISettings` / portal / Omarchy) behind a `ThemeSource` seam.
|
||||
|
||||
mod colors;
|
||||
|
||||
pub use colors::ColorTokens;
|
||||
|
||||
use gpui::{App, Global, WindowAppearance};
|
||||
|
||||
/// Light or dark, resolved from the system appearance.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum ThemeVariant {
|
||||
#[default]
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl ThemeVariant {
|
||||
fn from_appearance(appearance: WindowAppearance) -> Self {
|
||||
match appearance {
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved theme, installed as the app-wide global.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Theme {
|
||||
pub variant: ThemeVariant,
|
||||
pub colors: ColorTokens,
|
||||
}
|
||||
|
||||
impl Global for Theme {}
|
||||
|
||||
impl Theme {
|
||||
/// Reads the theme global. Panics before `init`.
|
||||
pub fn global(cx: &App) -> &Self {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
pub fn for_variant(variant: ThemeVariant) -> Self {
|
||||
Self {
|
||||
variant,
|
||||
colors: match variant {
|
||||
ThemeVariant::Light => ColorTokens::light(),
|
||||
ThemeVariant::Dark => ColorTokens::dark(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs the theme global for the current system appearance.
|
||||
pub fn init(cx: &mut App) {
|
||||
let variant = ThemeVariant::from_appearance(cx.window_appearance());
|
||||
cx.set_global(Self::for_variant(variant));
|
||||
}
|
||||
|
||||
/// Re-installs the global when a window's appearance differs from it.
|
||||
pub fn sync_from_window(window: &gpui::Window, cx: &mut App) {
|
||||
let variant = ThemeVariant::from_appearance(window.appearance());
|
||||
if cx.global::<Self>().variant != variant {
|
||||
cx.set_global(Self::for_variant(variant));
|
||||
}
|
||||
}
|
||||
}
|
||||