feat: Add GPUI component library

Add core UI components for TaskWarrior GUI application:

Components:
- Input: Text input with autocomplete suggestions, keyboard navigation
- Button: Multiple variants (Primary, Secondary, Ghost, Danger, Success,
  Text)
- DropdownButton: Compound button with dropdown menu
- Icon: SVG icons with customizable sizes
- Label: Simple text display
- Panel: Container with optional title
- List: List container
- Modal: Modal dialog
- Divider: Visual separator

Infrastructure:
- Theme system with dark/light modes (Catppuccin colors)
- App module with window management
This commit is contained in:
Ignacio Perez
2025-12-24 19:21:04 -03:00
parent 8490fb03f5
commit 7c1824dcf7
14 changed files with 2198 additions and 87 deletions
+82
View File
@@ -0,0 +1,82 @@
use gpui::prelude::*;
use std::sync::Arc;
use crate::components::{
self,
input::{Input, Suggestion},
};
pub(crate) struct App {
input: gpui::Entity<Input>,
current_text: gpui::SharedString,
}
impl gpui::Render for App {
fn render(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
components::panel::Panel::new()
.child(self.input.clone())
.child(components::label::Label::new(format!(
"Text: {}",
&self.current_text
)))
}
}
impl App {
pub fn run() -> () {
let app = gpui::Application::new();
app.run(|app: &mut gpui::App| {
app.set_global(crate::theme::Theme::dark());
app.open_window(
gpui::WindowOptions::default(),
|_window: &mut gpui::Window, app: &mut gpui::App| {
app.new(|cx: &mut gpui::Context<'_, App>| {
let input = cx.new(|cx| {
Input::new("input", cx, "Type here...").with_suggest(Arc::new(|text| {
let suggestions = vec![
("project:", "Filter by project"),
("+work", "Tag: work"),
("+personal", "Tag: personal"),
("due:today", "Tasks for today"),
("due:tomorrow", "Tasks for tomorrow"),
("priority:H", "Priority high"),
("priority:M", "Priority medium"),
("priority:L", "Priority low"),
];
suggestions
.into_iter()
.filter(|(insert, _)| {
text.is_empty()
|| insert.to_lowercase().contains(&text.to_lowercase())
})
.map(|(insert, label)| {
Suggestion::new(format!("{} - {}", insert, label), insert)
})
.collect()
}))
});
cx.observe(&input, |this, input, cx| {
let value = input.read(cx).value().to_string();
this.current_text = value.into();
cx.notify();
})
.detach();
App {
input,
current_text: "".into(),
}
})
},
)
.unwrap();
});
}
}
+261
View File
@@ -0,0 +1,261 @@
use std::sync::Arc;
use gpui::prelude::*;
use crate::components::button::Button;
use crate::theme::ActiveTheme;
#[derive(Clone, Debug)]
pub struct DropdownItem {
pub label: gpui::SharedString,
}
impl DropdownItem {
pub fn new(label: impl Into<gpui::SharedString>) -> Self {
Self {
label: label.into(),
}
}
}
pub struct Dropdown {
id: gpui::ElementId,
button: Option<Button>,
items: Vec<DropdownItem>,
open: bool,
selected_index: Option<usize>,
disabled: bool,
loading: bool,
placeholder: gpui::SharedString,
on_select: Option<Arc<dyn Fn(usize, &DropdownItem, &mut gpui::Context<Self>) + Send + Sync>>,
}
impl Dropdown {
pub fn new(id: impl Into<gpui::ElementId>) -> Self {
Self {
id: id.into(),
button: None,
items: Vec::new(),
open: false,
selected_index: None,
disabled: false,
loading: false,
placeholder: "Seleccionar".into(),
on_select: None,
}
}
pub fn button(mut self, button: Button) -> Self {
self.button = Some(button);
self
}
pub fn item(mut self, item: DropdownItem) -> Self {
self.items.push(item);
self
}
pub fn items<I>(mut self, items: I) -> Self
where
I: IntoIterator<Item = DropdownItem>,
{
self.items = items.into_iter().collect();
self
}
pub fn placeholder(mut self, placeholder: impl Into<gpui::SharedString>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn selected_index(mut self, index: usize) -> Self {
self.selected_index = Some(index);
self
}
pub fn selected_index_value(&self) -> Option<usize> {
self.selected_index
}
pub fn selected_item(&self) -> Option<&DropdownItem> {
self.selected_index.and_then(|index| self.items.get(index))
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
pub fn on_select(
mut self,
handler: Arc<dyn Fn(usize, &DropdownItem, &mut gpui::Context<Self>) + Send + Sync>,
) -> Self {
self.on_select = Some(handler);
self
}
pub fn selected_label(&self) -> Option<gpui::SharedString> {
self.selected_index
.and_then(|index| self.items.get(index).map(|item| item.label.clone()))
}
fn toggle_open(&mut self, cx: &mut gpui::Context<Self>) {
if self.disabled || self.loading || self.items.is_empty() {
return;
}
self.open = !self.open;
cx.notify();
}
fn select_item(&mut self, index: usize, cx: &mut gpui::Context<Self>) {
if self.disabled || self.loading {
return;
}
let item = match self.items.get(index) {
Some(item) => item,
None => return,
};
self.selected_index = Some(index);
self.open = false;
if let Some(on_select) = self.on_select.clone() {
on_select(index, item, cx);
}
cx.notify();
}
fn handle_trigger_mouse_down(
&mut self,
_event: &gpui::MouseDownEvent,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) {
self.toggle_open(cx);
}
fn handle_mouse_down_out(
&mut self,
_event: &gpui::MouseDownEvent,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) {
if self.open {
self.open = false;
cx.notify();
}
}
fn render_menu(&self, cx: &gpui::Context<Self>) -> gpui::AnyElement {
if !self.open || self.items.is_empty() {
return gpui::div().into_any_element();
}
let theme = cx.theme();
let is_disabled = self.disabled || self.loading;
let items: Vec<gpui::AnyElement> = self
.items
.iter()
.enumerate()
.map(|(index, item)| {
let is_selected = self.selected_index == Some(index);
let mut row = gpui::div()
.id(index)
.px_2()
.py_1()
.text_color(if is_selected {
theme.selection_foreground
} else {
theme.foreground
})
.when(is_selected, |el| el.bg(theme.selection))
.hover(|s: gpui::StyleRefinement| s.bg(theme.selection))
.child(item.label.clone());
if is_disabled {
row = row.text_color(theme.muted).cursor_not_allowed();
} else {
row = row.cursor_pointer().on_mouse_down(
gpui::MouseButton::Left,
cx.listener(move |this, _event, _window, cx| {
this.select_item(index, cx);
}),
);
}
row.into_any_element()
})
.collect();
gpui::div()
.absolute()
.top_full()
.left_0()
.right_0()
.mt_1()
.border_1()
.border_color(theme.border)
.bg(theme.panel)
.rounded_md()
.overflow_hidden()
.children(items)
.into_any_element()
}
}
impl gpui::Render for Dropdown {
fn render(
&mut self,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let is_disabled = self.disabled || self.loading;
let label = self
.selected_label()
.unwrap_or_else(|| self.placeholder.clone());
let mut trigger = if let Some(button) = self.button.clone() {
let mut button = button;
button.on_click = None;
button.label = Some(label.clone());
button.disabled(is_disabled).loading(self.loading)
} else {
Button::label(self.id.clone(), label)
.disabled(is_disabled)
.loading(self.loading)
};
if is_disabled || self.items.is_empty() {
trigger = trigger.disabled(true);
}
let mut trigger_wrap = gpui::div().child(trigger);
if !is_disabled && !self.items.is_empty() {
trigger_wrap = trigger_wrap.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(Self::handle_trigger_mouse_down),
);
}
let mut container = gpui::div()
.id(self.id.clone())
.relative()
.flex()
.flex_col()
.child(trigger_wrap)
.child(self.render_menu(cx));
if self.open {
container = container.on_mouse_down_out(cx.listener(Self::handle_mouse_down_out));
}
container
}
}
+287
View File
@@ -0,0 +1,287 @@
pub mod dropdown;
pub use dropdown::{Dropdown, DropdownItem};
use gpui::{
App, ClickEvent, ElementId, Pixels, SharedString, StyleRefinement, Window, div, prelude::*, px,
};
use std::sync::Arc;
use crate::components::icon::{Icon, IconSize};
use crate::theme::ActiveTheme;
fn darken(color: gpui::Rgba, amount: f32) -> gpui::Rgba {
gpui::Rgba {
r: (color.r * (1.0 - amount)).max(0.0),
g: (color.g * (1.0 - amount)).max(0.0),
b: (color.b * (1.0 - amount)).max(0.0),
a: color.a,
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum ButtonSize {
Small,
#[default]
Medium,
Large,
}
impl ButtonSize {
fn height(self) -> Pixels {
match self {
ButtonSize::Small => px(28.),
ButtonSize::Medium => px(36.),
ButtonSize::Large => px(44.),
}
}
fn px(self) -> Pixels {
match self {
ButtonSize::Small => px(8.),
ButtonSize::Medium => px(12.),
ButtonSize::Large => px(16.),
}
}
fn icon_size(self) -> IconSize {
match self {
ButtonSize::Small => IconSize::Small,
ButtonSize::Medium => IconSize::Medium,
ButtonSize::Large => IconSize::Large,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ButtonVariant {
Primary,
#[default]
Secondary,
Ghost,
Danger,
Success,
Text,
}
pub trait ButtonVariants: Sized {
fn with_variant(self, variant: ButtonVariant) -> Self;
fn primary(self) -> Self {
self.with_variant(ButtonVariant::Primary)
}
fn secondary(self) -> Self {
self.with_variant(ButtonVariant::Secondary)
}
fn ghost(self) -> Self {
self.with_variant(ButtonVariant::Ghost)
}
fn danger(self) -> Self {
self.with_variant(ButtonVariant::Danger)
}
fn success(self) -> Self {
self.with_variant(ButtonVariant::Success)
}
fn text(self) -> Self {
self.with_variant(ButtonVariant::Text)
}
}
#[derive(Clone, IntoElement)]
pub struct Button {
id: ElementId,
label: Option<SharedString>,
icon: Option<Icon>,
variant: ButtonVariant,
size: ButtonSize,
disabled: bool,
loading: bool,
style: StyleRefinement,
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl Button {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
label: None,
icon: None,
variant: ButtonVariant::default(),
size: ButtonSize::default(),
disabled: false,
loading: false,
style: StyleRefinement::default(),
on_click: None,
}
}
pub fn label(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
Self::new(id).with_label(label)
}
pub fn icon(id: impl Into<ElementId>, icon: impl Into<Icon>) -> Self {
Self::new(id).with_icon(icon)
}
pub fn with_label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn with_size(mut self, size: ButtonSize) -> Self {
self.size = size;
self
}
pub fn small(self) -> Self {
self.with_size(ButtonSize::Small)
}
pub fn medium(self) -> Self {
self.with_size(ButtonSize::Medium)
}
pub fn large(self) -> Self {
self.with_size(ButtonSize::Large)
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
pub fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Arc::new(handler));
self
}
}
impl ButtonVariants for Button {
fn with_variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
}
impl Styled for Button {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Button {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let is_icon_only = self.label.is_none();
let transparent = gpui::rgba(0x00000000);
let (bg, fg, border, hover_bg) = match self.variant {
ButtonVariant::Primary => (
theme.accent,
theme.background,
theme.accent,
darken(theme.accent, 0.1),
),
ButtonVariant::Secondary => {
(theme.panel, theme.foreground, theme.border, theme.selection)
}
ButtonVariant::Ghost => (transparent, theme.foreground, transparent, theme.selection),
ButtonVariant::Danger => (
theme.error,
theme.background,
theme.error,
darken(theme.error, 0.1),
),
ButtonVariant::Success => (
theme.success,
theme.background,
theme.success,
darken(theme.success, 0.1),
),
ButtonVariant::Text => (transparent, theme.foreground, transparent, transparent),
};
let (bg, fg, border) = if self.disabled {
(transparent, theme.muted, theme.border)
} else {
(bg, fg, border)
};
let height = self.size.height();
let px = self.size.px();
let icon_size = self.size.icon_size();
let mut base = div()
.id(self.id)
.flex()
.items_center()
.justify_center()
.gap_2()
.h(height)
.border_1()
.rounded_md()
.cursor_pointer()
.bg(bg)
.border_color(border)
.text_color(fg);
base = if is_icon_only {
base.px(px)
} else {
base.px(px * 1.5)
};
if !self.disabled && self.variant != ButtonVariant::Text {
base = base
.hover(|s: gpui::StyleRefinement| s.bg(hover_bg))
.active(|s: gpui::StyleRefinement| s.opacity(0.8));
}
if self.disabled {
base = base.cursor_not_allowed();
}
if let Some(icon) = self.icon {
base = base.child(icon.size(icon_size));
}
if let Some(label) = self.label {
base = base.child(label);
}
if self.loading {
base = base.opacity(0.7);
}
if let Some(on_click) = self.on_click {
let disabled = self.disabled || self.loading;
base = base.on_click(move |event, window, cx| {
if !disabled {
on_click(event, window, cx);
}
});
}
*base.style() = self.style;
base
}
}
+46
View File
@@ -0,0 +1,46 @@
use gpui::Styled as _;
use crate::theme::{ActiveTheme, Color};
pub enum DividerDirection {
Horizontal,
Vertical,
}
#[derive(gpui::IntoElement)]
pub struct Divider {
color: Color,
direction: DividerDirection,
}
impl Divider {
pub fn new(color: Color, direction: DividerDirection) -> Self {
Self { color, direction }
}
pub fn build(cx: &mut gpui::Context<Self>) -> Self {
Self {
color: cx.theme().border,
direction: DividerDirection::Horizontal,
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn direction(mut self, direction: DividerDirection) -> Self {
self.direction = direction;
self
}
}
impl gpui::RenderOnce for Divider {
fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl gpui::IntoElement {
match self.direction {
DividerDirection::Horizontal => gpui::div().h(gpui::px(1.0)).w_full().bg(self.color),
DividerDirection::Vertical => gpui::div().w(gpui::px(1.0)).h_full().bg(self.color),
}
}
}
+162
View File
@@ -0,0 +1,162 @@
use gpui::{Pixels, SharedString, StyleRefinement, Styled, prelude::*, px, rems};
#[derive(Debug, Clone, Copy, Default)]
pub enum IconSize {
XSmall, // 12px
Small, // 14px
#[default]
Medium, // 16px
Large, // 20px
XLarge, // 24px
Custom(Pixels),
}
impl IconSize {
pub fn px(self) -> Pixels {
match self {
IconSize::XSmall => px(12.),
IconSize::Small => px(14.),
IconSize::Medium => px(16.),
IconSize::Large => px(20.),
IconSize::XLarge => px(24.),
IconSize::Custom(size) => size,
}
}
pub fn rems(self) -> gpui::Rems {
match self {
IconSize::XSmall => rems(0.75),
IconSize::Small => rems(0.875),
IconSize::Medium => rems(1.),
IconSize::Large => rems(1.25),
IconSize::XLarge => rems(1.5),
IconSize::Custom(size) => rems(f32::from(size) / 16.),
}
}
}
impl From<Pixels> for IconSize {
fn from(px: Pixels) -> Self {
IconSize::Custom(px)
}
}
impl From<f32> for IconSize {
fn from(px: f32) -> Self {
IconSize::Custom(gpui::px(px))
}
}
pub trait IconNamed {
fn path(&self) -> SharedString;
}
#[derive(Debug, Clone, Copy)]
pub enum IconName {}
impl IconNamed for IconName {
fn path(&self) -> SharedString {
match self {
// Mapear cada variante a su path SVG
// IconName::Check => "icons/check.svg".into(),
// IconName::Close => "icons/close.svg".into(),
_ => "".into(),
}
}
}
impl<T: IconNamed> From<T> for Icon {
fn from(name: T) -> Self {
Self::new(name)
}
}
#[derive(Clone, IntoElement)]
pub struct Icon {
path: SharedString,
size: IconSize,
color: Option<gpui::Hsla>,
style: StyleRefinement,
}
impl Default for Icon {
fn default() -> Self {
Self {
path: SharedString::default(),
size: IconSize::default(),
color: None,
style: StyleRefinement::default(),
}
}
}
impl Icon {
pub fn new(name: impl IconNamed) -> Self {
Self::default().path(name.path())
}
pub fn from_path(path: impl Into<SharedString>) -> Self {
Self::default().path(path)
}
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = path.into();
self
}
pub fn size(mut self, size: impl Into<IconSize>) -> Self {
self.size = size.into();
self
}
pub fn xsmall(self) -> Self {
self.size(IconSize::XSmall)
}
pub fn small(self) -> Self {
self.size(IconSize::Small)
}
pub fn medium(self) -> Self {
self.size(IconSize::Medium)
}
pub fn large(self) -> Self {
self.size(IconSize::Large)
}
pub fn xlarge(self) -> Self {
self.size(IconSize::XLarge)
}
pub fn color(mut self, color: impl Into<gpui::Hsla>) -> Self {
self.color = Some(color.into());
self
}
pub fn with_style(mut self, style: StyleRefinement) -> Self {
self.style = style;
self
}
}
impl Styled for Icon {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Icon {
fn render(mut self, window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement {
let size = self.size.rems();
let color = self.color.unwrap_or_else(|| window.text_style().color);
self.style.size.width = Some(size.into());
self.style.size.height = Some(size.into());
let mut svg = gpui::svg().path(self.path).flex_none().text_color(color);
*svg.style() = self.style;
svg
}
}
+567
View File
@@ -0,0 +1,567 @@
mod suggestion;
use crate::theme::ActiveTheme;
use gpui::prelude::*;
use std::sync::Arc;
pub use suggestion::Suggestion;
pub struct Input {
id: gpui::ElementId,
focus: gpui::FocusHandle,
value: String,
placeholder: gpui::SharedString,
cursor_pos: usize,
suggestions: Vec<Suggestion>,
suggestions_open: bool,
active_suggestion: usize,
suggest: Option<Arc<dyn Fn(&str) -> Vec<Suggestion> + Send + Sync>>,
on_change: Option<Arc<dyn Fn(&str, &mut gpui::Context<Self>) + Send + Sync>>,
on_submit: Option<Arc<dyn Fn(&str, &mut gpui::Context<Self>) + Send + Sync>>,
}
impl Input {
pub fn new(
id: impl Into<gpui::ElementId>,
cx: &mut gpui::Context<Self>,
placeholder: impl Into<gpui::SharedString>,
) -> Self {
Self {
id: id.into(),
focus: cx.focus_handle(),
value: String::new(),
placeholder: placeholder.into(),
cursor_pos: 0,
suggestions: vec![],
suggestions_open: false,
active_suggestion: 0,
suggest: None,
on_change: None,
on_submit: None,
}
}
pub fn with_suggest(mut self, f: Arc<dyn Fn(&str) -> Vec<Suggestion> + Send + Sync>) -> Self {
self.suggest = Some(f);
self
}
pub fn with_on_change(
mut self,
f: Arc<dyn Fn(&str, &mut gpui::Context<Self>) + Send + Sync>,
) -> Self {
self.on_change = Some(f);
self
}
pub fn with_on_submit(
mut self,
f: Arc<dyn Fn(&str, &mut gpui::Context<Self>) + Send + Sync>,
) -> Self {
self.on_submit = Some(f);
self
}
pub fn value(&self) -> &str {
&self.value
}
pub fn set_value(&mut self, value: impl Into<String>, cx: &mut gpui::Context<Self>) {
self.value = value.into();
self.cursor_pos = self.value.len();
self.refresh_suggestions(cx);
cx.notify();
}
pub fn clear(&mut self, cx: &mut gpui::Context<Self>) {
self.set_value("", cx);
}
pub fn focus(&self, window: &mut gpui::Window, cx: &mut gpui::Context<Self>) {
window.focus(&self.focus);
cx.notify();
}
fn word_start_before(&self, pos: usize) -> usize {
if pos == 0 {
return 0;
}
let bytes = self.value.as_bytes();
let mut i = pos;
while i > 0 && bytes[i - 1].is_ascii_whitespace() {
i -= 1;
}
while i > 0 && !bytes[i - 1].is_ascii_whitespace() {
i -= 1;
}
i
}
fn word_end_after(&self, pos: usize) -> usize {
let len = self.value.len();
if pos >= len {
return len;
}
let bytes = self.value.as_bytes();
let mut i = pos;
while i < len && !bytes[i].is_ascii_whitespace() {
i += 1;
}
while i < len && bytes[i].is_ascii_whitespace() {
i += 1;
}
i
}
fn move_left(&mut self) {
if self.cursor_pos > 0 {
let mut new_pos = self.cursor_pos - 1;
while new_pos > 0 && !self.value.is_char_boundary(new_pos) {
new_pos -= 1;
}
self.cursor_pos = new_pos;
}
}
fn move_right(&mut self) {
if self.cursor_pos < self.value.len() {
let mut new_pos = self.cursor_pos + 1;
while new_pos < self.value.len() && !self.value.is_char_boundary(new_pos) {
new_pos += 1;
}
self.cursor_pos = new_pos;
}
}
fn refresh_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
if let Some(suggest) = &self.suggest {
self.suggestions = suggest(&self.value);
self.active_suggestion = 0;
self.suggestions_open = !self.suggestions.is_empty() && !self.value.is_empty();
cx.notify();
}
}
fn open_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
if let Some(suggest) = &self.suggest {
self.suggestions = suggest(&self.value);
self.active_suggestion = 0;
self.suggestions_open = !self.suggestions.is_empty();
cx.notify();
}
}
fn accept_suggestion(&mut self, cx: &mut gpui::Context<Self>) {
if !self.suggestions_open {
return;
}
if let Some(s) = self.suggestions.get(self.active_suggestion).cloned() {
self.value = s.insert.to_string();
self.cursor_pos = self.value.len();
self.suggestions_open = false;
if let Some(on_change) = self.on_change.clone() {
on_change(&self.value, cx);
}
cx.notify();
}
}
fn click_suggestion(&mut self, index: usize, cx: &mut gpui::Context<Self>) {
if !self.suggestions_open {
return;
}
if index >= self.suggestions.len() {
return;
}
self.active_suggestion = index;
self.accept_suggestion(cx);
}
fn submit(&mut self, cx: &mut gpui::Context<Self>) {
self.suggestions_open = false;
if let Some(on_submit) = self.on_submit.clone() {
on_submit(&self.value, cx);
}
cx.notify();
}
fn move_suggestion(&mut self, delta: isize, cx: &mut gpui::Context<Self>) {
if !self.suggestions_open || self.suggestions.is_empty() {
return;
}
let len = self.suggestions.len() as isize;
let mut next = self.active_suggestion as isize + delta;
if next < 0 {
next = len - 1;
}
if next >= len {
next = 0;
}
self.active_suggestion = next as usize;
cx.notify();
}
fn insert_text(&mut self, text: &str, cx: &mut gpui::Context<Self>) {
self.value.insert_str(self.cursor_pos, text);
self.cursor_pos += text.len();
if let Some(on_change) = self.on_change.clone() {
on_change(&self.value, cx);
}
self.refresh_suggestions(cx);
cx.notify();
}
fn delete_backward(&mut self, cx: &mut gpui::Context<Self>) {
if self.cursor_pos == 0 {
return;
}
let old_pos = self.cursor_pos;
self.move_left();
self.value.drain(self.cursor_pos..old_pos);
if let Some(on_change) = self.on_change.clone() {
on_change(&self.value, cx);
}
self.refresh_suggestions(cx);
cx.notify();
}
fn delete_forward(&mut self, cx: &mut gpui::Context<Self>) {
if self.cursor_pos >= self.value.len() {
return;
}
let mut end = self.cursor_pos + 1;
while end < self.value.len() && !self.value.is_char_boundary(end) {
end += 1;
}
self.value.drain(self.cursor_pos..end);
if let Some(on_change) = self.on_change.clone() {
on_change(&self.value, cx);
}
self.refresh_suggestions(cx);
cx.notify();
}
fn delete_word_backward(&mut self, cx: &mut gpui::Context<Self>) {
if self.cursor_pos == 0 {
return;
}
let word_start = self.word_start_before(self.cursor_pos);
self.value.drain(word_start..self.cursor_pos);
self.cursor_pos = word_start;
if let Some(on_change) = self.on_change.clone() {
on_change(&self.value, cx);
}
self.refresh_suggestions(cx);
cx.notify();
}
fn delete_word_forward(&mut self, cx: &mut gpui::Context<Self>) {
if self.cursor_pos >= self.value.len() {
return;
}
let word_end = self.word_end_after(self.cursor_pos);
self.value.drain(self.cursor_pos..word_end);
if let Some(on_change) = self.on_change.clone() {
on_change(&self.value, cx);
}
self.refresh_suggestions(cx);
cx.notify();
}
fn handle_key_down(
&mut self,
event: &gpui::KeyDownEvent,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) {
let key = event.keystroke.key.as_str();
let ctrl = event.keystroke.modifiers.control;
let shift = event.keystroke.modifiers.shift;
match key {
"enter" => {
if self.suggestions_open {
self.accept_suggestion(cx);
} else {
self.submit(cx);
}
}
"escape" => {
self.suggestions_open = false;
cx.notify();
}
"tab" => {
if shift {
if self.suggestions_open {
self.move_suggestion(-1, cx);
}
} else {
if self.suggestions_open {
self.move_suggestion(1, cx);
} else {
self.open_suggestions(cx);
}
}
}
"up" => self.move_suggestion(-1, cx),
"down" => self.move_suggestion(1, cx),
"left" => {
if ctrl {
self.cursor_pos = self.word_start_before(self.cursor_pos);
cx.notify();
} else {
self.move_left();
cx.notify();
}
}
"right" => {
if ctrl {
self.cursor_pos = self.word_end_after(self.cursor_pos);
cx.notify();
} else {
self.move_right();
cx.notify();
}
}
"home" => {
self.cursor_pos = 0;
cx.notify();
}
"end" => {
self.cursor_pos = self.value.len();
cx.notify();
}
"backspace" => {
if ctrl {
self.delete_word_backward(cx);
} else {
self.delete_backward(cx);
}
}
"delete" => {
if ctrl {
self.delete_word_forward(cx);
} else {
self.delete_forward(cx);
}
}
"w" if ctrl => {
self.delete_word_backward(cx);
}
"a" if ctrl => {
self.cursor_pos = 0;
cx.notify();
}
"e" if ctrl => {
self.cursor_pos = self.value.len();
cx.notify();
}
"u" if ctrl => {
self.value.drain(0..self.cursor_pos);
self.cursor_pos = 0;
if let Some(on_change) = self.on_change.clone() {
on_change(&self.value, cx);
}
self.refresh_suggestions(cx);
cx.notify();
}
"k" if ctrl => {
self.value.truncate(self.cursor_pos);
if let Some(on_change) = self.on_change.clone() {
on_change(&self.value, cx);
}
self.refresh_suggestions(cx);
cx.notify();
}
_ => {
if let Some(ch) = event.keystroke.key_char.as_ref() {
if !ctrl && ch != "\n" && ch != "\r" && ch != "\t" {
self.insert_text(ch, cx);
}
}
}
}
}
fn render_suggestions(&self, cx: &gpui::Context<Self>) -> impl IntoElement {
if !self.suggestions_open {
return gpui::div().into_any_element();
}
let theme = cx.theme();
let items: Vec<gpui::AnyElement> = self
.suggestions
.iter()
.enumerate()
.map(|(i, s)| {
let is_active = i == self.active_suggestion;
gpui::div()
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(move |this, _e, _w, cx| {
this.click_suggestion(i, cx);
}),
)
.cursor_pointer()
.px_2()
.py_1()
.when(is_active, |el| el.bg(theme.selection))
.text_color(if is_active {
theme.selection_foreground
} else {
theme.foreground
})
.child(s.label.clone())
.into_any_element()
})
.collect();
gpui::div()
.absolute()
.top_full()
.left_0()
.right_0()
.mt_1()
.border_1()
.border_color(theme.border)
.bg(theme.panel)
.rounded_md()
.overflow_hidden()
.children(items)
.into_any_element()
}
}
impl gpui::Render for Input {
fn render(
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let theme = cx.theme();
let is_focused = self.focus.is_focused(window);
let show_placeholder = self.value.is_empty();
let content = if show_placeholder {
let cursor = if is_focused {
gpui::div().w_px().h_4().bg(theme.accent).into_any_element()
} else {
gpui::div().into_any_element()
};
gpui::div()
.id(self.id.clone())
.flex()
.flex_row()
.items_center()
.child(cursor)
.child(
gpui::div()
.text_color(theme.muted)
.child(self.placeholder.clone()),
)
.into_any_element()
} else {
let before = &self.value[..self.cursor_pos];
let after = &self.value[self.cursor_pos..];
let cursor = if is_focused {
gpui::div()
.id(self.id.clone())
.w_px()
.h_4()
.bg(theme.accent)
.into_any_element()
} else {
gpui::div().id(self.id.clone()).into_any_element()
};
gpui::div()
.id(self.id.clone())
.flex()
.flex_row()
.items_center()
.child(
gpui::div()
.text_color(theme.foreground)
.child(before.to_string()),
)
.child(cursor)
.child(
gpui::div()
.text_color(theme.foreground)
.child(after.to_string()),
)
.into_any_element()
};
let focus_handle = self.focus.clone();
gpui::div()
.id(self.id.clone())
.key_context("Input")
.track_focus(&self.focus)
.on_key_down(cx.listener(Self::handle_key_down))
.on_mouse_down(gpui::MouseButton::Left, move |_ev, window, _cx| {
window.focus(&focus_handle);
})
.relative()
.min_w(gpui::rems(12.))
.border_1()
.border_color(if is_focused {
theme.accent
} else {
theme.border
})
.bg(theme.background)
.rounded_md()
.p_2()
.cursor(gpui::CursorStyle::IBeam)
.child(content)
.child(self.render_suggestions(cx))
}
}
impl gpui::Focusable for Input {
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
self.focus.clone()
}
}
+24
View File
@@ -0,0 +1,24 @@
use gpui::SharedString;
#[derive(Clone, Debug)]
pub struct Suggestion {
pub label: SharedString,
pub insert: SharedString,
}
impl Suggestion {
pub fn new(label: impl Into<SharedString>, insert: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
insert: insert.into(),
}
}
pub fn simple(text: impl Into<SharedString>) -> Self {
let text = text.into();
Self {
label: text.clone(),
insert: text,
}
}
}
+31
View File
@@ -0,0 +1,31 @@
use gpui::prelude::*;
use crate::theme::ActiveTheme;
#[derive(gpui::IntoElement)]
pub struct Label {
text: gpui::SharedString,
}
impl Label {
pub fn new(text: impl Into<gpui::SharedString>) -> Self {
Self { text: text.into() }
}
}
// impl gpui::Styled for Label {
// fn style(&mut self) -> &mut gpui::StyleRefinement {
// &mut self.style
// }
// }
impl RenderOnce for Label {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
let theme = cx.theme();
gpui::div()
.line_height(gpui::rems(1.25))
.text_color(theme.foreground)
.child(gpui::StyledText::new(&self.text))
}
}
+198
View File
@@ -0,0 +1,198 @@
use std::sync::Arc;
use gpui::prelude::*;
use crate::theme::ActiveTheme;
#[derive(Clone, Debug)]
pub struct ListItem {
pub label: gpui::SharedString,
pub disabled: bool,
}
impl ListItem {
pub fn new(label: impl Into<gpui::SharedString>) -> Self {
Self {
label: label.into(),
disabled: false,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
pub struct List {
id: gpui::ElementId,
items: Vec<ListItem>,
selected_index: Option<usize>,
height: Option<gpui::Pixels>,
on_click: Option<Arc<dyn Fn(usize, &ListItem, &mut gpui::Context<Self>) + Send + Sync>>,
on_hover: Option<Arc<dyn Fn(usize, bool, &ListItem, &mut gpui::Context<Self>) + Send + Sync>>,
}
impl List {
pub fn new(id: impl Into<gpui::ElementId>) -> Self {
Self {
id: id.into(),
items: Vec::new(),
selected_index: None,
height: None,
on_click: None,
on_hover: None,
}
}
pub fn item(mut self, item: ListItem) -> Self {
self.items.push(item);
self
}
pub fn items<I>(mut self, items: I) -> Self
where
I: IntoIterator<Item = ListItem>,
{
self.items = items.into_iter().collect();
self
}
pub fn selected_index(mut self, index: usize) -> Self {
self.selected_index = Some(index);
self
}
pub fn selected_index_value(&self) -> Option<usize> {
self.selected_index
}
pub fn selected_item(&self) -> Option<&ListItem> {
self.selected_index.and_then(|index| self.items.get(index))
}
pub fn height(mut self, height: gpui::Pixels) -> Self {
self.height = Some(height);
self
}
pub fn on_click(
mut self,
handler: Arc<dyn Fn(usize, &ListItem, &mut gpui::Context<Self>) + Send + Sync>,
) -> Self {
self.on_click = Some(handler);
self
}
pub fn on_hover(
mut self,
handler: Arc<dyn Fn(usize, bool, &ListItem, &mut gpui::Context<Self>) + Send + Sync>,
) -> Self {
self.on_hover = Some(handler);
self
}
fn click_item(&mut self, index: usize, cx: &mut gpui::Context<Self>) {
let item = match self.items.get(index) {
Some(item) => item,
None => return,
};
if item.disabled {
return;
}
self.selected_index = Some(index);
if let Some(on_click) = self.on_click.clone() {
on_click(index, item, cx);
}
cx.notify();
}
fn hover_item(&mut self, index: usize, hovering: bool, cx: &mut gpui::Context<Self>) {
let item = match self.items.get(index) {
Some(item) => item,
None => return,
};
if item.disabled {
return;
}
if let Some(on_hover) = self.on_hover.clone() {
on_hover(index, hovering, item, cx);
}
}
}
impl gpui::Render for List {
fn render(
&mut self,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let theme = cx.theme();
let items: Vec<gpui::AnyElement> = self
.items
.iter()
.enumerate()
.map(|(index, item)| {
let is_selected = self.selected_index == Some(index);
let mut row = gpui::div()
.id(index)
.w_full()
.px_2()
.py_1()
.text_color(if is_selected {
theme.selection_foreground
} else {
theme.foreground
})
.when(is_selected, |el| el.bg(theme.selection))
.hover(|s: gpui::StyleRefinement| s.bg(theme.selection))
.child(item.label.clone());
if item.disabled {
row = row.text_color(theme.muted).cursor_not_allowed();
} else {
row = row
.cursor_pointer()
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(move |this, _event, _window, cx| {
this.click_item(index, cx);
}),
)
.on_hover(cx.listener(move |this, hovering, _window, cx| {
this.hover_item(index, *hovering, cx);
}));
}
row.into_any_element()
})
.collect();
let mut container = gpui::div()
.id(self.id.clone())
.flex()
.flex_col()
.w_full()
.border_1()
.border_color(theme.border)
.bg(theme.panel)
.rounded_md()
.overflow_y_scroll()
.scrollbar_width(gpui::px(6.0))
.children(items);
if let Some(height) = self.height {
container = container.h(height);
}
container
}
}
+8
View File
@@ -0,0 +1,8 @@
pub mod button;
pub mod divider;
pub mod icon;
pub mod input;
pub mod label;
pub mod list;
pub mod modal;
pub mod panel;
+369
View File
@@ -0,0 +1,369 @@
use std::sync::Arc;
use gpui::prelude::*;
use crate::components::button::{Button, ButtonVariants};
use crate::theme::ActiveTheme;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModalAction {
Close,
Cancel,
Save,
}
#[derive(Clone, Debug, Default)]
pub struct ModalState {
pub open: bool,
pub last_action: Option<ModalAction>,
}
pub struct Modal {
id: gpui::ElementId,
focus: gpui::FocusHandle,
title: Option<gpui::SharedString>,
content: Vec<Arc<dyn Fn() -> gpui::AnyElement + Send + Sync>>,
footer: Vec<Arc<dyn Fn() -> gpui::AnyElement + Send + Sync>>,
width: Option<gpui::Pixels>,
open: bool,
close_on_backdrop: bool,
last_action: Option<ModalAction>,
on_close: Option<Arc<dyn Fn(ModalAction, &mut gpui::Context<Self>) + Send + Sync>>,
on_save: Option<Arc<dyn Fn(&mut gpui::Context<Self>) + Send + Sync>>,
on_cancel: Option<Arc<dyn Fn(&mut gpui::Context<Self>) + Send + Sync>>,
}
impl Modal {
pub fn new(id: impl Into<gpui::ElementId>, cx: &mut gpui::Context<Self>) -> Self {
Self {
id: id.into(),
focus: cx.focus_handle(),
title: None,
content: Vec::new(),
footer: Vec::new(),
width: Some(gpui::px(520.0)),
open: false,
close_on_backdrop: true,
last_action: None,
on_close: None,
on_save: None,
on_cancel: None,
}
}
pub fn title(mut self, title: impl Into<gpui::SharedString>) -> Self {
self.title = Some(title.into());
self
}
pub fn width(mut self, width: gpui::Pixels) -> Self {
self.width = Some(width);
self
}
pub fn close_on_backdrop(mut self, close: bool) -> Self {
self.close_on_backdrop = close;
self
}
pub fn child<E>(mut self, child: E) -> Self
where
E: gpui::IntoElement + Clone + Send + Sync + 'static,
{
let element = child.clone();
self.content
.push(Arc::new(move || element.clone().into_any_element()));
self
}
pub fn children<E>(mut self, children: impl IntoIterator<Item = E>) -> Self
where
E: gpui::IntoElement + Clone + Send + Sync + 'static,
{
self.content.extend(
children
.into_iter()
.map(|child| {
let element = child.clone();
Arc::new(move || element.clone().into_any_element())
as Arc<dyn Fn() -> gpui::AnyElement + Send + Sync>
})
.collect::<Vec<_>>(),
);
self
}
pub fn footer_child<E>(mut self, child: E) -> Self
where
E: gpui::IntoElement + Clone + Send + Sync + 'static,
{
let element = child.clone();
self.footer
.push(Arc::new(move || element.clone().into_any_element()));
self
}
pub fn footer_children<E>(mut self, children: impl IntoIterator<Item = E>) -> Self
where
E: gpui::IntoElement + Clone + Send + Sync + 'static,
{
self.footer.extend(
children
.into_iter()
.map(|child| {
let element = child.clone();
Arc::new(move || element.clone().into_any_element())
as Arc<dyn Fn() -> gpui::AnyElement + Send + Sync>
})
.collect::<Vec<_>>(),
);
self
}
pub fn on_close(
mut self,
handler: Arc<dyn Fn(ModalAction, &mut gpui::Context<Self>) + Send + Sync>,
) -> Self {
self.on_close = Some(handler);
self
}
pub fn on_save(mut self, handler: Arc<dyn Fn(&mut gpui::Context<Self>) + Send + Sync>) -> Self {
self.on_save = Some(handler);
self
}
pub fn on_cancel(
mut self,
handler: Arc<dyn Fn(&mut gpui::Context<Self>) + Send + Sync>,
) -> Self {
self.on_cancel = Some(handler);
self
}
pub fn set_open(&mut self, open: bool, cx: &mut gpui::Context<Self>) {
if self.open == open {
return;
}
self.open = open;
cx.notify();
}
pub fn open(&mut self, cx: &mut gpui::Context<Self>) {
self.set_open(true, cx);
}
pub fn close(&mut self, cx: &mut gpui::Context<Self>) {
self.close_with_action(ModalAction::Close, cx);
}
pub fn toggle(&mut self, cx: &mut gpui::Context<Self>) {
let next = !self.open;
self.set_open(next, cx);
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn focus_handle(&self) -> &gpui::FocusHandle {
&self.focus
}
pub fn state(&self) -> ModalState {
ModalState {
open: self.open,
last_action: self.last_action,
}
}
pub fn last_action(&self) -> Option<ModalAction> {
self.last_action
}
pub fn take_last_action(&mut self) -> Option<ModalAction> {
self.last_action.take()
}
fn close_with_action(&mut self, action: ModalAction, cx: &mut gpui::Context<Self>) {
self.last_action = Some(action);
if let Some(on_close) = self.on_close.clone() {
on_close(action, cx);
}
self.open = false;
cx.notify();
}
fn handle_backdrop_mouse_down(
&mut self,
_event: &gpui::MouseDownEvent,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) {
if self.close_on_backdrop {
self.close_with_action(ModalAction::Close, cx);
}
}
fn handle_cancel(&mut self, cx: &mut gpui::Context<Self>) {
if let Some(on_cancel) = self.on_cancel.clone() {
on_cancel(cx);
}
self.close_with_action(ModalAction::Cancel, cx);
}
fn handle_save(&mut self, cx: &mut gpui::Context<Self>) {
if let Some(on_save) = self.on_save.clone() {
on_save(cx);
}
self.close_with_action(ModalAction::Save, cx);
}
}
impl gpui::Render for Modal {
fn render(
&mut self,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
if !self.open {
return gpui::Empty.into_any_element();
}
let theme = cx.theme();
let title = self.title.clone().map(|title| {
gpui::div()
.px_3()
.py_2()
.text_color(theme.foreground)
.child(title)
.into_any_element()
});
let body: Vec<gpui::AnyElement> = self
.content
.iter()
.enumerate()
.map(|(ix, build)| gpui::div().id(ix).child(build()).into_any_element())
.collect();
let mut footer: Vec<gpui::AnyElement> = self.footer.iter().map(|build| build()).collect();
if footer.is_empty() {
let mut actions = Vec::new();
if self.on_cancel.is_some() {
actions.push(
Button::label((self.id.clone(), "cancel"), "Cancel")
.text()
.on_click(cx.listener(|this, _e, _w, cx| {
this.handle_cancel(cx);
}))
.into_any_element(),
);
}
if self.on_save.is_some() {
actions.push(
Button::label((self.id.clone(), "save"), "Save")
.primary()
.on_click(cx.listener(|this, _e, _w, cx| {
this.handle_save(cx);
}))
.into_any_element(),
);
}
if !actions.is_empty() {
footer = actions;
}
}
let footer = if footer.is_empty() {
gpui::div().into_any_element()
} else {
gpui::div()
.px_3()
.py_2()
.flex()
.flex_row()
.justify_end()
.gap_2()
.children(footer)
.into_any_element()
};
let mut panel = gpui::div()
.id(gpui::ElementId::Name(format!("{}-panel", self.id).into()))
.flex()
.flex_col()
.bg(theme.panel)
.border_1()
.border_color(theme.border)
.rounded_md()
.children(title)
.child(
gpui::div()
.px_3()
.py_2()
.flex()
.flex_col()
.gap_2()
.children(body),
)
.child(footer)
.block_mouse_except_scroll()
.track_focus(&self.focus)
.on_key_down(
cx.listener(|this, event: &gpui::KeyDownEvent, _window, cx| {
if event.keystroke.key.as_str() == "escape" {
this.close_with_action(ModalAction::Cancel, cx);
}
}),
);
if let Some(width) = self.width {
panel = panel.w(width);
}
let panel_wrap = if self.close_on_backdrop {
gpui::div()
.child(panel)
.on_mouse_down_out(cx.listener(|this, event, window, cx| {
this.handle_backdrop_mouse_down(event, window, cx);
}))
} else {
gpui::div().child(panel)
};
gpui::div()
.id(self.id.clone())
.size_full()
.absolute()
.top_0()
.left_0()
.occlude()
.child(
gpui::div()
.size_full()
.bg(gpui::rgba(0x00000080))
.absolute()
.top_0()
.left_0(),
)
.child(
gpui::div()
.size_full()
.absolute()
.top_0()
.left_0()
.flex()
.flex_col()
.items_center()
.justify_center()
.child(panel_wrap),
)
.into_any_element()
}
}
+88
View File
@@ -0,0 +1,88 @@
use gpui::prelude::*;
use crate::theme::ActiveTheme;
#[derive(gpui::IntoElement)]
pub struct Panel {
content: Vec<gpui::AnyElement>,
title: Option<String>,
border: f32,
padding: f32,
}
impl Panel {
pub fn new() -> Self {
Self {
content: Vec::new(),
title: None,
border: 1.0,
padding: 8.0,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn border(mut self, border: f32) -> Self {
self.border = border;
self
}
pub fn padding(mut self, padding: f32) -> Self {
self.padding = padding;
self
}
pub fn child(mut self, child: impl gpui::IntoElement) -> Self {
self.content.push(child.into_any_element());
self
}
pub fn children<E>(mut self, children: impl IntoIterator<Item = E>) -> Self
where
E: gpui::IntoElement,
{
self.content
.extend(children.into_iter().map(|c| c.into_any_element()));
self
}
}
impl gpui::RenderOnce for Panel {
fn render(mut self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
let theme = cx.theme();
let header = self.title.as_ref().map(|title| {
gpui::div()
.px_2()
.py_1()
.text_sm()
.text_color(theme.muted)
.border_b(gpui::px(self.border))
.border_color(theme.border)
.child(title.clone())
.into_any_element()
});
let children: Vec<gpui::AnyElement> = self
.content
.drain(..)
.enumerate()
.map(|(ix, c)| gpui::div().id(ix).child(c).into_any_element())
.collect();
gpui::div()
.size_full()
.bg(theme.panel)
.border(gpui::px(self.border))
.border_color(theme.border)
.rounded_md()
.p(gpui::px(self.padding))
.flex()
.flex_col()
.children(header)
.children(children)
}
}
+5 -87
View File
@@ -1,91 +1,9 @@
use gpui::{ use crate::app::App;
prelude::*, App, Application, Context, CursorStyle, KeyDownEvent, SharedString, Window,
WindowOptions,
};
struct HelloWorld { mod app;
text: SharedString, mod components;
focus_handle: gpui::FocusHandle, mod theme;
}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let input_content = if self.text.is_empty() {
gpui::div()
.text_color(gpui::rgb(0x888888))
.child("Escribe aqui...")
} else {
gpui::div().child(self.text.clone())
};
gpui::div()
.size_full()
.bg(gpui::white())
.flex()
.flex_col()
.gap_3()
.justify_center()
.items_center()
.text_3xl()
.child(
gpui::div()
.min_w(gpui::rems(12.))
.border_1()
.border_color(gpui::black())
.bg(gpui::white())
.p_2()
.cursor(CursorStyle::IBeam)
.track_focus(&self.focus_handle)
.on_key_down(cx.listener(Self::on_key_down))
.child(input_content),
)
.child(format!("Hello, {}!", &self.text))
}
}
impl HelloWorld {
fn on_key_down(
&mut self,
event: &KeyDownEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let mut next = self.text.to_string();
match event.keystroke.key.as_str() {
"backspace" | "delete" => {
next.pop();
}
_ => {
let Some(ch) = event.keystroke.key_char.as_ref() else {
return;
};
if ch == "\n" || ch == "\r" {
return;
}
next.push_str(ch);
}
}
if next != self.text.as_str() {
self.text = next.into();
cx.notify();
}
}
}
fn main() { fn main() {
let app = Application::new(); App::run();
app.run(|app: &mut App| {
app.open_window(
WindowOptions::default(),
|_window: &mut Window, app: &mut App| {
app.new(|cx: &mut Context<'_, HelloWorld>| HelloWorld {
text: SharedString::from("World"),
focus_handle: cx.focus_handle(),
})
},
)
.unwrap();
});
} }
+70
View File
@@ -0,0 +1,70 @@
pub type Color = gpui::Rgba;
#[derive(Debug, Clone)]
pub struct Theme {
pub background: Color,
pub foreground: Color,
pub panel: Color,
pub muted: Color,
pub accent: Color,
pub border: Color,
pub error: Color,
pub success: Color,
pub selection: Color,
pub selection_foreground: Color,
pub text: Color,
pub text_size: Option<gpui::Size<u32>>,
}
impl Theme {
pub fn dark() -> Self {
Self {
background: gpui::rgb(0x1E1E2E),
panel: gpui::rgb(0x181825),
foreground: gpui::rgb(0xCDD6F4),
muted: gpui::rgb(0x7F849C),
accent: gpui::rgb(0x89B4FA),
border: gpui::rgb(0x313244),
error: gpui::rgb(0xF38BA8),
success: gpui::rgb(0xA6E3A1),
selection: gpui::rgb(0x45475A),
selection_foreground: gpui::rgb(0xCDD6F4),
text: gpui::rgb(0xCDD6F4),
text_size: Some(gpui::Size::new(14, 14)),
}
}
pub fn light() -> Self {
Self {
background: gpui::rgb(0xF5F5F5),
panel: gpui::rgb(0xF0F0F0),
foreground: gpui::rgb(0x333333),
muted: gpui::rgb(0x999999),
accent: gpui::rgb(0x0078D4),
border: gpui::rgb(0xE0E0E0),
error: gpui::rgb(0xFF4444),
success: gpui::rgb(0x4CAF50),
selection: gpui::rgb(0xD0D0D0),
selection_foreground: gpui::rgb(0x333333),
text: gpui::rgb(0x333333),
text_size: Some(gpui::Size::new(14, 14)),
}
}
pub fn global(app: &gpui::App) -> &Self {
app.global::<Self>()
}
}
impl gpui::Global for Theme {}
pub trait ActiveTheme {
fn theme(&self) -> &Theme;
}
impl ActiveTheme for gpui::App {
#[inline(always)]
fn theme(&self) -> &Theme {
Theme::global(self)
}
}