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:
+82
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,91 +1,9 @@
|
||||
use gpui::{
|
||||
prelude::*, App, Application, Context, CursorStyle, KeyDownEvent, SharedString, Window,
|
||||
WindowOptions,
|
||||
};
|
||||
use crate::app::App;
|
||||
|
||||
struct HelloWorld {
|
||||
text: SharedString,
|
||||
focus_handle: gpui::FocusHandle,
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
mod app;
|
||||
mod components;
|
||||
mod theme;
|
||||
|
||||
fn main() {
|
||||
let app = Application::new();
|
||||
|
||||
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();
|
||||
});
|
||||
App::run();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user