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::{
|
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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