A ui toolkit for building gpui apps
rust gpui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

start on elements

+462 -19
+120 -1
crates/gpuikit-theme/src/lib.rs
··· 1 1 //! A simple theme system for gpui-kit 2 2 3 - use gpui::{App, Global, Hsla, SharedString}; 3 + use gpui::{hsla, App, Global, Hsla, SharedString}; 4 4 use std::collections::HashMap; 5 5 use std::sync::Arc; 6 + 7 + pub fn init(cx: &mut App) { 8 + cx.set_global(GlobalTheme::default()); 9 + } 6 10 7 11 /// Available theme variants 8 12 #[derive(Debug, Clone, Copy, PartialEq, Eq)] ··· 37 41 38 42 /// Outline color for focus states 39 43 pub outline: Hsla, 44 + 45 + /// Muted foreground color for secondary text 46 + pub fg_muted: Hsla, 47 + 48 + /// Disabled foreground color 49 + pub fg_disabled: Hsla, 50 + 51 + /// Secondary surface color for nested cards/panels 52 + pub surface_secondary: Hsla, 53 + 54 + /// Tertiary surface color for deeply nested elements 55 + pub surface_tertiary: Hsla, 56 + 57 + /// Secondary border color for hover states 58 + pub border_secondary: Hsla, 59 + 60 + /// Subtle border color for minimal separation 61 + pub border_subtle: Hsla, 62 + 63 + /// Accent color for primary actions 64 + pub accent: Hsla, 65 + 66 + /// Accent background color 67 + pub accent_bg: Hsla, 68 + 69 + /// Accent background hover color 70 + pub accent_bg_hover: Hsla, 71 + 72 + /// Danger color for errors and destructive actions 73 + pub danger: Hsla, 74 + 75 + /// Selection color for text and items 76 + pub selection: Hsla, 77 + 78 + // UI element specific colors 79 + /// Button background color 80 + pub button_bg: Hsla, 81 + 82 + /// Button background hover color 83 + pub button_bg_hover: Hsla, 84 + 85 + /// Button background active/pressed color 86 + pub button_bg_active: Hsla, 87 + 88 + /// Button border color 89 + pub button_border: Hsla, 90 + 91 + /// Input background color 92 + pub input_bg: Hsla, 93 + 94 + /// Input border color 95 + pub input_border: Hsla, 96 + 97 + /// Input border hover color 98 + pub input_border_hover: Hsla, 99 + 100 + /// Input border focused color 101 + pub input_border_focused: Hsla, 40 102 } 41 103 42 104 impl Theme { ··· 50 112 surface: parse_hex("#3c3836"), 51 113 border: parse_hex("#504945"), 52 114 outline: parse_hex("#458588"), 115 + fg_muted: parse_hex("#a89984"), 116 + fg_disabled: parse_hex("#7c6f64"), 117 + surface_secondary: parse_hex("#504945"), 118 + surface_tertiary: parse_hex("#665c54"), 119 + border_secondary: parse_hex("#7c6f64"), 120 + border_subtle: parse_hex("#3c3836"), 121 + accent: parse_hex("#8ec07c"), 122 + accent_bg: hsla(104.0 / 360.0, 0.35, 0.63, 0.15), 123 + accent_bg_hover: hsla(104.0 / 360.0, 0.35, 0.63, 0.25), 124 + danger: parse_hex("#fb4934"), 125 + selection: hsla(55.0 / 360.0, 0.56, 0.64, 0.25), 126 + button_bg: parse_hex("#504945"), 127 + button_bg_hover: parse_hex("#665c54"), 128 + button_bg_active: parse_hex("#7c6f64"), 129 + button_border: parse_hex("#7c6f64"), 130 + input_bg: parse_hex("#3c3836"), 131 + input_border: parse_hex("#504945"), 132 + input_border_hover: parse_hex("#665c54"), 133 + input_border_focused: parse_hex("#8ec07c"), 53 134 } 54 135 } 55 136 ··· 63 144 surface: parse_hex("#ebdbb2"), 64 145 border: parse_hex("#d5c4a1"), 65 146 outline: parse_hex("#076678"), 147 + fg_muted: parse_hex("#665c54"), 148 + fg_disabled: parse_hex("#a89984"), 149 + surface_secondary: parse_hex("#d5c4a1"), 150 + surface_tertiary: parse_hex("#bdae93"), 151 + border_secondary: parse_hex("#a89984"), 152 + border_subtle: parse_hex("#ebdbb2"), 153 + accent: parse_hex("#427b58"), 154 + accent_bg: hsla(145.0 / 360.0, 0.30, 0.38, 0.10), 155 + accent_bg_hover: hsla(145.0 / 360.0, 0.30, 0.38, 0.15), 156 + danger: parse_hex("#cc241d"), 157 + selection: hsla(48.0 / 360.0, 0.87, 0.61, 0.15), 158 + button_bg: parse_hex("#ebdbb2"), 159 + button_bg_hover: parse_hex("#d5c4a1"), 160 + button_bg_active: parse_hex("#bdae93"), 161 + button_border: parse_hex("#a89984"), 162 + input_bg: parse_hex("#fbf1c7"), 163 + input_border: parse_hex("#d5c4a1"), 164 + input_border_hover: parse_hex("#bdae93"), 165 + input_border_focused: parse_hex("#427b58"), 66 166 } 67 167 } 68 168 ··· 247 347 surface: parse_hex("#111111"), 248 348 border: parse_hex("#222222"), 249 349 outline: parse_hex("#0066cc"), 350 + fg_muted: parse_hex("#888888"), 351 + fg_disabled: parse_hex("#555555"), 352 + surface_secondary: parse_hex("#1a1a1a"), 353 + surface_tertiary: parse_hex("#222222"), 354 + border_secondary: parse_hex("#333333"), 355 + border_subtle: parse_hex("#111111"), 356 + accent: parse_hex("#0066cc"), 357 + accent_bg: hsla(210.0 / 360.0, 1.0, 0.4, 0.15), 358 + accent_bg_hover: hsla(210.0 / 360.0, 1.0, 0.4, 0.25), 359 + danger: parse_hex("#cc0000"), 360 + selection: hsla(210.0 / 360.0, 1.0, 0.5, 0.25), 361 + button_bg: parse_hex("#222222"), 362 + button_bg_hover: parse_hex("#333333"), 363 + button_bg_active: parse_hex("#444444"), 364 + button_border: parse_hex("#333333"), 365 + input_bg: parse_hex("#111111"), 366 + input_border: parse_hex("#222222"), 367 + input_border_hover: parse_hex("#333333"), 368 + input_border_focused: parse_hex("#0066cc"), 250 369 }; 251 370 252 371 themes.add(custom);
-4
crates/gpuikit/Cargo.toml
··· 31 31 [dev-dependencies] 32 32 gpui = { workspace = true, features = ["test-support"] } 33 33 34 - [[example]] 35 - name = "file_dialog" 36 - path = "../../examples/file_dialog.rs" 37 - 38 34 [features] 39 35 default = [] 40 36
+2
crates/gpuikit/src/elements.rs
··· 1 + pub mod avatar; 2 + pub mod button;
+58
crates/gpuikit/src/elements/avatar.rs
··· 1 + use gpui::{ 2 + div, img, prelude::FluentBuilder, px, rems, AbsoluteLength, App, Hsla, ImageSource, Img, 3 + IntoElement, ParentElement, RenderOnce, Styled, Window, 4 + }; 5 + use gpuikit_theme::ActiveTheme; 6 + 7 + #[derive(IntoElement)] 8 + pub struct Avatar { 9 + image: Img, 10 + size: Option<AbsoluteLength>, 11 + border_color: Option<Hsla>, 12 + } 13 + 14 + impl Avatar { 15 + pub fn new(src: impl Into<ImageSource>) -> Self { 16 + Avatar { 17 + image: img(src), 18 + size: None, 19 + border_color: None, 20 + } 21 + } 22 + 23 + pub fn border_color(mut self, color: impl Into<Hsla>) -> Self { 24 + self.border_color = Some(color.into()); 25 + self 26 + } 27 + 28 + pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self { 29 + self.size = size.into().map(Into::into); 30 + self 31 + } 32 + } 33 + 34 + impl RenderOnce for Avatar { 35 + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { 36 + let border_width = if self.border_color.is_some() { 37 + px(2.) 38 + } else { 39 + px(0.) 40 + }; 41 + 42 + let image_size = self.size.unwrap_or_else(|| rems(1.).into()); 43 + let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.; 44 + 45 + div() 46 + .size(container_size) 47 + .rounded_full() 48 + .when_some(self.border_color, |this, color| { 49 + this.border(border_width).border_color(color) 50 + }) 51 + .child( 52 + self.image 53 + .size(image_size) 54 + .rounded_full() 55 + .bg(cx.theme().surface), 56 + ) 57 + } 58 + }
+95
crates/gpuikit/src/elements/button.rs
··· 1 + use gpui::{ 2 + div, rems, App, ClickEvent, ElementId, FontWeight, InteractiveElement, IntoElement, 3 + MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, 4 + Window, 5 + }; 6 + use gpuikit_theme::ActiveTheme; 7 + 8 + use crate::utils::element_manager::ElementManagerExt; 9 + 10 + pub fn button(cx: &App, label: impl Into<SharedString>) -> Button { 11 + let label = label.into(); 12 + let id = cx.next_id_named(label.clone()); 13 + Button::new(id, label) 14 + } 15 + 16 + #[derive(IntoElement)] 17 + pub struct Button { 18 + id: ElementId, 19 + label: SharedString, 20 + disabled: bool, 21 + handler: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, 22 + } 23 + 24 + impl Button { 25 + pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self { 26 + let id = id.into(); 27 + let label = label.into(); 28 + 29 + Button { 30 + id, 31 + label, 32 + disabled: false, 33 + handler: None, 34 + } 35 + } 36 + 37 + pub fn on_click( 38 + mut self, 39 + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, 40 + ) -> Self { 41 + self.handler = Some(Box::new(handler)); 42 + self 43 + } 44 + 45 + pub fn disabled(mut self, disabled: bool) -> Self { 46 + self.disabled = disabled; 47 + self 48 + } 49 + } 50 + 51 + impl RenderOnce for Button { 52 + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { 53 + let theme = cx.theme(); 54 + 55 + let mut button = div() 56 + .id(self.id) 57 + .h(rems(1.0)) 58 + .px(rems(0.125)) 59 + .gap(rems(0.125)) 60 + .flex() 61 + .flex_none() 62 + .items_center() 63 + .justify_center() 64 + .rounded(rems(0.125)) 65 + .text_xs() 66 + .font_weight(FontWeight::MEDIUM) 67 + .bg(cx.theme().button_bg) 68 + .text_color(theme.fg) 69 + .whitespace_nowrap(); 70 + 71 + if !self.disabled { 72 + button = button 73 + .hover(|div| div.bg(cx.theme().button_bg_hover)) 74 + .active(|div| div.bg(cx.theme().button_bg_active)) 75 + } else { 76 + button = button 77 + .opacity(0.65) 78 + .cursor_not_allowed() 79 + .text_color(theme.fg_muted) 80 + } 81 + 82 + if !self.disabled { 83 + if let Some(handler) = self.handler { 84 + button = button 85 + .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) 86 + .on_click(move |event, window, cx| { 87 + cx.stop_propagation(); 88 + handler(event, window, cx) 89 + }); 90 + } 91 + } 92 + 93 + button.child(self.label) 94 + } 95 + }
+11 -14
crates/gpuikit/src/lib.rs
··· 1 1 //! gpuikit 2 2 3 + use gpui::App; 3 4 pub use gpuikit_theme as theme; 4 5 6 + pub mod elements; 5 7 pub mod error; 6 8 pub mod fs; 7 9 pub mod layout; 8 10 pub mod resource; 11 + pub mod traits; 12 + pub mod utils; 9 13 10 - pub mod style { 11 - use gpuikit_theme::Theme; 12 - 13 - pub trait Themed { 14 - fn themed(self, theme: &Theme) -> Self; 15 - } 16 - 17 - // todo: is Themed useful? 18 - // 19 - // I could see most gpuikit components being something like: 20 - // 21 - // pub trait Component: IntoElement + Themed {} 22 - // 23 - // where Themed eventually gets more style helpers... 14 + /// Initialize gpuikit - this sets up & loads themes, sets up global state, etc. 15 + /// 16 + /// This must be called as soon as possible after your `gpui::Application` is created, 17 + /// as calling a gpuikit component before initialization will panic. 18 + pub fn init(cx: &mut App) { 19 + gpuikit_theme::init(cx); 20 + utils::element_manager::init(cx); 24 21 }
+3
crates/gpuikit/src/traits.rs
··· 1 + pub mod button; 2 + pub mod clickable; 3 + pub mod visual_focus;
+9
crates/gpuikit/src/traits/button.rs
··· 1 + use super::clickable::Clickable; 2 + 3 + /// A button is a clickable element that dispatches some 4 + /// handler when clicked. 5 + pub trait Button: Clickable { 6 + type Variant; 7 + 8 + fn variant(&self) -> Self::Variant; 9 + }
+10
crates/gpuikit/src/traits/clickable.rs
··· 1 + use gpui::{App, ClickEvent, Window}; 2 + 3 + /// Trait for elements that can be clicked 4 + pub trait Clickable { 5 + /// Check if this element is disabled 6 + fn disabled(&self) -> bool; 7 + 8 + /// Set the click handler 9 + fn on_click(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self; 10 + }
+14
crates/gpuikit/src/traits/visual_focus.rs
··· 1 + /// Indicates what kind of visual focus style an element should use 2 + /// 3 + /// - Ring: A focus ring is drawn around the element 4 + /// - Highlight: Element is highlighted (e.g. background color change) 5 + pub enum FocusStyle { 6 + Ring, 7 + Highlight, 8 + } 9 + 10 + // A trait for elements that can receive visual focus 11 + // usually denoted by a focus ring or highlight 12 + pub trait VisualFocus: gpui::Focusable { 13 + fn focus_style(&self) -> FocusStyle; 14 + }
+1
crates/gpuikit/src/utils.rs
··· 1 + pub mod element_manager;
+139
crates/gpuikit/src/utils/element_manager.rs
··· 1 + use gpui::{App, Global, SharedString}; 2 + use std::sync::atomic::{AtomicUsize, Ordering}; 3 + use std::sync::Arc; 4 + 5 + /// Powers gpuikit's syntactic sugar, allowing elements to not 6 + /// have to specify ids manually when created. 7 + #[derive(Debug, Clone)] 8 + pub struct ElementManager { 9 + next_id: Arc<AtomicUsize>, 10 + } 11 + 12 + impl ElementManager { 13 + pub fn new() -> Self { 14 + Self { 15 + next_id: Arc::new(AtomicUsize::new(0)), 16 + } 17 + } 18 + 19 + /// Get the next button ID, incrementing the counter 20 + pub fn id(&self) -> usize { 21 + self.next_id.fetch_add(1, Ordering::SeqCst) 22 + } 23 + 24 + // Note: this is intentionally not pub 25 + // we don't want to expose the current ID publicly 26 + // as it could get used multiple times leading to collisions 27 + #[allow(dead_code)] 28 + fn current(&self) -> usize { 29 + self.next_id.load(Ordering::SeqCst) 30 + } 31 + } 32 + 33 + impl Default for ElementManager { 34 + fn default() -> Self { 35 + Self::new() 36 + } 37 + } 38 + 39 + /// Global wrapper for ElementManager to make it accessible throughout the app 40 + #[derive(Debug, Clone)] 41 + pub struct GlobalElementManager(pub Arc<ElementManager>); 42 + 43 + impl GlobalElementManager { 44 + pub fn new() -> Self { 45 + Self(Arc::new(ElementManager::new())) 46 + } 47 + } 48 + 49 + impl Default for GlobalElementManager { 50 + fn default() -> Self { 51 + Self::new() 52 + } 53 + } 54 + 55 + impl std::ops::Deref for GlobalElementManager { 56 + type Target = Arc<ElementManager>; 57 + 58 + fn deref(&self) -> &Self::Target { 59 + &self.0 60 + } 61 + } 62 + 63 + impl std::ops::DerefMut for GlobalElementManager { 64 + fn deref_mut(&mut self) -> &mut Self::Target { 65 + &mut self.0 66 + } 67 + } 68 + 69 + impl Global for GlobalElementManager {} 70 + 71 + /// Extension trait for App to access the element manager 72 + pub trait ElementManagerExt { 73 + /// Get a reference to the global element manager 74 + fn element_manager(&self) -> &Arc<ElementManager>; 75 + 76 + /// Get the next button ID 77 + fn next_id(&self) -> usize; 78 + 79 + /// Get the next id as a `gpui::ElementId::NamedInteger` 80 + fn next_id_named(&self, name: impl Into<SharedString>) -> gpui::ElementId; 81 + } 82 + 83 + impl ElementManagerExt for App { 84 + fn element_manager(&self) -> &Arc<ElementManager> { 85 + &self.global::<GlobalElementManager>().0 86 + } 87 + 88 + fn next_id(&self) -> usize { 89 + self.element_manager().id() 90 + } 91 + 92 + fn next_id_named(&self, name: impl Into<SharedString>) -> gpui::ElementId { 93 + let name = name.into(); 94 + let id = self.next_id(); 95 + 96 + gpui::ElementId::named_usize(name, id) 97 + } 98 + } 99 + 100 + pub fn init(cx: &mut App) { 101 + cx.set_global(GlobalElementManager::new()); 102 + } 103 + 104 + #[cfg(test)] 105 + mod tests { 106 + use super::*; 107 + 108 + #[test] 109 + fn test_element_manager_next() { 110 + let manager = ElementManager::new(); 111 + 112 + assert_eq!(manager.id(), 0); 113 + assert_eq!(manager.id(), 1); 114 + assert_eq!(manager.id(), 2); 115 + } 116 + 117 + #[test] 118 + fn test_element_manager_thread_safe() { 119 + let manager = Arc::new(ElementManager::new()); 120 + let mut handles = vec![]; 121 + 122 + for _ in 0..10 { 123 + let manager_clone = Arc::clone(&manager); 124 + let handle = std::thread::spawn(move || { 125 + for _ in 0..100 { 126 + manager_clone.id(); 127 + } 128 + }); 129 + handles.push(handle); 130 + } 131 + 132 + for handle in handles { 133 + handle.join().unwrap(); 134 + } 135 + 136 + // Should have incremented 1000 times (10 threads * 100 increments each) 137 + assert_eq!(manager.current(), 1000); 138 + } 139 + }