First commit, with Button UI.
This commit is contained in:
commit
9ffea5798e
20 changed files with 6281 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.DS_Store
|
||||
5286
Cargo.lock
generated
Normal file
5286
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/app",
|
||||
"crates/ui",
|
||||
"crates/workspace",
|
||||
"crates/util"
|
||||
]
|
||||
|
||||
default-members = ["crates/app"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
gpui = { git = "https://github.com/zed-industries/zed.git" }
|
||||
ui = { path = "crates/ui" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
util = { path = "crates/util" }
|
||||
anyhow = "1"
|
||||
log = "0.4"
|
||||
11
crates/app/Cargo.toml
Normal file
11
crates/app/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "main-app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
anyhow.workspace = true
|
||||
rust-embed = "8"
|
||||
log.workspace = true
|
||||
workspace.workspace = true
|
||||
31
crates/app/src/assets.rs
Normal file
31
crates/app/src/assets.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use anyhow::anyhow;
|
||||
|
||||
use gpui::AssetSource;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../../assets"]
|
||||
#[include = "fonts/**/*"]
|
||||
#[include = "icons/**/*"]
|
||||
#[exclude = "*.DS_Store"]
|
||||
pub struct Assets;
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
Self::get(path)
|
||||
.map(|f| Some(f.data))
|
||||
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> gpui::Result<Vec<gpui::SharedString>> {
|
||||
Ok(Self::iter()
|
||||
.filter_map(|p| {
|
||||
if p.starts_with(path) {
|
||||
Some(p.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
34
crates/app/src/main.rs
Normal file
34
crates/app/src/main.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use gpui::{App, AppContext};
|
||||
use workspace::AppState;
|
||||
|
||||
mod assets;
|
||||
|
||||
fn init(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
|
||||
workspace::init(app_state.clone(), cx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let app_state = Arc::new(AppState {});
|
||||
|
||||
let app = App::new().with_assets(Assets);
|
||||
|
||||
app.run(move |cx| {
|
||||
AppState::set_global(Arc::downgrade(&app_state), cx);
|
||||
|
||||
if let Err(e) = init(app_state.clone(), cx) {
|
||||
log::error!("{}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
workspace::open_new(app_state.clone(), cx, |workspace, cx| {
|
||||
// do something
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
7
crates/ui/Cargo.toml
Normal file
7
crates/ui/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
319
crates/ui/src/button.rs
Normal file
319
crates/ui/src/button.rs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, rems, rgb, ClickEvent, DefiniteLength, Div, ElementId, Hsla,
|
||||
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement as _, Styled, WindowContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
colors::Color,
|
||||
disableable::{Clickable, Disableable, Selectable},
|
||||
label::Label,
|
||||
preview::Preview,
|
||||
HlsaExt as _,
|
||||
};
|
||||
|
||||
pub enum ButtonRounded {
|
||||
None,
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
pub enum ButtonSize {
|
||||
Small,
|
||||
Medium,
|
||||
}
|
||||
|
||||
pub enum ButtonStyle {
|
||||
Primary,
|
||||
Secondary,
|
||||
Danger,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Button {
|
||||
pub base: Div,
|
||||
id: ElementId,
|
||||
label: SharedString,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
width: Option<DefiniteLength>,
|
||||
height: Option<DefiniteLength>,
|
||||
style: ButtonStyle,
|
||||
rounded: ButtonRounded,
|
||||
size: ButtonSize,
|
||||
tooltip: Option<SharedString>,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: div(),
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
style: ButtonStyle::Secondary,
|
||||
width: None,
|
||||
height: None,
|
||||
rounded: ButtonRounded::Medium,
|
||||
size: ButtonSize::Medium,
|
||||
tooltip: None,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
|
||||
self.width = Some(width.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, height: impl Into<DefiniteLength>) -> Self {
|
||||
self.height = Some(height.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rounded(mut self, rounded: ButtonRounded) -> Self {
|
||||
self.rounded = rounded;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ButtonSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
|
||||
self.tooltip = Some(tooltip.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: ButtonStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Button {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Button {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for Button {
|
||||
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Button {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let style = self.style;
|
||||
let normal_style = style.normal(cx);
|
||||
|
||||
self.base
|
||||
.id(self.id)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
Label::new(self.label)
|
||||
.color(style.text_color())
|
||||
.map(|this| match self.size {
|
||||
ButtonSize::Small => this.text_sm(),
|
||||
ButtonSize::Medium => this.text_base(),
|
||||
}),
|
||||
)
|
||||
.map(|this| match self.size {
|
||||
ButtonSize::Small => this.px_3().py_2().h_7(),
|
||||
ButtonSize::Medium => this.px_4().py_2().h_10(),
|
||||
})
|
||||
.map(|this| match self.rounded {
|
||||
ButtonRounded::Small => this.rounded_sm(),
|
||||
ButtonRounded::Medium => this.rounded_md(),
|
||||
ButtonRounded::Large => this.rounded_lg(),
|
||||
ButtonRounded::None => this.rounded_none(),
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| {
|
||||
let hover_style = style.hovered(cx);
|
||||
this.bg(hover_style.bg).border_color(hover_style.border)
|
||||
})
|
||||
.active(|this| {
|
||||
let active_style = style.active(cx);
|
||||
this.bg(active_style.bg).border_color(active_style.border)
|
||||
})
|
||||
})
|
||||
.when(self.disabled, |this| {
|
||||
let disabled_style = style.disabled(cx);
|
||||
this.cursor_not_allowed()
|
||||
.bg(disabled_style.bg)
|
||||
.border_color(disabled_style.border)
|
||||
})
|
||||
.border_1()
|
||||
.border_color(normal_style.border)
|
||||
.bg(normal_style.bg)
|
||||
}
|
||||
}
|
||||
|
||||
struct ButtonStyles {
|
||||
bg: Hsla,
|
||||
border: Hsla,
|
||||
fg: Hsla,
|
||||
}
|
||||
|
||||
impl ButtonStyle {
|
||||
fn bg_color(&self) -> Color {
|
||||
match self {
|
||||
ButtonStyle::Primary => Color::Primary,
|
||||
ButtonStyle::Secondary => Color::Secondary,
|
||||
ButtonStyle::Danger => Color::Destructive,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_color(&self) -> Color {
|
||||
match self {
|
||||
ButtonStyle::Primary => Color::PrimaryForeground,
|
||||
ButtonStyle::Secondary => Color::SecondaryForeground,
|
||||
ButtonStyle::Danger => Color::DestructiveForeground,
|
||||
}
|
||||
}
|
||||
|
||||
fn border_color(&self) -> Color {
|
||||
match self {
|
||||
ButtonStyle::Primary => Color::Primary,
|
||||
ButtonStyle::Secondary => Color::Secondary,
|
||||
ButtonStyle::Danger => Color::Destructive,
|
||||
}
|
||||
}
|
||||
|
||||
fn normal(&self, cx: &WindowContext) -> ButtonStyles {
|
||||
let bg = self.bg_color().color(cx);
|
||||
let border = self.border_color().color(cx);
|
||||
let fg = self.text_color().color(cx);
|
||||
|
||||
ButtonStyles { bg, border, fg }
|
||||
}
|
||||
|
||||
fn hovered(&self, cx: &WindowContext) -> ButtonStyles {
|
||||
let bg = self.bg_color().color(cx).lighten(0.05);
|
||||
let border = self.border_color().color(cx).lighten(0.05);
|
||||
let fg = self.text_color().color(cx);
|
||||
|
||||
ButtonStyles { bg, border, fg }
|
||||
}
|
||||
|
||||
fn active(&self, cx: &WindowContext) -> ButtonStyles {
|
||||
let bg = self.bg_color().color(cx).darken(0.05);
|
||||
let border = self.border_color().color(cx).darken(0.05);
|
||||
let fg = self.text_color().color(cx);
|
||||
|
||||
ButtonStyles { bg, border, fg }
|
||||
}
|
||||
|
||||
fn disabled(&self, cx: &WindowContext) -> ButtonStyles {
|
||||
let bg = self.bg_color().color(cx).grayscale();
|
||||
let border = self.border_color().color(cx).grayscale();
|
||||
let fg = self.text_color().color(cx).grayscale();
|
||||
|
||||
ButtonStyles { bg, border, fg }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ButtonPreview {}
|
||||
|
||||
impl ButtonPreview {
|
||||
fn on_click(ev: &ClickEvent, cx: &mut WindowContext) {
|
||||
println!("Button clicked! {:?}", ev);
|
||||
}
|
||||
}
|
||||
|
||||
impl Preview for ButtonPreview {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Button"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Displays a button or a component that looks like a button."
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ButtonPreview {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_start()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("button-1", "Primary Button")
|
||||
.style(ButtonStyle::Primary)
|
||||
.on_click(Self::on_click)
|
||||
.render(cx),
|
||||
)
|
||||
.child(
|
||||
Button::new("button-2", "Secondary Button")
|
||||
.style(ButtonStyle::Secondary)
|
||||
.on_click(Self::on_click)
|
||||
.render(cx),
|
||||
)
|
||||
.child(
|
||||
Button::new("button-4", "Danger Button")
|
||||
.style(ButtonStyle::Danger)
|
||||
.on_click(Self::on_click)
|
||||
.render(cx),
|
||||
)
|
||||
.child(
|
||||
Button::new("button-5", "Disabled Button")
|
||||
.style(ButtonStyle::Primary)
|
||||
.on_click(Self::on_click)
|
||||
.disabled(true)
|
||||
.render(cx),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("button-6", "Primary Button")
|
||||
.style(ButtonStyle::Primary)
|
||||
.size(ButtonSize::Small)
|
||||
.on_click(Self::on_click)
|
||||
.render(cx),
|
||||
)
|
||||
.child(
|
||||
Button::new("button-7", "Secondary Button")
|
||||
.style(ButtonStyle::Secondary)
|
||||
.size(ButtonSize::Small)
|
||||
.on_click(Self::on_click)
|
||||
.render(cx),
|
||||
)
|
||||
.child(
|
||||
Button::new("button-8", "Danger Button")
|
||||
.style(ButtonStyle::Danger)
|
||||
.size(ButtonSize::Small)
|
||||
.on_click(Self::on_click)
|
||||
.render(cx),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
99
crates/ui/src/colors.rs
Normal file
99
crates/ui/src/colors.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use gpui::{hsla, Hsla, WindowContext};
|
||||
|
||||
pub fn hls(h: f32, l: f32, s: f32) -> Hsla {
|
||||
hsla(h, l / 100., s / 100., 1.)
|
||||
}
|
||||
|
||||
/// Extension trait for `Hsla` to provide more color manipulation methods.
|
||||
pub trait HlsaExt {
|
||||
fn darken(&self, amount: f32) -> Hsla;
|
||||
fn lighten(&self, amount: f32) -> Hsla;
|
||||
}
|
||||
|
||||
impl HlsaExt for Hsla {
|
||||
/// Darken the color by a percentage.
|
||||
///
|
||||
/// `amount` value is 0.0 - 1.0
|
||||
fn darken(&self, amount: f32) -> Hsla {
|
||||
let l = self.l - (self.l * amount);
|
||||
hsla(self.h, l, self.s, self.a)
|
||||
}
|
||||
|
||||
/// Lighten the color by a percentage.
|
||||
///
|
||||
/// `amount` value is 0.0 - 1.0
|
||||
fn lighten(&self, amount: f32) -> Hsla {
|
||||
let l = self.l + (self.l * amount);
|
||||
hsla(self.h, l, self.s, self.a)
|
||||
}
|
||||
}
|
||||
|
||||
// .dark {
|
||||
// --background: 240 10% 3.9%;
|
||||
// --foreground: 0 0% 98%;
|
||||
// --card: 240 10% 3.9%;
|
||||
// --card-foreground: 0 0% 98%;
|
||||
// --popover: 240 10% 3.9%;
|
||||
// --popover-foreground: 0 0% 98%;
|
||||
// --primary: 0 0% 98%;
|
||||
// --primary-foreground: 240 5.9% 10%;
|
||||
// --secondary: 240 3.7% 15.9%;
|
||||
// --secondary-foreground: 0 0% 98%;
|
||||
// --muted: 240 3.7% 15.9%;
|
||||
// --muted-foreground: 240 5% 64.9%;
|
||||
// --accent: 240 3.7% 15.9%;
|
||||
// --accent-foreground: 0 0% 98%;
|
||||
// --destructive: 0 62.8% 30.6%;
|
||||
// --destructive-foreground: 0 85.7% 97.3%;
|
||||
// --border: 240 3.7% 15.9%;
|
||||
// --input: 240 3.7% 15.9%;
|
||||
// --ring: 240 4.9% 83.9%;
|
||||
// }
|
||||
|
||||
pub enum Color {
|
||||
Background,
|
||||
Foreground,
|
||||
Card,
|
||||
CardForeground,
|
||||
Popover,
|
||||
PopoverForeground,
|
||||
Primary,
|
||||
PrimaryForeground,
|
||||
Secondary,
|
||||
SecondaryForeground,
|
||||
Muted,
|
||||
MutedForeground,
|
||||
Accent,
|
||||
AccentForeground,
|
||||
Destructive,
|
||||
DestructiveForeground,
|
||||
Border,
|
||||
Input,
|
||||
Ring,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn color(&self, _cx: &WindowContext) -> Hsla {
|
||||
match self {
|
||||
Color::Background => hls(240., 10., 3.9),
|
||||
Color::Foreground => hls(0., 0., 98.),
|
||||
Color::Card => hls(240., 10., 3.9),
|
||||
Color::CardForeground => hls(0., 0., 98.),
|
||||
Color::Popover => hls(240., 10., 3.9),
|
||||
Color::PopoverForeground => hls(0., 0., 98.),
|
||||
Color::Primary => hls(0., 0., 98.),
|
||||
Color::PrimaryForeground => hls(240., 5.9, 10.),
|
||||
Color::Secondary => hls(240., 3.7, 15.9),
|
||||
Color::SecondaryForeground => hls(0., 0., 98.),
|
||||
Color::Muted => hls(240., 3.7, 15.9),
|
||||
Color::MutedForeground => hls(240., 5., 64.9),
|
||||
Color::Accent => hls(240., 3.7, 15.9),
|
||||
Color::AccentForeground => hls(0., 0., 98.),
|
||||
Color::Destructive => hls(0., 62.8, 30.6),
|
||||
Color::DestructiveForeground => hls(0., 85.7, 97.3),
|
||||
Color::Border => hls(240., 3.7, 15.9),
|
||||
Color::Input => hls(240., 3.7, 15.9),
|
||||
Color::Ring => hls(240., 4.9, 83.9),
|
||||
}
|
||||
}
|
||||
}
|
||||
13
crates/ui/src/disableable.rs
Normal file
13
crates/ui/src/disableable.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use gpui::{ClickEvent, WindowContext};
|
||||
|
||||
pub trait Disableable {
|
||||
fn disabled(self, disabled: bool) -> Self;
|
||||
}
|
||||
|
||||
pub trait Selectable {
|
||||
fn selected(self, selected: bool) -> Self;
|
||||
}
|
||||
|
||||
pub trait Clickable {
|
||||
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
|
||||
}
|
||||
70
crates/ui/src/label.rs
Normal file
70
crates/ui/src/label.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, AbsoluteLength, DefiniteLength, Div, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, Style, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
|
||||
use crate::colors::Color;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Label {
|
||||
base: Div,
|
||||
label: SharedString,
|
||||
color: Color,
|
||||
multiple_lines: bool,
|
||||
line_height: Option<DefiniteLength>,
|
||||
text_size: Option<AbsoluteLength>,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: div(),
|
||||
label: label.into(),
|
||||
multiple_lines: false,
|
||||
color: Color::Foreground,
|
||||
line_height: None,
|
||||
text_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn multiple_lines(mut self) -> Self {
|
||||
self.multiple_lines = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Label {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let label_text = if !self.multiple_lines {
|
||||
SharedString::from(self.label.replace('\n', ""))
|
||||
} else {
|
||||
self.label
|
||||
};
|
||||
|
||||
self.base
|
||||
.child(label_text)
|
||||
.text_color(self.color.color(cx))
|
||||
.map(|this| {
|
||||
if let Some(text_size) = self.text_size {
|
||||
this.text_size(text_size)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.map(|this| match self.line_height {
|
||||
Some(line_height) => this.line_height(line_height),
|
||||
None => this,
|
||||
})
|
||||
}
|
||||
}
|
||||
7
crates/ui/src/lib.rs
Normal file
7
crates/ui/src/lib.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub mod button;
|
||||
mod colors;
|
||||
pub mod disableable;
|
||||
pub mod label;
|
||||
pub mod preview;
|
||||
|
||||
pub use colors::*;
|
||||
55
crates/ui/src/preview.rs
Normal file
55
crates/ui/src/preview.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use gpui::{div, px, IntoElement, ParentElement as _, RenderOnce, Styled as _};
|
||||
|
||||
use crate::{button::ButtonPreview, label::Label};
|
||||
|
||||
pub trait Preview {
|
||||
fn name(&self) -> &'static str;
|
||||
fn description(&self) -> &'static str;
|
||||
fn new() -> Self;
|
||||
}
|
||||
|
||||
enum PreviewStory {
|
||||
Button,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PreviewBox {
|
||||
active_preview: PreviewStory,
|
||||
}
|
||||
|
||||
impl PreviewBox {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_preview: PreviewStory::Button,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_preview(&self) -> impl Preview + IntoElement {
|
||||
match self.active_preview {
|
||||
PreviewStory::Button => ButtonPreview::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PreviewBox {
|
||||
fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement {
|
||||
let component_preview = self.active_preview();
|
||||
|
||||
let heading = Label::new(component_preview.name()).text_size(px(24.0));
|
||||
let description = Label::new(component_preview.description()).multiple_lines();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(heading)
|
||||
.child(description),
|
||||
)
|
||||
.child(component_preview)
|
||||
}
|
||||
}
|
||||
7
crates/util/Cargo.toml
Normal file
7
crates/util/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "util"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
log.workspace = true
|
||||
3
crates/util/src/lib.rs
Normal file
3
crates/util/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod log;
|
||||
|
||||
pub use log::*;
|
||||
180
crates/util/src/log.rs
Normal file
180
crates/util/src/log.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
use std::{
|
||||
future::Future,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug_panic {
|
||||
( $($fmt_arg:tt)* ) => {
|
||||
if cfg!(debug_assertions) {
|
||||
panic!( $($fmt_arg)* );
|
||||
} else {
|
||||
let backtrace = std::backtrace::Backtrace::capture();
|
||||
log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait ResultExt<E> {
|
||||
type Ok;
|
||||
|
||||
fn log_err(self) -> Option<Self::Ok>;
|
||||
/// Assert that this result should never be an error in development or tests.
|
||||
fn debug_assert_ok(self, reason: &str) -> Self;
|
||||
fn warn_on_err(self) -> Option<Self::Ok>;
|
||||
fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
|
||||
}
|
||||
|
||||
impl<T, E> ResultExt<E> for Result<T, E>
|
||||
where
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
type Ok = T;
|
||||
|
||||
#[track_caller]
|
||||
fn log_err(self) -> Option<T> {
|
||||
match self {
|
||||
Ok(value) => Some(value),
|
||||
Err(error) => {
|
||||
let caller = Location::caller();
|
||||
log::error!("{}:{}: {:?}", caller.file(), caller.line(), error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn debug_assert_ok(self, reason: &str) -> Self {
|
||||
if let Err(error) = &self {
|
||||
debug_panic!("{reason} - {error:?}");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn warn_on_err(self) -> Option<T> {
|
||||
match self {
|
||||
Ok(value) => Some(value),
|
||||
Err(error) => {
|
||||
log::warn!("{:?}", error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err
|
||||
fn inspect_error(self, func: impl FnOnce(&E)) -> Self {
|
||||
if let Err(err) = &self {
|
||||
func(err);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
|
||||
|
||||
impl<F, T, E> Future for LogErrorFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
type Output = Option<T>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let level = self.1;
|
||||
let location = self.2;
|
||||
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
|
||||
match inner.poll(cx) {
|
||||
Poll::Ready(output) => Poll::Ready(match output {
|
||||
Ok(output) => Some(output),
|
||||
Err(error) => {
|
||||
log::log!(
|
||||
level,
|
||||
"{}:{}: {:?}",
|
||||
location.file(),
|
||||
location.line(),
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
}),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnwrapFuture<F>(F);
|
||||
|
||||
impl<F, T, E> Future for UnwrapFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
|
||||
match inner.poll(cx) {
|
||||
Poll::Ready(result) => Poll::Ready(result.unwrap()),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TryFutureExt {
|
||||
fn log_err(self) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn warn_on_err(self) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
fn unwrap(self) -> UnwrapFuture<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl<F, T, E> TryFutureExt for F
|
||||
where
|
||||
F: Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
#[track_caller]
|
||||
fn log_err(self) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let location = Location::caller();
|
||||
LogErrorFuture(self, log::Level::Error, *location)
|
||||
}
|
||||
|
||||
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
LogErrorFuture(self, log::Level::Error, location)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn warn_on_err(self) -> LogErrorFuture<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let location = Location::caller();
|
||||
LogErrorFuture(self, log::Level::Warn, *location)
|
||||
}
|
||||
|
||||
fn unwrap(self) -> UnwrapFuture<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
UnwrapFuture(self)
|
||||
}
|
||||
}
|
||||
10
crates/workspace/Cargo.toml
Normal file
10
crates/workspace/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "workspace"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
ui.workspace = true
|
||||
anyhow.workspace = true
|
||||
util.workspace = true
|
||||
15
crates/workspace/src/app_state.rs
Normal file
15
crates/workspace/src/app_state.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use std::sync::{Arc, Weak};
|
||||
|
||||
use gpui::{AppContext, Global};
|
||||
|
||||
pub struct AppState {}
|
||||
|
||||
struct GlobalAppState(Weak<AppState>);
|
||||
|
||||
impl Global for GlobalAppState {}
|
||||
|
||||
impl AppState {
|
||||
pub fn set_global(app_state: Weak<AppState>, cx: &mut AppContext) {
|
||||
cx.set_global(GlobalAppState(app_state));
|
||||
}
|
||||
}
|
||||
1
crates/workspace/src/item.rs
Normal file
1
crates/workspace/src/item.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub trait ItemHandle: 'static + Send {}
|
||||
113
crates/workspace/src/lib.rs
Normal file
113
crates/workspace/src/lib.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use gpui::{prelude::FluentBuilder, *};
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
button::{Button, ButtonStyle},
|
||||
disableable::Clickable as _,
|
||||
preview::PreviewBox,
|
||||
Color,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
mod app_state;
|
||||
mod item;
|
||||
|
||||
pub use app_state::AppState;
|
||||
|
||||
pub struct Workspace {
|
||||
weak_self: WeakView<Self>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(
|
||||
app_state: Arc<AppState>,
|
||||
parent: Option<WeakView<Self>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let weak_handle = cx.view().downgrade();
|
||||
|
||||
let workspace = Workspace {
|
||||
weak_self: weak_handle.clone(),
|
||||
};
|
||||
|
||||
workspace
|
||||
}
|
||||
|
||||
pub fn new_local(
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<anyhow::Result<WindowHandle<Workspace>>> {
|
||||
let window_bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(window_bounds)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let window = cx.open_window(options, {
|
||||
let app_state = app_state.clone();
|
||||
move |cx| cx.new_view(|cx| Workspace::new(app_state.clone(), None, cx))
|
||||
})?;
|
||||
|
||||
window
|
||||
.update(&mut cx, |_, cx| {
|
||||
cx.activate_window();
|
||||
cx.set_window_title("GPUI App");
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Ok(window)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
actions!(workspace, [Open]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
cx.on_action({
|
||||
let app_state = app_state.clone();
|
||||
move |action: &Open, cx: &mut AppContext| {}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_new(
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut AppContext,
|
||||
init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
|
||||
) -> Task<()> {
|
||||
let task = Workspace::new_local(app_state, cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
if let Some(workspace) = task.await.log_err() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| init(workspace, cx))
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn render_ok_button(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Button::new("ok-button", "OK")
|
||||
.style(ButtonStyle::Primary)
|
||||
.on_click(|_, cx| {
|
||||
// todo
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Workspace {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let preview_box = PreviewBox::new();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.p_4()
|
||||
.bg(Color::Background.color(cx))
|
||||
.child(div().flex().py_3().gap_2().child(preview_box))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue