Start reorganizing examples (#10)

* Update crate name

* wip

* reduce to just the gpui crate

* Fix misisng example images

* Delete ownership_post.rs

* Update Cargo.toml

* Update README.md

* bump to 0.3.3

* organize

* organize

* Add more learn examples

* Delete _ownership_and_data_flow.rs

* wip learn examples

* Fix colors

* tidy

* Move _template up

* add animation example

* Update examples

* remove legacy examples from toml

* Delete examples.toml
This commit is contained in:
Nate Butler
2025-12-28 23:14:33 -05:00
committed by GitHub
parent 6a74dbe3f4
commit 18aeb44a54
295 changed files with 5174 additions and 23483 deletions
+473
View File
@@ -0,0 +1,473 @@
use std::{ops::Range, rc::Rc, time::Duration};
use gpui::{
App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
Render, SharedString, UniformListScrollHandle, Window, WindowBounds, WindowOptions, canvas,
div, point, prelude::*, px, rgb, size, uniform_list,
};
const TOTAL_ITEMS: usize = 10000;
const SCROLLBAR_THUMB_WIDTH: Pixels = px(8.);
const SCROLLBAR_THUMB_HEIGHT: Pixels = px(100.);
pub struct Quote {
name: SharedString,
symbol: SharedString,
last_done: f64,
prev_close: f64,
open: f64,
high: f64,
low: f64,
timestamp: Duration,
volume: i64,
turnover: f64,
ttm: f64,
market_cap: f64,
float_cap: f64,
shares: f64,
pb: f64,
pe: f64,
eps: f64,
dividend: f64,
dividend_yield: f64,
dividend_per_share: f64,
dividend_date: SharedString,
dividend_payment: f64,
}
impl Quote {
pub fn random() -> Self {
use rand::Rng;
let mut rng = rand::rng();
// simulate a base price in a realistic range
let prev_close = rng.random_range(100.0..200.0);
let change = rng.random_range(-5.0..5.0);
let last_done = prev_close + change;
let open = prev_close + rng.random_range(-3.0..3.0);
let high = (prev_close + rng.random_range::<f64, _>(0.0..10.0)).max(open);
let low = (prev_close - rng.random_range::<f64, _>(0.0..10.0)).min(open);
let timestamp = Duration::from_secs(rng.random_range(0..86400));
let volume = rng.random_range(1_000_000..100_000_000);
let turnover = last_done * volume as f64;
let symbol = {
let mut ticker = String::new();
if rng.random_bool(0.5) {
ticker.push_str(&format!(
"{:03}.{}",
rng.random_range(100..1000),
rng.random_range(0..10)
));
} else {
ticker.push_str(&format!(
"{}{}",
rng.random_range('A'..='Z'),
rng.random_range('A'..='Z')
));
}
ticker.push_str(&format!(".{}", rng.random_range('A'..='Z')));
ticker
};
let name = format!(
"{} {} - #{}",
symbol,
rng.random_range(1..100),
rng.random_range(10000..100000)
);
let ttm = rng.random_range(0.0..10.0);
let market_cap = rng.random_range(1_000_000.0..10_000_000.0);
let float_cap = market_cap + rng.random_range(1_000.0..10_000.0);
let shares = rng.random_range(100.0..1000.0);
let pb = market_cap / shares;
let pe = market_cap / shares;
let eps = market_cap / shares;
let dividend = rng.random_range(0.0..10.0);
let dividend_yield = rng.random_range(0.0..10.0);
let dividend_per_share = rng.random_range(0.0..10.0);
let dividend_date = SharedString::new(format!(
"{}-{}-{}",
rng.random_range(2000..2023),
rng.random_range(1..12),
rng.random_range(1..28)
));
let dividend_payment = rng.random_range(0.0..10.0);
Self {
name: name.into(),
symbol: symbol.into(),
last_done,
prev_close,
open,
high,
low,
timestamp,
volume,
turnover,
pb,
pe,
eps,
ttm,
market_cap,
float_cap,
shares,
dividend,
dividend_yield,
dividend_per_share,
dividend_date,
dividend_payment,
}
}
fn change(&self) -> f64 {
(self.last_done - self.prev_close) / self.prev_close * 100.0
}
fn change_color(&self) -> gpui::Hsla {
if self.change() > 0.0 {
gpui::green()
} else {
gpui::red()
}
}
fn turnover_ratio(&self) -> f64 {
self.volume as f64 / self.turnover * 100.0
}
}
#[derive(IntoElement)]
struct TableRow {
ix: usize,
quote: Rc<Quote>,
}
impl TableRow {
fn new(ix: usize, quote: Rc<Quote>) -> Self {
Self { ix, quote }
}
fn render_cell(&self, key: &str, width: Pixels, color: gpui::Hsla) -> impl IntoElement {
div()
.whitespace_nowrap()
.truncate()
.w(width)
.px_1()
.child(match key {
"id" => div().child(format!("{}", self.ix)),
"symbol" => div().child(self.quote.symbol.clone()),
"name" => div().child(self.quote.name.clone()),
"last_done" => div()
.text_color(color)
.child(format!("{:.3}", self.quote.last_done)),
"prev_close" => div()
.text_color(color)
.child(format!("{:.3}", self.quote.prev_close)),
"change" => div()
.text_color(color)
.child(format!("{:.2}%", self.quote.change())),
"timestamp" => div()
.text_color(color)
.child(format!("{:?}", self.quote.timestamp.as_secs())),
"open" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.open)),
"low" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.low)),
"high" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.high)),
"ttm" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.ttm)),
"eps" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.eps)),
"market_cap" => {
div().child(format!("{:.2} M", self.quote.market_cap / 1_000_000.0))
}
"float_cap" => div().child(format!("{:.2} M", self.quote.float_cap / 1_000_000.0)),
"turnover" => div().child(format!("{:.2} M", self.quote.turnover / 1_000_000.0)),
"volume" => div().child(format!("{:.2} M", self.quote.volume as f64 / 1_000_000.0)),
"turnover_ratio" => div().child(format!("{:.2}%", self.quote.turnover_ratio())),
"pe" => div().child(format!("{:.2}", self.quote.pe)),
"pb" => div().child(format!("{:.2}", self.quote.pb)),
"shares" => div().child(format!("{:.2}", self.quote.shares)),
"dividend" => div().child(format!("{:.2}", self.quote.dividend)),
"yield" => div().child(format!("{:.2}%", self.quote.dividend_yield)),
"dividend_per_share" => {
div().child(format!("{:.2}", self.quote.dividend_per_share))
}
"dividend_date" => div().child(format!("{}", self.quote.dividend_date)),
"dividend_payment" => div().child(format!("{:.2}", self.quote.dividend_payment)),
_ => div().child("--"),
})
}
}
const FIELDS: [(&str, f32); 24] = [
("id", 64.),
("symbol", 64.),
("name", 180.),
("last_done", 80.),
("prev_close", 80.),
("open", 80.),
("low", 80.),
("high", 80.),
("ttm", 50.),
("market_cap", 96.),
("float_cap", 96.),
("turnover", 120.),
("volume", 100.),
("turnover_ratio", 96.),
("pe", 64.),
("pb", 64.),
("eps", 64.),
("shares", 96.),
("dividend", 64.),
("yield", 64.),
("dividend_per_share", 64.),
("dividend_date", 96.),
("dividend_payment", 64.),
("timestamp", 120.),
];
impl RenderOnce for TableRow {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let color = self.quote.change_color();
div()
.flex()
.flex_row()
.border_b_1()
.border_color(rgb(0xE0E0E0))
.bg(if self.ix.is_multiple_of(2) {
rgb(0xFFFFFF)
} else {
rgb(0xFAFAFA)
})
.py_0p5()
.px_2()
.children(FIELDS.map(|(key, width)| self.render_cell(key, px(width), color)))
}
}
struct DataTable {
/// Use `Rc` to share the same quote data across multiple items, avoid cloning.
quotes: Vec<Rc<Quote>>,
visible_range: Range<usize>,
scroll_handle: UniformListScrollHandle,
/// The position in thumb bounds when dragging start mouse down.
drag_position: Option<Point<Pixels>>,
}
impl DataTable {
fn new() -> Self {
Self {
quotes: Vec::new(),
visible_range: 0..0,
scroll_handle: UniformListScrollHandle::new(),
drag_position: None,
}
}
fn generate(&mut self) {
self.quotes = (0..TOTAL_ITEMS).map(|_| Rc::new(Quote::random())).collect();
}
fn table_bounds(&self) -> Bounds<Pixels> {
self.scroll_handle.0.borrow().base_handle.bounds()
}
fn scroll_top(&self) -> Pixels {
self.scroll_handle.0.borrow().base_handle.offset().y
}
fn scroll_height(&self) -> Pixels {
self.scroll_handle
.0
.borrow()
.last_item_size
.unwrap_or_default()
.contents
.height
}
fn render_scrollbar(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let scroll_height = self.scroll_height();
let table_bounds = self.table_bounds();
let table_height = table_bounds.size.height;
if table_height == px(0.) {
return div().id("scrollbar");
}
let percentage = -self.scroll_top() / scroll_height;
let offset_top = (table_height * percentage).clamp(
px(4.),
(table_height - SCROLLBAR_THUMB_HEIGHT - px(4.)).max(px(4.)),
);
let entity = cx.entity();
let scroll_handle = self.scroll_handle.0.borrow().base_handle.clone();
div()
.id("scrollbar")
.absolute()
.top(offset_top)
.right_1()
.h(SCROLLBAR_THUMB_HEIGHT)
.w(SCROLLBAR_THUMB_WIDTH)
.bg(rgb(0xC0C0C0))
.hover(|this| this.bg(rgb(0xA0A0A0)))
.rounded_lg()
.child(
canvas(
|_, _, _| (),
move |thumb_bounds, _, window, _| {
window.on_mouse_event({
let entity = entity.clone();
move |ev: &MouseDownEvent, _, _, cx| {
if !thumb_bounds.contains(&ev.position) {
return;
}
entity.update(cx, |this, _| {
this.drag_position = Some(
ev.position - thumb_bounds.origin - table_bounds.origin,
);
})
}
});
window.on_mouse_event({
let entity = entity.clone();
move |_: &MouseUpEvent, _, _, cx| {
entity.update(cx, |this, _| {
this.drag_position = None;
})
}
});
window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| {
if !ev.dragging() {
return;
}
let Some(drag_pos) = entity.read(cx).drag_position else {
return;
};
let inside_offset = drag_pos.y;
let percentage = ((ev.position.y - table_bounds.origin.y
+ inside_offset)
/ (table_bounds.size.height))
.clamp(0., 1.);
let offset_y = ((scroll_height - table_bounds.size.height)
* percentage)
.clamp(px(0.), scroll_height - SCROLLBAR_THUMB_HEIGHT);
scroll_handle.set_offset(point(px(0.), -offset_y));
cx.notify(entity.entity_id());
})
},
)
.size_full(),
)
}
}
impl Render for DataTable {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.bg(gpui::white())
.text_sm()
.size_full()
.p_4()
.gap_2()
.flex()
.flex_col()
.child(format!(
"Total {} items, visible range: {:?}",
self.quotes.len(),
self.visible_range
))
.child(
div()
.flex()
.flex_col()
.flex_1()
.overflow_hidden()
.border_1()
.border_color(rgb(0xE0E0E0))
.rounded_sm()
.child(
div()
.flex()
.flex_row()
.w_full()
.overflow_hidden()
.border_b_1()
.border_color(rgb(0xE0E0E0))
.text_color(rgb(0x555555))
.bg(rgb(0xF0F0F0))
.py_1()
.px_2()
.text_xs()
.children(FIELDS.map(|(key, width)| {
div()
.whitespace_nowrap()
.flex_shrink_0()
.truncate()
.px_1()
.w(px(width))
.child(key.replace("_", " ").to_uppercase())
})),
)
.child(
div()
.relative()
.size_full()
.child(
uniform_list(
"items",
self.quotes.len(),
cx.processor(move |this, range: Range<usize>, _, _| {
this.visible_range = range.clone();
let mut items = Vec::with_capacity(range.end - range.start);
for i in range {
if let Some(quote) = this.quotes.get(i) {
items.push(TableRow::new(i, quote.clone()));
}
}
items
}),
)
.size_full()
.track_scroll(&self.scroll_handle),
)
.child(self.render_scrollbar(window, cx)),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.open_window(
WindowOptions {
focus: true,
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(1280.0), px(1000.0)),
cx,
))),
..Default::default()
},
|_, cx| {
cx.new(|_| {
let mut table = DataTable::new();
table.generate();
table
})
},
)
.unwrap();
cx.activate(true);
});
}
+92
View File
@@ -0,0 +1,92 @@
use gpui::{
Application, Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render,
TitlebarOptions, Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop,
linear_gradient, point, prelude::*, px, rgb, size,
};
const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0);
const DEFAULT_WINDOW_HEIGHT: Pixels = px(768.0);
struct PaintingViewer {
default_lines: Vec<(Path<Pixels>, Background)>,
_painting: bool,
}
impl PaintingViewer {
fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
let mut lines = vec![];
// draw a lightening bolt ⚡
for _ in 0..2000 {
// draw a ⭐
let mut builder = PathBuilder::fill();
builder.move_to(point(px(350.), px(100.)));
builder.line_to(point(px(370.), px(160.)));
builder.line_to(point(px(430.), px(160.)));
builder.line_to(point(px(380.), px(200.)));
builder.line_to(point(px(400.), px(260.)));
builder.line_to(point(px(350.), px(220.)));
builder.line_to(point(px(300.), px(260.)));
builder.line_to(point(px(320.), px(200.)));
builder.line_to(point(px(270.), px(160.)));
builder.line_to(point(px(330.), px(160.)));
builder.line_to(point(px(350.), px(100.)));
let path = builder.build().unwrap();
lines.push((
path,
linear_gradient(
180.,
linear_color_stop(rgb(0xFACC15), 0.7),
linear_color_stop(rgb(0xD56D0C), 1.),
)
.color_space(ColorSpace::Oklab),
));
}
Self {
default_lines: lines,
_painting: false,
}
}
}
impl Render for PaintingViewer {
fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
window.request_animation_frame();
let lines = self.default_lines.clone();
div().size_full().child(
canvas(
move |_, _, _| {},
move |_, _, window, _| {
for (path, color) in lines {
window.paint_path(path, color);
}
},
)
.size_full(),
)
}
}
fn main() {
Application::new().run(|cx| {
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some("Vulkan".into()),
..Default::default()
}),
focus: true,
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT),
cx,
))),
..Default::default()
},
|window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
)
.unwrap();
cx.activate(true);
});
}
+115
View File
@@ -0,0 +1,115 @@
use gpui::{
App, AppContext, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div,
linear_color_stop, linear_gradient, pattern_slash, prelude::*, px, rgb, size,
};
struct PatternExample;
impl Render for PatternExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.bg(rgb(0xffffff))
.size(px(600.0))
.justify_center()
.items_center()
.shadow_lg()
.text_xl()
.text_color(rgb(0x000000))
.child("Pattern Example")
.child(
div()
.flex()
.flex_col()
.border_1()
.border_color(gpui::blue())
.child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(
gpui::red(),
18.0 / 4.0,
18.0 / 4.0,
)))
.child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(
gpui::red(),
18.0 / 4.0,
18.0 / 4.0,
)))
.child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(
gpui::red(),
18.0 / 4.0,
18.0 / 4.0,
)))
.child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(
gpui::red(),
18.0 / 4.0,
18.0 / 2.0,
))),
)
.child(
div()
.flex()
.flex_col()
.border_1()
.border_color(gpui::blue())
.bg(gpui::green().opacity(0.16))
.child("Elements the same height should align")
.child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash(
gpui::red(),
56.0 / 6.0,
56.0 / 6.0,
)))
.child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash(
gpui::green(),
56.0 / 6.0,
56.0 / 6.0,
)))
.child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash(
gpui::blue(),
56.0 / 6.0,
56.0 / 6.0,
)))
.child(div().w(px(256.0)).h(px(26.0)).bg(pattern_slash(
gpui::yellow(),
56.0 / 6.0,
56.0 / 6.0,
))),
)
.child(
div()
.border_1()
.border_color(gpui::blue())
.w(px(240.0))
.h(px(40.0))
.bg(gpui::red()),
)
.child(
div()
.border_1()
.border_color(gpui::blue())
.w(px(240.0))
.h(px(40.0))
.bg(linear_gradient(
45.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::blue(), 1.),
)),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_window, cx| cx.new(|_cx| PatternExample),
)
.unwrap();
cx.activate(true);
});
}
+585
View File
@@ -0,0 +1,585 @@
use gpui::{
App, Application, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds,
WindowOptions, div, hsla, point, prelude::*, px, relative, rgb, size,
};
struct Shadow {}
impl Shadow {
fn base() -> Div {
div()
.size_16()
.bg(rgb(0xffffff))
.rounded_full()
.border_1()
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
}
fn square() -> Div {
div()
.size_16()
.bg(rgb(0xffffff))
.border_1()
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
}
fn rounded_small() -> Div {
div()
.size_16()
.bg(rgb(0xffffff))
.rounded(px(4.))
.border_1()
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
}
fn rounded_medium() -> Div {
div()
.size_16()
.bg(rgb(0xffffff))
.rounded(px(8.))
.border_1()
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
}
fn rounded_large() -> Div {
div()
.size_16()
.bg(rgb(0xffffff))
.rounded(px(12.))
.border_1()
.border_color(hsla(0.0, 0.0, 0.0, 0.1))
}
}
fn example(label: impl Into<SharedString>, example: impl IntoElement) -> impl IntoElement {
let label = label.into();
div()
.flex()
.flex_col()
.justify_center()
.items_center()
.w(relative(1. / 6.))
.border_r_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.child(
div()
.flex()
.items_center()
.justify_center()
.flex_1()
.py_12()
.child(example),
)
.child(
div()
.w_full()
.border_t_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.p_1()
.flex()
.items_center()
.child(label),
)
}
impl Render for Shadow {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("shadow-example")
.overflow_y_scroll()
.bg(rgb(0xffffff))
.size_full()
.text_xs()
.child(div().flex().flex_col().w_full().children(vec![
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.flex_row()
.children(vec![
example(
"Square",
Shadow::square()
.shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Rounded 4",
Shadow::rounded_small()
.shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Rounded 8",
Shadow::rounded_medium()
.shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Rounded 16",
Shadow::rounded_large()
.shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Circle",
Shadow::base()
.shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
]),
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.w_full()
.children(vec![
example("None", Shadow::base()),
// 2Xsmall shadow
example("2X Small", Shadow::base().shadow_2xs()),
// Xsmall shadow
example("Extra Small", Shadow::base().shadow_xs()),
// Small shadow
example("Small", Shadow::base().shadow_sm()),
// Medium shadow
example("Medium", Shadow::base().shadow_md()),
// Large shadow
example("Large", Shadow::base().shadow_lg()),
example("Extra Large", Shadow::base().shadow_xl()),
example("2X Large", Shadow::base().shadow_2xl()),
]),
// Horizontal list of increasing blur radii
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.children(vec![
example(
"Blur 0",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(0.),
spread_radius: px(0.),
}]),
),
example(
"Blur 2",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(2.),
spread_radius: px(0.),
}]),
),
example(
"Blur 4",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(4.),
spread_radius: px(0.),
}]),
),
example(
"Blur 8",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Blur 16",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(16.),
spread_radius: px(0.),
}]),
),
]),
// Horizontal list of increasing spread radii
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.children(vec![
example(
"Spread 0",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Spread 2",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(2.),
}]),
),
example(
"Spread 4",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(4.),
}]),
),
example(
"Spread 8",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(8.),
}]),
),
example(
"Spread 16",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(16.),
}]),
),
]),
// Square spread examples
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.children(vec![
example(
"Square Spread 0",
Shadow::square().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Square Spread 8",
Shadow::square().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(8.),
}]),
),
example(
"Square Spread 16",
Shadow::square().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(16.),
}]),
),
]),
// Rounded large spread examples
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.children(vec![
example(
"Rounded Large Spread 0",
Shadow::rounded_large().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Rounded Large Spread 8",
Shadow::rounded_large().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(8.),
}]),
),
example(
"Rounded Large Spread 16",
Shadow::rounded_large().shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(16.),
}]),
),
]),
// Directional shadows
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.children(vec![
example(
"Left",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(-8.), px(0.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Right",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(8.), px(0.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Top",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(-8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Bottom",
Shadow::base().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
]),
// Square directional shadows
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.children(vec![
example(
"Square Left",
Shadow::square().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(-8.), px(0.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Square Right",
Shadow::square().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(8.), px(0.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Square Top",
Shadow::square().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(-8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Square Bottom",
Shadow::square().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
]),
// Rounded large directional shadows
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.children(vec![
example(
"Rounded Large Left",
Shadow::rounded_large().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(-8.), px(0.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Rounded Large Right",
Shadow::rounded_large().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(8.), px(0.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Rounded Large Top",
Shadow::rounded_large().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(-8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
example(
"Rounded Large Bottom",
Shadow::rounded_large().shadow(vec![BoxShadow {
color: hsla(0.0, 0.5, 0.5, 0.3),
offset: point(px(0.), px(8.)),
blur_radius: px(8.),
spread_radius: px(0.),
}]),
),
]),
// Multiple shadows for different shapes
div()
.border_b_1()
.border_color(hsla(0.0, 0.0, 0.0, 1.0))
.flex()
.children(vec![
example(
"Circle Multiple",
Shadow::base().shadow(vec![
BoxShadow {
color: hsla(0.0 / 360., 1.0, 0.5, 0.3), // Red
offset: point(px(0.), px(-12.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(60.0 / 360., 1.0, 0.5, 0.3), // Yellow
offset: point(px(12.), px(0.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(120.0 / 360., 1.0, 0.5, 0.3), // Green
offset: point(px(0.), px(12.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(240.0 / 360., 1.0, 0.5, 0.3), // Blue
offset: point(px(-12.), px(0.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
]),
),
example(
"Square Multiple",
Shadow::square().shadow(vec![
BoxShadow {
color: hsla(0.0 / 360., 1.0, 0.5, 0.3), // Red
offset: point(px(0.), px(-12.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(60.0 / 360., 1.0, 0.5, 0.3), // Yellow
offset: point(px(12.), px(0.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(120.0 / 360., 1.0, 0.5, 0.3), // Green
offset: point(px(0.), px(12.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(240.0 / 360., 1.0, 0.5, 0.3), // Blue
offset: point(px(-12.), px(0.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
]),
),
example(
"Rounded Large Multiple",
Shadow::rounded_large().shadow(vec![
BoxShadow {
color: hsla(0.0 / 360., 1.0, 0.5, 0.3), // Red
offset: point(px(0.), px(-12.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(60.0 / 360., 1.0, 0.5, 0.3), // Yellow
offset: point(px(12.), px(0.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(120.0 / 360., 1.0, 0.5, 0.3), // Green
offset: point(px(0.), px(12.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
BoxShadow {
color: hsla(240.0 / 360., 1.0, 0.5, 0.3), // Blue
offset: point(px(-12.), px(0.)),
blur_radius: px(8.),
spread_radius: px(2.),
},
]),
),
]),
]))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Shadow {}),
)
.unwrap();
cx.activate(true);
});
}
+278
View File
@@ -0,0 +1,278 @@
//! Animation Example
//!
//! This example demonstrates animation capabilities in GPUI:
//!
//! 1. Basic animations with `with_animation`
//! 2. Easing functions - ease_in_out, bounce, linear
//! 3. Transformations - rotate, scale, translate
//! 4. Repeating and duration controls
#[path = "../prelude.rs"]
mod example_prelude;
use std::time::Duration;
use anyhow::Result;
use gpui::{
Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Colors, Context, Hsla,
SharedString, Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out,
linear, percentage, prelude::*, px, size as gpui_size, svg,
};
struct Assets {}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
std::fs::read(path)
.map(Into::into)
.map_err(Into::into)
.map(Some)
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
Ok(std::fs::read_dir(path)?
.filter_map(|entry| {
Some(SharedString::from(
entry.ok()?.path().to_string_lossy().into_owned(),
))
})
.collect::<Vec<_>>())
}
}
const ARROW_CIRCLE_SVG: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/legacy/image/arrow_circle.svg"
);
struct AnimationExample;
impl Render for AnimationExample {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
div()
.id("main")
.size_full()
.p_6()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_6()
.max_w(px(600.))
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Animation Patterns"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Animations, easing, and transformations in GPUI"),
),
)
.child(section(
&colors,
"Rotation Animation",
rotation_example(&colors),
))
.child(section(&colors, "Bounce Easing", bounce_example(&colors)))
.child(section(&colors, "Scale Animation", scale_example(&colors)))
.child(section(
&colors,
"Combined Animations",
combined_example(&colors),
)),
)
}
}
fn rotation_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let accent = colors.accent;
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Continuous rotation with ease_in_out easing"),
)
.child(
div().flex().items_center().justify_center().h_24().child(
svg()
.size_16()
.overflow_hidden()
.path(ARROW_CIRCLE_SVG)
.text_color(accent)
.with_animation(
"rotation",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(ease_in_out),
|svg, delta| {
svg.with_transformation(Transformation::rotate(percentage(delta)))
},
),
),
)
}
fn bounce_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let success = colors.success;
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Bouncing rotation with bounce(ease_in_out)"),
)
.child(
div().flex().items_center().justify_center().h_24().child(
svg()
.size_16()
.overflow_hidden()
.path(ARROW_CIRCLE_SVG)
.text_color(success)
.with_animation(
"bounce_rotation",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(bounce(ease_in_out)),
|svg, delta| {
svg.with_transformation(Transformation::rotate(percentage(delta)))
},
),
),
)
}
fn scale_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let warning = colors.warning;
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Scale pulsing with linear easing"),
)
.child(
div().flex().items_center().justify_center().h_24().child(
svg()
.size_16()
.overflow_hidden()
.path(ARROW_CIRCLE_SVG)
.text_color(warning)
.with_animation(
"scale",
Animation::new(Duration::from_millis(1500))
.repeat()
.with_easing(bounce(linear)),
|svg, delta| {
let scale = 0.8 + (delta * 0.4);
svg.with_transformation(Transformation::scale(gpui_size(scale, scale)))
},
),
),
)
}
fn combined_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let error = colors.error;
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Rotation + scale combined"),
)
.child(
div().flex().items_center().justify_center().h_24().child(
svg()
.size_16()
.overflow_hidden()
.path(ARROW_CIRCLE_SVG)
.text_color(error)
.with_animation(
"combined",
Animation::new(Duration::from_secs(3))
.repeat()
.with_easing(ease_in_out),
|svg, delta| {
let scale = 0.7 + (delta * 0.6);
svg.with_transformation(
Transformation::rotate(percentage(delta))
.with_scaling(gpui_size(scale, scale)),
)
},
),
),
)
}
fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
let surface: Hsla = colors.surface.into();
div()
.flex()
.flex_col()
.gap_2()
.p_4()
.bg(surface.opacity(0.5))
.rounded_lg()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child(title),
)
.child(content)
}
fn main() {
Application::new()
.with_assets(Assets {})
.run(|cx: &mut App| {
let bounds = Bounds::centered(None, gpui_size(px(500.), px(650.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| AnimationExample),
)
.expect("Failed to open window");
example_prelude::init_example(cx, "Animation");
});
}
+561
View File
@@ -0,0 +1,561 @@
//! Async Tasks Example
//!
//! This example demonstrates different async patterns in GPUI:
//!
//! 1. `cx.spawn` - Foreground tasks for UI updates
//! 2. `cx.background_spawn` - Background tasks for heavy computation
//! 3. Task management - Storing, canceling, and detaching tasks
//! 4. Progress updates - Communicating from background to UI
#[path = "../prelude.rs"]
mod example_prelude;
use std::time::Duration;
use gpui::{
App, Application, Bounds, Colors, Context, Entity, Render, Task, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
};
// Example 1: Simple Foreground Task
//
// `cx.spawn` runs an async closure on the foreground thread.
// Use it when you need to perform async work that updates UI.
struct ForegroundTaskDemo {
message: String,
is_loading: bool,
}
impl ForegroundTaskDemo {
fn new() -> Self {
Self {
message: "Click to start a foreground task".into(),
is_loading: false,
}
}
fn start_task(&mut self, cx: &mut Context<Self>) {
self.is_loading = true;
self.message = "Loading...".into();
cx.notify();
cx.spawn(async move |this, cx| {
cx.background_spawn(async {
std::thread::sleep(Duration::from_secs(1));
})
.await;
this.update(cx, |this, cx| {
this.message = "Task completed!".into();
this.is_loading = false;
cx.notify();
})
.ok();
})
.detach();
}
}
// Example 2: Background Task with Progress
//
// `cx.background_spawn` runs work off the UI thread.
// Use it for heavy computation that shouldn't block the UI.
struct BackgroundTaskDemo {
progress: u32,
result: Option<u64>,
is_computing: bool,
}
impl BackgroundTaskDemo {
fn new() -> Self {
Self {
progress: 0,
result: None,
is_computing: false,
}
}
fn start_computation(&mut self, cx: &mut Context<Self>) {
self.is_computing = true;
self.progress = 0;
self.result = None;
cx.notify();
cx.spawn(async move |this, cx| {
for i in 0..100 {
let computation = cx.background_spawn(async move {
std::thread::sleep(Duration::from_millis(10));
(i + 1) as u64
});
let partial_result = computation.await;
this.update(cx, |this, cx| {
this.progress = i as u32 + 1;
this.result = Some(partial_result);
cx.notify();
})
.ok();
}
this.update(cx, |this, cx| {
this.is_computing = false;
cx.notify();
})
.ok();
})
.detach();
}
}
// Example 3: Cancellable Task
//
// Tasks can be cancelled by dropping them.
// Store a task in a field to keep it running.
struct CancellableTaskDemo {
counter: u32,
counting_task: Option<Task<()>>,
}
impl CancellableTaskDemo {
fn new() -> Self {
Self {
counter: 0,
counting_task: None,
}
}
fn is_running(&self) -> bool {
self.counting_task.is_some()
}
fn toggle(&mut self, cx: &mut Context<Self>) {
if self.counting_task.is_some() {
self.counting_task = None;
cx.notify();
} else {
self.counting_task = Some(cx.spawn(async move |this, cx| {
loop {
cx.background_spawn(async {
std::thread::sleep(Duration::from_millis(100));
})
.await;
let should_continue = this
.update(cx, |this, cx| {
this.counter += 1;
cx.notify();
true
})
.unwrap_or(false);
if !should_continue {
break;
}
}
}));
cx.notify();
}
}
}
// Example 4: Task with Return Value
//
// Tasks can return values that you can await.
struct ReturnValueDemo {
numbers: Vec<i32>,
sum: Option<i32>,
is_calculating: bool,
}
impl ReturnValueDemo {
fn new() -> Self {
Self {
numbers: vec![1, 2, 3, 4, 5],
sum: None,
is_calculating: false,
}
}
fn calculate_sum(&mut self, cx: &mut Context<Self>) {
self.is_calculating = true;
cx.notify();
let numbers = self.numbers.clone();
cx.spawn(async move |this, cx| {
let result = cx
.background_spawn(async move {
std::thread::sleep(Duration::from_millis(500));
numbers.iter().sum::<i32>()
})
.await;
this.update(cx, |this, cx| {
this.sum = Some(result);
this.is_calculating = false;
cx.notify();
})
.ok();
})
.detach();
}
fn randomize(&mut self, cx: &mut Context<Self>) {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
let hasher = RandomState::new().build_hasher().finish();
self.numbers = (0..5)
.map(|i| ((hasher >> (i * 8)) & 0xFF) as i32 % 100)
.collect();
self.sum = None;
cx.notify();
}
}
// Main Application
struct AsyncTasksExample {
foreground_demo: Entity<ForegroundTaskDemo>,
background_demo: Entity<BackgroundTaskDemo>,
cancellable_demo: Entity<CancellableTaskDemo>,
return_demo: Entity<ReturnValueDemo>,
}
impl AsyncTasksExample {
fn new(cx: &mut Context<Self>) -> Self {
Self {
foreground_demo: cx.new(|_| ForegroundTaskDemo::new()),
background_demo: cx.new(|_| BackgroundTaskDemo::new()),
cancellable_demo: cx.new(|_| CancellableTaskDemo::new()),
return_demo: cx.new(|_| ReturnValueDemo::new()),
}
}
}
impl Render for AsyncTasksExample {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let foreground = self.foreground_demo.read(cx);
let background = self.background_demo.read(cx);
let cancellable = self.cancellable_demo.read(cx);
let return_demo = self.return_demo.read(cx);
div()
.id("main")
.size_full()
.p_6()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_6()
.max_w(px(500.))
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Async Tasks"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Spawning, background work, and task management"),
),
)
.child(demo_section(
&colors,
"1. Foreground Task (cx.spawn)",
"Runs async work on the UI thread. Good for sequential async operations.",
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_sm()
.text_color(colors.text)
.child(foreground.message.clone()),
)
.child(
button(&colors, "foreground-btn", "Start Task", foreground.is_loading)
.on_click(cx.listener(|this, _, _, cx| {
this.foreground_demo.update(cx, |demo, cx| {
demo.start_task(cx);
});
})),
),
))
.child(demo_section(
&colors,
"2. Background Task (cx.background_spawn)",
"Runs heavy computation off the UI thread with progress updates.",
div()
.flex()
.flex_col()
.gap_2()
.child(progress_bar(&colors, background.progress))
.child(
div()
.text_sm()
.text_color(colors.text)
.child(format!(
"Progress: {}% | Result: {}",
background.progress,
background
.result
.map(|r| r.to_string())
.unwrap_or_else(|| "-".into())
)),
)
.child(
button(&colors, "background-btn", "Compute", background.is_computing)
.on_click(cx.listener(|this, _, _, cx| {
this.background_demo.update(cx, |demo, cx| {
demo.start_computation(cx);
});
})),
),
))
.child(demo_section(
&colors,
"3. Cancellable Task",
"Store Task in a field to keep it running. Drop to cancel.",
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_2xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child(format!("{}", cancellable.counter)),
)
.child({
let is_running = cancellable.is_running();
let (bg, bg_hover) = if is_running {
(colors.error, colors.error_hover)
} else {
(colors.success, colors.success_hover)
};
div()
.id("cancel-btn")
.px_3()
.py_1p5()
.rounded_md()
.text_sm()
.text_color(colors.text)
.cursor_pointer()
.bg(bg)
.hover(move |style| style.bg(bg_hover))
.child(if is_running { "Stop" } else { "Start Counter" })
.on_click(cx.listener(|this, _, _, cx| {
this.cancellable_demo.update(cx, |demo, cx| {
demo.toggle(cx);
});
}))
}),
))
.child(demo_section(
&colors,
"4. Task with Return Value",
"Tasks can return values that can be awaited or used in chained operations.",
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child(format!("Numbers: {:?}", return_demo.numbers)),
)
.child(
div()
.text_sm()
.text_color(colors.text)
.child(format!(
"Sum: {}",
if return_demo.is_calculating {
"Calculating...".into()
} else {
return_demo
.sum
.map(|s| s.to_string())
.unwrap_or_else(|| "Not calculated".into())
}
)),
)
.child(
div()
.flex()
.gap_2()
.child(
button(
&colors,
"sum-btn",
"Calculate Sum",
return_demo.is_calculating,
)
.on_click(cx.listener(|this, _, _, cx| {
this.return_demo.update(cx, |demo, cx| {
demo.calculate_sum(cx);
});
})),
)
.child(
secondary_button(&colors, "random-btn", "Randomize")
.on_click(cx.listener(|this, _, _, cx| {
this.return_demo.update(cx, |demo, cx| {
demo.randomize(cx);
});
})),
),
),
))
)
}
}
// Helper Components
fn demo_section(
colors: &Colors,
title: &'static str,
description: &'static str,
content: impl IntoElement,
) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded_lg()
.bg(colors.surface)
.border_1()
.border_color(colors.border)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child(title),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child(description),
),
)
.child(content)
}
fn button(
colors: &Colors,
id: impl Into<gpui::ElementId>,
label: &'static str,
disabled: bool,
) -> gpui::Stateful<gpui::Div> {
let disabled_bg = colors.surface_hover;
let bg = colors.accent;
let bg_hover = colors.accent_hover;
let bg_active = colors.accent_active;
let text = colors.selected_text;
div()
.id(id)
.px_3()
.py_1p5()
.rounded_md()
.text_sm()
.text_color(text)
.when(disabled, |el| {
el.bg(disabled_bg).cursor_not_allowed().opacity(0.6)
})
.when(!disabled, |el| {
el.bg(bg)
.cursor_pointer()
.hover(move |style| style.bg(bg_hover))
.active(move |style| style.bg(bg_active))
})
.child(label)
}
fn secondary_button(
colors: &Colors,
id: impl Into<gpui::ElementId>,
label: &'static str,
) -> gpui::Stateful<gpui::Div> {
let bg = colors.surface_hover;
let bg_hover = colors.border;
let text = colors.text;
div()
.id(id)
.px_3()
.py_1p5()
.rounded_md()
.text_sm()
.text_color(text)
.bg(bg)
.cursor_pointer()
.hover(move |style| style.bg(bg_hover))
.child(label)
}
fn progress_bar(colors: &Colors, progress: u32) -> impl IntoElement {
let clamped = progress.min(100);
let bar_bg = colors.surface_hover;
let bar_fill = colors.success;
div()
.h_2()
.w_full()
.rounded_full()
.bg(bar_bg)
.overflow_hidden()
.child(
div()
.h_full()
.rounded_full()
.bg(bar_fill)
.w(gpui::relative(clamped as f32 / 100.0)),
)
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(550.), px(850.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|cx| AsyncTasksExample::new(cx)),
)
.expect("Failed to open window");
example_prelude::init_example(cx, "Async Tasks");
});
}
+434
View File
@@ -0,0 +1,434 @@
//! Creating Components Example
//!
//! This example demonstrates three different approaches to creating interactive
//! stateful components in GPUI:
//!
//! 1. `use_state` - Hook-like state scoped to an element's lifetime
//! 2. `RenderOnce` - Stateless component that receives state from parent
//! 3. `Render` - Entity-backed view with persistent internal state
#[path = "../prelude.rs"]
mod example_prelude;
use example_prelude::init_example;
use gpui::{
App, Application, Bounds, Colors, Context, Entity, IntoElement, Render, RenderOnce, Window,
WindowBounds, WindowOptions, div, prelude::*, px, size,
};
// ============================================================================
// Approach 1: use_state
// ============================================================================
//
// `use_state` creates element-scoped state that persists across renders.
// It's similar to React's useState hook. The state is automatically tied
// to the element's identity via caller location or a provided key.
//
// Pros:
// - Simple, hook-like API
// - State is scoped to element lifetime
// - No boilerplate for simple state
//
// Cons:
// - Less explicit than Entity-backed state
// - State is tied to call site location
struct UseStateCounter {
count: i32,
}
fn use_state_counter(colors: &Colors, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state: Entity<UseStateCounter> =
window.use_state(cx, |_window, _cx| UseStateCounter { count: 0 });
let count = state.read(cx).count;
let error = colors.error;
let error_hover = colors.error_hover;
let success = colors.success;
let success_hover = colors.success_hover;
div()
.id("use-state-counter")
.flex()
.flex_col()
.gap_2()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("use_state Counter"),
)
.child(
div()
.text_2xl()
.text_color(colors.text)
.child(format!("{}", count)),
)
.child(
div()
.flex()
.gap_2()
.child(
div()
.id("use-state-decrement")
.px_3()
.py_1()
.rounded_md()
.bg(error)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(error_hover))
.child("−")
.on_click({
let state = state.clone();
move |_, _, cx| {
state.update(cx, |state, cx| {
state.count -= 1;
cx.notify();
});
}
}),
)
.child(
div()
.id("use-state-increment")
.px_3()
.py_1()
.rounded_md()
.bg(success)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(success_hover))
.child("+")
.on_click(move |_, _, cx| {
state.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
}),
),
)
}
// ============================================================================
// Approach 2: RenderOnce
// ============================================================================
//
// `RenderOnce` components are stateless and consumed when rendered.
// They receive all data as props and delegate state management to the parent.
// This is the recommended approach for presentational components.
//
// Pros:
// - Clear data flow (props down, events up)
// - Lightweight (no Entity allocation)
// - Easy to test
// - Highly composable
//
// Cons:
// - Cannot maintain internal state
// - Parent must manage all state
#[derive(IntoElement)]
struct RenderOnceCounter {
colors: Colors,
count: i32,
on_increment: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
on_decrement: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
}
impl RenderOnceCounter {
fn new(colors: Colors, count: i32) -> Self {
Self {
colors,
count,
on_increment: None,
on_decrement: None,
}
}
fn on_increment(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_increment = Some(Box::new(callback));
self
}
fn on_decrement(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_decrement = Some(Box::new(callback));
self
}
}
impl RenderOnce for RenderOnceCounter {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let colors = self.colors;
let error = colors.error;
let error_hover = colors.error_hover;
let success = colors.success;
let success_hover = colors.success_hover;
div()
.id("render-once-counter")
.flex()
.flex_col()
.gap_2()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("RenderOnce Counter"),
)
.child(
div()
.text_2xl()
.text_color(colors.text)
.child(format!("{}", self.count)),
)
.child(
div()
.flex()
.gap_2()
.child(
div()
.id("render-once-decrement")
.px_3()
.py_1()
.rounded_md()
.bg(error)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(error_hover))
.child("−")
.when_some(self.on_decrement, |element, callback| {
element.on_click(move |_, window, cx| callback(window, cx))
}),
)
.child(
div()
.id("render-once-increment")
.px_3()
.py_1()
.rounded_md()
.bg(success)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(success_hover))
.child("+")
.when_some(self.on_increment, |element, callback| {
element.on_click(move |_, window, cx| callback(window, cx))
}),
),
)
}
}
// ============================================================================
// Approach 3: Render (Entity-backed)
// ============================================================================
//
// `Render` components are backed by an `Entity<T>` and maintain their own
// internal state. This is the recommended approach for complex components
// that need to manage their own state, subscribe to events, or spawn tasks.
//
// Pros:
// - Full control over internal state
// - Can subscribe to events and observe other entities
// - Can spawn async tasks
// - Has identity (can be passed around as Entity<T>)
//
// Cons:
// - More boilerplate
// - Higher memory overhead
// - More complex lifecycle
struct RenderCounter {
count: i32,
}
impl RenderCounter {
fn new() -> Self {
Self { count: 0 }
}
fn increment(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.count += 1;
cx.notify();
}
fn decrement(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.count -= 1;
cx.notify();
}
}
impl Render for RenderCounter {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let error = colors.error;
let error_hover = colors.error_hover;
let success = colors.success;
let success_hover = colors.success_hover;
div()
.id("render-counter")
.flex()
.flex_col()
.gap_2()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Render Counter"),
)
.child(
div()
.text_2xl()
.text_color(colors.text)
.child(format!("{}", self.count)),
)
.child(
div()
.flex()
.gap_2()
.child(
div()
.id("render-decrement")
.px_3()
.py_1()
.rounded_md()
.bg(error)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(error_hover))
.child("−")
.on_click(cx.listener(|this, _, window, cx| {
this.decrement(window, cx);
})),
)
.child(
div()
.id("render-increment")
.px_3()
.py_1()
.rounded_md()
.bg(success)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(success_hover))
.child("+")
.on_click(cx.listener(|this, _, window, cx| {
this.increment(window, cx);
})),
),
)
}
}
// ============================================================================
// Main Application
// ============================================================================
struct CreatingComponentsExample {
render_counter: Entity<RenderCounter>,
render_once_count: i32,
}
impl CreatingComponentsExample {
fn new(cx: &mut Context<Self>) -> Self {
Self {
render_counter: cx.new(|_| RenderCounter::new()),
render_once_count: 0,
}
}
}
impl Render for CreatingComponentsExample {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let render_once_count = self.render_once_count;
let handle = cx.entity().downgrade();
div()
.id("main")
.size_full()
.flex()
.flex_col()
.gap_6()
.p_8()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_2xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Creating Components"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Three approaches to stateful components in GPUI"),
),
)
.child(
div()
.flex()
.flex_row()
.gap_4()
.child(use_state_counter(&colors, window, cx))
.child(
RenderOnceCounter::new(colors.clone(), render_once_count)
.on_increment({
let handle = handle.clone();
move |_window, cx| {
handle
.update(cx, |this, cx| {
this.render_once_count += 1;
cx.notify();
})
.ok();
}
})
.on_decrement(move |_window, cx| {
handle
.update(cx, |this, cx| {
this.render_once_count -= 1;
cx.notify();
})
.ok();
}),
)
.child(self.render_counter.clone()),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(700.), px(400.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|cx| CreatingComponentsExample::new(cx)),
)
.expect("Failed to open window");
init_example(cx, "Creating Components");
});
}
+479
View File
@@ -0,0 +1,479 @@
//! Custom Drawing Example
//!
//! This example demonstrates custom drawing in GPUI using:
//!
//! 1. `canvas` element - For direct painting control
//! 2. `PathBuilder` - Creating custom vector shapes
//! 3. `window.paint_*` methods - Drawing quads, paths, and more
//! 4. Interactive drawing - Responding to mouse events
use gpui::{
App, Application, Bounds, Colors, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Path, PathBuilder, Pixels, Point, Render, Rgba, Window, WindowBounds,
WindowOptions, canvas, div, fill, point, prelude::*, px, rgb, size,
};
#[path = "../prelude.rs"]
mod example_prelude;
// Example 1: Basic Canvas Drawing
//
// The `canvas` element provides two callbacks:
// - prepaint: Called during layout to prepare drawing state
// - paint: Called during paint to actually draw
fn basic_shapes_canvas(colors: &Colors) -> impl IntoElement {
let error = colors.error;
let success = colors.success;
let accent = colors.accent;
canvas(
move |_bounds, _window, _cx| {},
move |bounds, _prepaint_state, window, _cx| {
// Draw a filled rectangle
window.paint_quad(fill(
Bounds {
origin: point(bounds.origin.x + px(10.), bounds.origin.y + px(10.)),
size: size(px(60.), px(40.)),
},
error,
));
// Draw another rectangle
window.paint_quad(fill(
Bounds {
origin: point(bounds.origin.x + px(80.), bounds.origin.y + px(10.)),
size: size(px(60.), px(40.)),
},
success,
));
// Draw a third rectangle
window.paint_quad(fill(
Bounds {
origin: point(bounds.origin.x + px(150.), bounds.origin.y + px(10.)),
size: size(px(60.), px(40.)),
},
accent,
));
},
)
.size_full()
}
// Example 2: Custom Paths with PathBuilder
//
// PathBuilder lets you create complex vector shapes:
// - move_to: Start a new subpath
// - line_to: Draw a straight line
// - curve_to: Draw a bezier curve
// - close: Close the current subpath
fn create_star(center: Point<Pixels>, outer_radius: f32, inner_radius: f32) -> Path<Pixels> {
let mut builder = PathBuilder::fill();
let points = 5;
for i in 0..points * 2 {
let angle =
std::f32::consts::PI / 2.0 - (i as f32) * std::f32::consts::PI / (points as f32);
let radius = if i % 2 == 0 {
outer_radius
} else {
inner_radius
};
let x = center.x + px(angle.cos() * radius);
let y = center.y - px(angle.sin() * radius);
if i == 0 {
builder.move_to(point(x, y));
} else {
builder.line_to(point(x, y));
}
}
builder.close();
builder.build().unwrap()
}
fn create_triangle(p1: Point<Pixels>, p2: Point<Pixels>, p3: Point<Pixels>) -> Path<Pixels> {
let mut builder = PathBuilder::fill();
builder.move_to(p1);
builder.line_to(p2);
builder.line_to(p3);
builder.close();
builder.build().unwrap()
}
fn custom_paths_canvas(colors: &Colors) -> impl IntoElement {
let warning = colors.warning;
let accent = colors.accent;
canvas(
move |_bounds, _window, _cx| {},
move |bounds, _, window, _cx| {
let center_y = bounds.origin.y + bounds.size.height / 2.0;
// Draw a star
let star_center = point(bounds.origin.x + px(50.), center_y);
let star = create_star(star_center, 30., 15.);
window.paint_path(star, warning);
// Draw a triangle
let tri_base_x = bounds.origin.x + px(120.);
let triangle = create_triangle(
point(tri_base_x + px(30.), center_y - px(25.)),
point(tri_base_x, center_y + px(25.)),
point(tri_base_x + px(60.), center_y + px(25.)),
);
window.paint_path(triangle, rgb(0x8b5cf6)); // Purple
// Draw a custom shape (arrow)
let arrow_x = bounds.origin.x + px(200.);
let mut arrow_builder = PathBuilder::fill();
arrow_builder.move_to(point(arrow_x, center_y));
arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(20.)));
arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(10.)));
arrow_builder.line_to(point(arrow_x + px(50.), center_y - px(10.)));
arrow_builder.line_to(point(arrow_x + px(50.), center_y + px(10.)));
arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(10.)));
arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(20.)));
arrow_builder.close();
let arrow = arrow_builder.build().unwrap();
window.paint_path(arrow, accent);
},
)
.size_full()
}
// Example 3: Interactive Drawing
//
// Combine canvas with mouse events for interactive drawing
struct DrawingCanvas {
lines: Vec<Vec<Point<Pixels>>>,
current_line: Vec<Point<Pixels>>,
is_drawing: bool,
color_index: usize,
}
impl DrawingCanvas {
fn new() -> Self {
Self {
lines: Vec::new(),
current_line: Vec::new(),
is_drawing: false,
color_index: 0,
}
}
fn get_colors(colors: &Colors) -> Vec<Rgba> {
vec![
colors.error,
colors.success,
colors.accent,
colors.warning,
rgb(0x8b5cf6), // Purple
rgb(0x06b6d4), // Cyan
]
}
fn current_color(&self, colors: &Colors) -> Rgba {
let palette = Self::get_colors(colors);
palette[self.color_index % palette.len()]
}
fn next_color(&mut self, colors: &Colors) {
let palette = Self::get_colors(colors);
self.color_index = (self.color_index + 1) % palette.len();
}
fn on_mouse_down(
&mut self,
event: &MouseDownEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if event.button == MouseButton::Left {
self.is_drawing = true;
self.current_line = vec![event.position];
cx.notify();
}
}
fn on_mouse_move(
&mut self,
event: &MouseMoveEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.is_drawing {
self.current_line.push(event.position);
cx.notify();
}
}
fn on_mouse_up(&mut self, _event: &MouseUpEvent, window: &mut Window, cx: &mut Context<Self>) {
if self.is_drawing && self.current_line.len() > 1 {
self.lines.push(std::mem::take(&mut self.current_line));
let colors = Colors::for_appearance(window);
self.next_color(&colors);
}
self.is_drawing = false;
self.current_line.clear();
cx.notify();
}
fn clear(&mut self, cx: &mut Context<Self>) {
self.lines.clear();
self.current_line.clear();
self.color_index = 0;
cx.notify();
}
fn draw_line(window: &mut Window, points: &[Point<Pixels>], color: Rgba) {
if points.len() < 2 {
return;
}
for pair in points.windows(2) {
let start = pair[0];
let end = pair[1];
let dx = end.x - start.x;
let dy = end.y - start.y;
let dx_f = f32::from(dx);
let dy_f = f32::from(dy);
let len = (dx_f * dx_f + dy_f * dy_f).sqrt();
if len < 0.1 {
continue;
}
let thickness = 3.0_f32;
let px_offset = px(-dy_f / len * thickness / 2.0);
let py_offset = px(dx_f / len * thickness / 2.0);
let mut builder = PathBuilder::fill();
builder.move_to(point(start.x + px_offset, start.y + py_offset));
builder.line_to(point(end.x + px_offset, end.y + py_offset));
builder.line_to(point(end.x - px_offset, end.y - py_offset));
builder.line_to(point(start.x - px_offset, start.y - py_offset));
builder.close();
if let Ok(path) = builder.build() {
window.paint_path(path, color);
}
}
}
}
impl Render for DrawingCanvas {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let lines = self.lines.clone();
let current_line = self.current_line.clone();
let current_color = self.current_color(&colors);
let palette = Self::get_colors(&colors);
let surface = colors.surface;
let border = colors.border;
let error = colors.error;
let error_hover = colors.error_hover;
let text = colors.selected_text;
let text_muted = colors.text_muted;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.id("drawing-area")
.h_48()
.rounded_lg()
.bg(surface)
.border_1()
.border_color(border)
.cursor_crosshair()
.overflow_hidden()
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
.on_mouse_move(cx.listener(Self::on_mouse_move))
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
.child(
canvas(
move |_, _, _| {},
move |_bounds, _, window, _cx| {
for (i, line) in lines.iter().enumerate() {
let color = palette[i % palette.len()];
DrawingCanvas::draw_line(window, line, color);
}
if !current_line.is_empty() {
DrawingCanvas::draw_line(window, &current_line, current_color);
}
},
)
.size_full(),
),
)
.child(
div()
.flex()
.gap_2()
.child(
div()
.id("clear-btn")
.px_3()
.py_1()
.rounded_md()
.bg(error)
.text_sm()
.text_color(text)
.cursor_pointer()
.hover(move |s| s.bg(error_hover))
.child("Clear")
.on_click(cx.listener(|this, _, _, cx| {
this.clear(cx);
})),
)
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Click and drag to draw"),
),
)
}
}
// Main Application View
struct CustomDrawingExample {
drawing_canvas: gpui::Entity<DrawingCanvas>,
}
impl CustomDrawingExample {
fn new(cx: &mut Context<Self>) -> Self {
Self {
drawing_canvas: cx.new(|_| DrawingCanvas::new()),
}
}
}
impl Render for CustomDrawingExample {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
div()
.id("main")
.size_full()
.p_6()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_6()
.max_w(px(500.))
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Custom Drawing"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Canvas element, paths, and interactive painting"),
),
)
.child(section(
&colors,
"1. Basic Shapes (paint_quad)",
"Use window.paint_quad() to draw filled rectangles",
basic_shapes_canvas(&colors),
px(70.),
))
.child(section(
&colors,
"2. Custom Paths (PathBuilder)",
"Create complex shapes with PathBuilder and paint_path()",
custom_paths_canvas(&colors),
px(80.),
))
.child(section(
&colors,
"3. Interactive Drawing",
"Combine canvas with mouse events for drawing",
self.drawing_canvas.clone(),
px(240.),
)),
)
}
}
fn section(
colors: &Colors,
title: &'static str,
description: &'static str,
content: impl IntoElement,
height: Pixels,
) -> impl IntoElement {
let surface: Hsla = colors.surface.into();
div()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded_lg()
.bg(surface.opacity(0.5))
.border_1()
.border_color(colors.border)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child(title),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child(description),
),
)
.child(div().h(height).child(content))
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|cx| CustomDrawingExample::new(cx)),
)
.expect("Failed to open window");
example_prelude::init_example(cx, "Custom Drawing");
});
}
+567
View File
@@ -0,0 +1,567 @@
//! Interactive Elements Example
//!
//! This example demonstrates interactive patterns in GPUI:
//!
//! 1. Click events - single click, double click, click count
//! 2. Hover states - hover styling and on_hover callbacks
//! 3. Mouse events - mouse down, mouse up, mouse move
//! 4. Drag and drop - draggable elements and drop targets
#[path = "../prelude.rs"]
mod example_prelude;
use example_prelude::init_example;
use gpui::{
App, Application, Bounds, ClickEvent, Colors, Context, Entity, Half, Hsla, IntoElement,
MouseButton, MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds, WindowOptions, div,
prelude::*, px, size,
};
// ============================================================================
// Click Events Demo
// ============================================================================
//
// Demonstrates different click interactions:
// - Single click
// - Double click (click_count == 2)
// - Click count tracking
struct ClickDemo {
click_count: usize,
last_click_type: String,
}
impl ClickDemo {
fn new() -> Self {
Self {
click_count: 0,
last_click_type: "None".to_string(),
}
}
}
impl Render for ClickDemo {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
div()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child("Click Events"),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("Single click, double click, or triple click the button"),
)
.child(
div()
.id("click-target")
.px_4()
.py_2()
.rounded_md()
.bg(colors.accent)
.text_color(colors.selected_text)
.text_sm()
.cursor_pointer()
.hover(|style| style.bg(colors.accent_hover))
.active(|style| style.bg(colors.accent_active))
.child("Click Me!")
// on_click receives a ClickEvent with click_count() method
.on_click(cx.listener(|this, event: &ClickEvent, _window, cx| {
this.click_count += 1;
this.last_click_type = match event.click_count() {
1 => "Single Click".to_string(),
2 => "Double Click".to_string(),
3 => "Triple Click".to_string(),
n => format!("{n}x Click"),
};
cx.notify();
})),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.mt_2()
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child(format!("Total clicks: {}", self.click_count)),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child(format!("Last: {}", self.last_click_type)),
),
)
}
}
// ============================================================================
// Hover Demo
// ============================================================================
//
// Demonstrates hover interactions:
// - hover() style modifier for CSS-like hover states
// - on_hover() callback for programmatic hover detection
struct HoverDemo {
is_hovered: bool,
hover_count: usize,
}
impl HoverDemo {
fn new() -> Self {
Self {
is_hovered: false,
hover_count: 0,
}
}
}
impl Render for HoverDemo {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let is_hovered = self.is_hovered;
div()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child("Hover Events"),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("Move your mouse in and out of the target"),
)
.child(
div()
.id("hover-target")
.px_4()
.py_3()
.rounded_md()
.border_2()
.border_color(if is_hovered {
colors.accent
} else {
colors.border
})
.bg(if is_hovered {
colors.accent_hover
} else {
colors.surface_hover
})
.text_color(if is_hovered {
colors.selected_text
} else {
colors.text
})
.text_sm()
.cursor_pointer()
.child(if is_hovered {
"Mouse Inside!"
} else {
"Hover Over Me"
})
// on_hover callback receives a bool: true when mouse enters, false when it leaves
.on_hover(cx.listener(|this, &hovered, _window, cx| {
this.is_hovered = hovered;
if hovered {
this.hover_count += 1;
}
cx.notify();
})),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.mt_2()
.child(format!("Times hovered: {}", self.hover_count)),
)
}
}
// ============================================================================
// Mouse Events Demo
// ============================================================================
//
// Demonstrates low-level mouse events:
// - on_mouse_down - fires when mouse button is pressed
// - on_mouse_up - fires when mouse button is released
// - on_mouse_move - fires when mouse moves over element
struct MouseEventsDemo {
mouse_position: Option<Point<Pixels>>,
is_pressed: bool,
event_log: Vec<String>,
}
impl MouseEventsDemo {
fn new() -> Self {
Self {
mouse_position: None,
is_pressed: false,
event_log: Vec::new(),
}
}
fn log_event(&mut self, event: &str) {
self.event_log.push(event.to_string());
if self.event_log.len() > 5 {
self.event_log.remove(0);
}
}
}
impl Render for MouseEventsDemo {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let is_pressed = self.is_pressed;
let position_text = self
.mouse_position
.map(|p| format!("({:.0}, {:.0})", f32::from(p.x), f32::from(p.y)))
.unwrap_or_else(|| "—".to_string());
div()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child("Mouse Events"),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("Move and click within the target area"),
)
.child(
div()
.id("mouse-events-target")
.h_20()
.rounded_md()
.border_2()
.border_color(if is_pressed {
colors.accent
} else {
colors.border
})
.bg(if is_pressed {
colors.accent_hover
} else {
colors.surface_hover
})
.flex()
.items_center()
.justify_center()
.text_sm()
.text_color(colors.text)
.child(format!("Position: {}", position_text))
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _event, _window, cx| {
this.is_pressed = true;
this.log_event("Mouse Down");
cx.notify();
}),
)
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _event, _window, cx| {
this.is_pressed = false;
this.log_event("Mouse Up");
cx.notify();
}),
)
.on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| {
this.mouse_position = Some(event.position);
cx.notify();
})),
)
.child(
div()
.flex()
.flex_col()
.gap_0p5()
.mt_2()
.text_xs()
.text_color(colors.text_muted)
.children(
self.event_log
.iter()
.map(|e| div().child(format!("• {}", e))),
),
)
}
}
// ============================================================================
// Drag and Drop Demo
// ============================================================================
//
// Demonstrates drag and drop:
// - on_drag - makes an element draggable, provides drag data
// - on_drop - makes an element a drop target, receives drag data
#[derive(Clone, Copy)]
struct DragData {
index: usize,
color: Hsla,
position: Point<Pixels>,
}
impl DragData {
fn new(index: usize, color: Hsla) -> Self {
Self {
index,
color,
position: Point::default(),
}
}
fn with_position(mut self, position: Point<Pixels>) -> Self {
self.position = position;
self
}
}
// Render trait for DragData allows it to be rendered as drag feedback
impl Render for DragData {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let size = gpui::size(px(80.), px(40.));
// Position the drag preview at the cursor
div()
.pl(self.position.x - size.width.half())
.pt(self.position.y - size.height.half())
.child(
div()
.flex()
.justify_center()
.items_center()
.w(size.width)
.h(size.height)
.bg(self.color.opacity(0.8))
.text_color(gpui::white())
.text_xs()
.rounded_md()
.shadow_lg()
.child(format!("Item {}", self.index + 1)),
)
}
}
struct DragDropDemo {
dropped_item: Option<DragData>,
}
impl DragDropDemo {
fn new() -> Self {
Self { dropped_item: None }
}
}
impl Render for DragDropDemo {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let item_colors = [colors.error, colors.success, colors.warning];
div()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child("Drag and Drop"),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("Drag items to the drop zone below"),
)
.child(
div()
.flex()
.gap_2()
.children(item_colors.into_iter().enumerate().map(|(index, color)| {
let drag_data = DragData::new(index, color.into());
div()
.id(("drag-item", index))
.px_3()
.py_2()
.rounded_md()
.border_2()
.border_color(color)
.text_color(color)
.text_xs()
.cursor_grab()
.hover(move |style| {
let c: Hsla = color.into();
style.bg(c.opacity(0.1))
})
.child(format!("Item {}", index + 1))
// on_drag takes: drag data, and a closure that creates the drag preview
.on_drag(drag_data, |data: &DragData, position, _, cx| {
cx.new(|_| data.with_position(position))
})
})),
)
.child(
div()
.id("drop-target")
.mt_2()
.h_16()
.rounded_md()
.border_2()
.border_dashed()
.border_color(
self.dropped_item
.map(|d| d.color)
.unwrap_or_else(|| colors.border.into()),
)
.when_some(self.dropped_item, |el, data| el.bg(data.color.opacity(0.2)))
.flex()
.items_center()
.justify_center()
.text_xs()
.text_color(colors.text_muted)
// on_drop receives the drag data when an item is dropped
.on_drop(cx.listener(|this, data: &DragData, _window, cx| {
this.dropped_item = Some(*data);
cx.notify();
}))
.child(
self.dropped_item
.map(|d| format!("Dropped: Item {}", d.index + 1))
.unwrap_or_else(|| "Drop Zone".to_string()),
),
)
}
}
// ============================================================================
// Main Application
// ============================================================================
struct InteractiveElementsExample {
click_demo: Entity<ClickDemo>,
hover_demo: Entity<HoverDemo>,
mouse_events_demo: Entity<MouseEventsDemo>,
drag_drop_demo: Entity<DragDropDemo>,
}
impl InteractiveElementsExample {
fn new(cx: &mut Context<Self>) -> Self {
Self {
click_demo: cx.new(|_| ClickDemo::new()),
hover_demo: cx.new(|_| HoverDemo::new()),
mouse_events_demo: cx.new(|_| MouseEventsDemo::new()),
drag_drop_demo: cx.new(|_| DragDropDemo::new()),
}
}
}
impl Render for InteractiveElementsExample {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
div()
.id("main")
.size_full()
.p_6()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_6()
.max_w(px(800.))
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Interactive Elements"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Click, hover, mouse events, and drag-and-drop in GPUI"),
),
)
.child(
div()
.grid()
.grid_cols(2)
.gap_4()
.child(self.click_demo.clone())
.child(self.hover_demo.clone())
.child(self.mouse_events_demo.clone())
.child(self.drag_drop_demo.clone()),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(700.), px(650.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|cx| InteractiveElementsExample::new(cx)),
)
.expect("Failed to open window");
init_example(cx, "Interactive Elements");
});
}
+499
View File
@@ -0,0 +1,499 @@
//! Layout Patterns Example
//!
//! This example demonstrates different layout approaches in GPUI:
//!
//! 1. Flexbox - Row and column layouts with alignment
//! 2. Grid - Two-dimensional layouts with spans
//! 3. Common patterns - Sidebar, header/footer, centering
#[path = "../prelude.rs"]
mod example_prelude;
use example_prelude::init_example;
use gpui::{
App, Application, Bounds, Colors, Context, Div, Hsla, Render, Rgba, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
};
// Helper: Colored block for visualization
fn block(label: &'static str, color: Hsla, text_color: Rgba) -> Div {
div()
.flex()
.items_center()
.justify_center()
.bg(color)
.border_1()
.border_color(gpui::white().opacity(0.3))
.rounded_md()
.text_xs()
.text_color(text_color)
.child(label)
}
// Flexbox Examples
fn flexbox_row_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let text = colors.selected_text;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("flex().flex_row().gap_2()"),
)
.child(
div()
.flex()
.flex_row()
.gap_2()
.child(block("A", gpui::red(), text).size_8())
.child(block("B", gpui::green(), text).size_8())
.child(block("C", gpui::blue(), text).size_8()),
)
}
fn flexbox_column_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let text = colors.selected_text;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("flex().flex_col().gap_2()"),
)
.child(
div()
.h_24()
.flex()
.flex_col()
.gap_2()
.child(block("A", gpui::red(), text).h_6())
.child(block("B", gpui::green(), text).h_6())
.child(block("C", gpui::blue(), text).h_6()),
)
}
fn flexbox_justify_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let text = colors.selected_text;
let surface = colors.surface;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("justify_between / justify_center / justify_end"),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.flex()
.justify_between()
.p_1()
.bg(surface)
.rounded_sm()
.child(block("Start", gpui::red(), text).px_2().py_1())
.child(block("End", gpui::blue(), text).px_2().py_1()),
)
.child(
div()
.flex()
.justify_center()
.p_1()
.bg(surface)
.rounded_sm()
.child(block("Center", gpui::green(), text).px_2().py_1()),
)
.child(
div()
.flex()
.justify_end()
.p_1()
.bg(surface)
.rounded_sm()
.child(block("End", gpui::yellow(), text).px_2().py_1()),
),
)
}
fn flexbox_grow_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let text = colors.selected_text;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("flex_1 (grow) vs flex_none (fixed)"),
)
.child(
div()
.flex()
.gap_2()
.child(block("fixed", gpui::red(), text).flex_none().w_16().h_8())
.child(block("flex_1 (grows)", gpui::green(), text).flex_1().h_8())
.child(block("fixed", gpui::blue(), text).flex_none().w_16().h_8()),
)
}
// Grid Examples
fn grid_basic_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let text = colors.selected_text;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("grid().grid_cols(3).gap_1()"),
)
.child(
div()
.grid()
.grid_cols(3)
.gap_1()
.child(block("1", gpui::red(), text).h_8())
.child(block("2", gpui::green(), text).h_8())
.child(block("3", gpui::blue(), text).h_8())
.child(block("4", gpui::yellow(), text).h_8())
.child(block("5", gpui::red(), text).h_8())
.child(block("6", gpui::green(), text).h_8()),
)
}
fn grid_span_example(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let text = colors.selected_text;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("col_span / row_span"),
)
.child(
div()
.grid()
.grid_cols(4)
.grid_rows(3)
.gap_1()
.child(
block("Header (col_span_full)", gpui::red(), text)
.col_span_full()
.h_6(),
)
.child(
block("Side", gpui::green(), text)
.col_span(1)
.row_span(2)
.h_full(),
)
.child(
block("Content (col_span 3)", gpui::blue(), text)
.col_span(3)
.row_span(2)
.h_full(),
),
)
}
// Common Layout Patterns
fn app_shell_pattern(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let text = colors.text;
let surface = colors.surface;
let surface_hover = colors.surface_hover;
let background = colors.background;
let border = colors.border;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("App Shell: Header + Sidebar + Content"),
)
.child(
div()
.h_32()
.flex()
.flex_col()
.border_1()
.border_color(border)
.rounded_md()
.overflow_hidden()
.child(
div()
.h_6()
.flex()
.items_center()
.px_2()
.bg(surface_hover)
.text_xs()
.text_color(text)
.child("Header"),
)
.child(
div()
.flex_1()
.flex()
.child(
div()
.w_16()
.bg(surface)
.flex()
.items_center()
.justify_center()
.text_xs()
.text_color(text_muted)
.child("Side"),
)
.child(
div()
.flex_1()
.bg(background)
.flex()
.items_center()
.justify_center()
.text_xs()
.text_color(text_muted)
.child("Content"),
),
),
)
}
fn centered_pattern(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let text = colors.selected_text;
let surface = colors.surface;
let accent = colors.accent;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Centering: items_center + justify_center"),
)
.child(
div()
.h_20()
.flex()
.items_center()
.justify_center()
.bg(surface)
.rounded_md()
.child(
div()
.px_4()
.py_2()
.bg(accent)
.rounded_md()
.text_xs()
.text_color(text)
.child("Perfectly Centered"),
),
)
}
fn stack_pattern(colors: &Colors) -> impl IntoElement {
let text_muted = colors.text_muted;
let surface = colors.surface;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Stack: Overlapping with absolute positioning"),
)
.child(
div()
.h_20()
.relative()
.bg(surface)
.rounded_md()
.child(
div()
.absolute()
.top_2()
.left_2()
.size_10()
.bg(gpui::red().opacity(0.7))
.rounded_md(),
)
.child(
div()
.absolute()
.top_4()
.left_4()
.size_10()
.bg(gpui::green().opacity(0.7))
.rounded_md(),
)
.child(
div()
.absolute()
.top_6()
.left_6()
.size_10()
.bg(gpui::blue().opacity(0.7))
.rounded_md(),
),
)
}
// Main Application View
struct LayoutExample;
impl Render for LayoutExample {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
div()
.id("main")
.size_full()
.p_4()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_4()
.max_w(px(600.))
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Layout Patterns"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Flexbox, Grid, and common layout patterns in GPUI"),
),
)
.child(section(
&colors,
"Flexbox: Row",
flexbox_row_example(&colors),
))
.child(section(
&colors,
"Flexbox: Column",
flexbox_column_example(&colors),
))
.child(section(
&colors,
"Flexbox: Justify Content",
flexbox_justify_example(&colors),
))
.child(section(
&colors,
"Flexbox: Grow/Shrink",
flexbox_grow_example(&colors),
))
.child(section(&colors, "Grid: Basic", grid_basic_example(&colors)))
.child(section(&colors, "Grid: Spans", grid_span_example(&colors)))
.child(section(
&colors,
"Pattern: App Shell",
app_shell_pattern(&colors),
))
.child(section(
&colors,
"Pattern: Centering",
centered_pattern(&colors),
))
.child(section(&colors, "Pattern: Stack", stack_pattern(&colors))),
)
}
}
fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
let surface: Hsla = colors.surface.into();
div()
.flex()
.flex_col()
.gap_2()
.p_3()
.bg(surface.opacity(0.5))
.rounded_lg()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child(title),
)
.child(content)
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(650.), px(700.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| LayoutExample),
)
.expect("Failed to open window");
init_example(cx, "Layout");
});
}
+511
View File
@@ -0,0 +1,511 @@
//! Styling Patterns Example
//!
//! This example demonstrates different styling approaches in GPUI:
//!
//! 1. Interactive states - hover, active, focus, focus_visible
//! 2. Conditional styling - when, when_some, map
//! 3. Theming patterns - using Colors for consistent styling
use gpui::{
App, Application, Bounds, Colors, Context, FocusHandle, Hsla, KeyBinding, Menu, MenuItem,
Render, Rgba, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
};
actions!(styling_example, [Quit, Tab, TabPrev]);
// Interactive States Example
fn interactive_button(
id: impl Into<gpui::ElementId>,
label: &'static str,
colors: &Colors,
) -> impl IntoElement {
let accent = colors.accent;
let accent_hover = colors.accent_hover;
let accent_active = colors.accent_active;
let text = colors.selected_text;
div()
.id(id)
.px_4()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(accent)
.text_color(text)
.text_sm()
.hover(move |style| style.bg(accent_hover))
.active(move |style| style.bg(accent_active))
.child(label)
}
fn focus_button(
id: impl Into<gpui::ElementId>,
label: &'static str,
focus_handle: &FocusHandle,
colors: &Colors,
) -> impl IntoElement {
let surface = colors.surface;
let surface_hover = colors.surface_hover;
let text = colors.text;
let accent = colors.accent;
let focus_ring: Rgba = rgb(0x60a5fa);
div()
.id(id)
.track_focus(focus_handle)
.px_4()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(surface)
.text_color(text)
.text_sm()
.border_2()
.border_color(gpui::transparent_black())
.hover(move |style| style.bg(surface_hover))
.focus(move |style| style.border_color(accent))
.focus_visible(move |style| style.border_color(focus_ring).shadow_sm())
.child(label)
}
fn interactive_states_section(colors: &Colors) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("hover() / active() - Mouse interaction states"),
)
.child(
div()
.flex()
.gap_2()
.child(interactive_button("btn-1", "Hover me", colors))
.child(interactive_button("btn-2", "Click me", colors)),
)
}
// Conditional Styling Example
fn status_badge(status: &'static str, variant: StatusVariant, colors: &Colors) -> impl IntoElement {
let (bg, text): (Rgba, Rgba) = match variant {
StatusVariant::Success => (colors.success, colors.selected_text),
StatusVariant::Warning => (colors.warning, rgb(0x000000)),
StatusVariant::Error => (colors.error, colors.selected_text),
StatusVariant::Neutral => (colors.surface, colors.text),
};
div()
.px_2()
.py_0p5()
.rounded_full()
.text_xs()
.bg(bg)
.text_color(text)
.child(status)
}
#[derive(Clone, Copy)]
enum StatusVariant {
Success,
Warning,
Error,
Neutral,
}
fn list_item(
id: impl Into<gpui::ElementId>,
label: &'static str,
is_selected: bool,
is_disabled: bool,
colors: &Colors,
) -> impl IntoElement {
let surface = colors.surface;
let surface_hover = colors.surface_hover;
let text = colors.text;
let text_muted = colors.text_muted;
let accent = colors.accent;
div()
.id(id)
.px_3()
.py_2()
.rounded_md()
.text_sm()
.cursor_pointer()
.border_1()
.border_color(gpui::transparent_black())
.when(is_disabled, |el| {
el.opacity(0.5)
.cursor_not_allowed()
.bg(surface)
.text_color(text_muted)
})
.when(!is_disabled && is_selected, move |el| {
let accent_bg: Hsla = accent.into();
el.bg(accent_bg.opacity(0.2))
.border_color(accent)
.text_color(text)
})
.when(!is_disabled && !is_selected, move |el| {
el.bg(surface)
.text_color(text)
.hover(move |style| style.bg(surface_hover))
})
.child(label)
}
fn conditional_section(colors: &Colors) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("when() - Apply styles conditionally"),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(list_item("item-1", "Normal item", false, false, colors))
.child(list_item("item-2", "Selected item", true, false, colors))
.child(list_item("item-3", "Disabled item", false, true, colors)),
)
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.mt_2()
.child("Status badges with variant-based styling"),
)
.child(
div()
.flex()
.gap_2()
.child(status_badge("Success", StatusVariant::Success, colors))
.child(status_badge("Warning", StatusVariant::Warning, colors))
.child(status_badge("Error", StatusVariant::Error, colors))
.child(status_badge("Neutral", StatusVariant::Neutral, colors)),
)
}
// Group Hover Example
fn card_with_group_hover(
id: impl Into<gpui::ElementId>,
title: &'static str,
description: &'static str,
colors: &Colors,
) -> impl IntoElement {
let surface = colors.surface;
let border = colors.border;
let accent = colors.accent;
let text = colors.text;
let text_muted = colors.text_muted;
div()
.id(id)
.group("card")
.p_4()
.rounded_lg()
.bg(surface)
.border_1()
.border_color(border)
.cursor_pointer()
.hover(move |style| style.border_color(accent))
.child(
div()
.flex()
.justify_between()
.items_center()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(text)
.child(title),
)
.child(
div()
.text_xs()
.text_color(text_muted)
.opacity(0.)
.group_hover("card", |style| style.opacity(1.))
.child("→"),
),
)
.child(
div()
.mt_1()
.text_xs()
.text_color(text_muted)
.child(description),
)
}
fn group_hover_section(colors: &Colors) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("group() / group_hover() - Parent hover affects children"),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(card_with_group_hover(
"card-1",
"Documents",
"View and manage your documents",
colors,
))
.child(card_with_group_hover(
"card-2",
"Settings",
"Configure application settings",
colors,
)),
)
}
// Main Application View
struct StylingExample {
focus_handle: FocusHandle,
buttons: Vec<FocusHandle>,
}
impl StylingExample {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
window.focus(&focus_handle);
let buttons = vec![
cx.focus_handle().tab_index(1).tab_stop(true),
cx.focus_handle().tab_index(2).tab_stop(true),
cx.focus_handle().tab_index(3).tab_stop(true),
];
Self {
focus_handle,
buttons,
}
}
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
}
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
window.focus_prev();
}
}
impl Render for StylingExample {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
div()
.id("app")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.size_full()
.p_6()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_6()
.max_w(px(500.))
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Styling Patterns"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Interactive states, conditional styling, and theming"),
),
)
.child(section(
&colors,
"Interactive States",
interactive_states_section(&colors),
))
.child(section(
&colors,
"Focus States (Tab to navigate)",
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("focus() / focus_visible() - Keyboard navigation"),
)
.child(
div()
.flex()
.gap_2()
.child(focus_button(
"focus-1",
"Button 1",
&self.buttons[0],
&colors,
))
.child(focus_button(
"focus-2",
"Button 2",
&self.buttons[1],
&colors,
))
.child(focus_button(
"focus-3",
"Button 3",
&self.buttons[2],
&colors,
)),
),
))
.child(section(
&colors,
"Conditional Styling",
conditional_section(&colors),
))
.child(section(
&colors,
"Group Hover",
group_hover_section(&colors),
))
.child(section(
&colors,
"Default Colors",
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(colors.text_muted)
.child("Using Colors::for_appearance() for consistent theming"),
)
.child(
div()
.flex()
.flex_wrap()
.gap_2()
.child(color_swatch(&colors, "background", colors.background))
.child(color_swatch(&colors, "surface", colors.surface))
.child(color_swatch(&colors, "accent", colors.accent))
.child(color_swatch(&colors, "success", colors.success))
.child(color_swatch(&colors, "warning", colors.warning))
.child(color_swatch(&colors, "error", colors.error))
.child(color_swatch(&colors, "border", colors.border)),
),
)),
)
}
}
fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
let surface: Hsla = colors.surface.into();
let border: Hsla = colors.border.into();
div()
.flex()
.flex_col()
.gap_3()
.p_4()
.bg(surface.opacity(0.3))
.rounded_lg()
.border_1()
.border_color(border.opacity(0.5))
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(colors.text)
.child(title),
)
.child(content)
}
fn color_swatch(colors: &Colors, name: &'static str, color: Rgba) -> impl IntoElement {
let text_muted = colors.text_muted;
div()
.flex()
.flex_col()
.items_center()
.gap_1()
.child(
div()
.size_8()
.rounded_md()
.bg(color)
.border_1()
.border_color(gpui::white().opacity(0.2)),
)
.child(div().text_xs().text_color(text_muted).child(name))
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([
KeyBinding::new("cmd-q", Quit, None),
KeyBinding::new("tab", Tab, None),
KeyBinding::new("shift-tab", TabPrev, None),
]);
cx.set_menus(vec![Menu {
name: "Styling".into(),
items: vec![MenuItem::action("Quit", Quit)],
}]);
cx.on_window_closed(|cx| {
if cx.windows().is_empty() {
cx.quit();
}
})
.detach();
let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| StylingExample::new(window, cx)),
)
.expect("Failed to open window");
});
}
+582
View File
@@ -0,0 +1,582 @@
//! Text Example
//!
//! This example demonstrates text capabilities in GPUI:
//!
//! 1. Text Styling - Font sizes, weights, and colors
//! 2. Text Alignment - Left, center, right alignment
//! 3. Text Decoration - Underline, strikethrough
//! 4. Text Overflow - Ellipsis, truncation, line clamping
//! 5. Styled Text - Inline style variations with highlights
//! 6. Character Grid - Unicode and emoji support
#[path = "../prelude.rs"]
mod example_prelude;
use example_prelude::init_example;
use gpui::{
App, Application, Bounds, Colors, Context, FontStyle, FontWeight, Hsla, Render, StyledText,
TextOverflow, Window, WindowBounds, WindowOptions, div, prelude::*, px, relative, size,
};
// Text Styling Examples
fn text_sizes_example(colors: &Colors) -> impl IntoElement {
let text = colors.text;
let text_muted = colors.text_muted;
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Font sizes: text_xs, text_sm, text_base, text_lg, text_xl"),
)
.child(
div()
.flex()
.flex_wrap()
.items_baseline()
.gap_3()
.child(div().text_xs().text_color(text).child("Extra Small"))
.child(div().text_sm().text_color(text).child("Small"))
.child(div().text_base().text_color(text).child("Base"))
.child(div().text_lg().text_color(text).child("Large"))
.child(div().text_xl().text_color(text).child("Extra Large")),
)
}
fn text_weights_example(colors: &Colors) -> impl IntoElement {
let text = colors.text;
let text_muted = colors.text_muted;
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Font weights: THIN through BLACK"),
)
.child(
div()
.flex()
.flex_wrap()
.gap_3()
.child(
div()
.text_color(text)
.font_weight(FontWeight::THIN)
.child("Thin"),
)
.child(
div()
.text_color(text)
.font_weight(FontWeight::LIGHT)
.child("Light"),
)
.child(
div()
.text_color(text)
.font_weight(FontWeight::NORMAL)
.child("Normal"),
)
.child(
div()
.text_color(text)
.font_weight(FontWeight::MEDIUM)
.child("Medium"),
)
.child(
div()
.text_color(text)
.font_weight(FontWeight::SEMIBOLD)
.child("Semibold"),
)
.child(
div()
.text_color(text)
.font_weight(FontWeight::BOLD)
.child("Bold"),
)
.child(
div()
.text_color(text)
.font_weight(FontWeight::BLACK)
.child("Black"),
),
)
}
// Text Alignment Examples
fn text_alignment_example(colors: &Colors) -> impl IntoElement {
let text = colors.text;
let text_muted = colors.text_muted;
let surface = colors.surface;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Alignment: default (left), text_center, text_right"),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.p_2()
.bg(surface)
.rounded_sm()
.text_color(text)
.child("Left aligned (default)"),
)
.child(
div()
.p_2()
.bg(surface)
.rounded_sm()
.text_center()
.text_color(text)
.child("Center aligned"),
)
.child(
div()
.p_2()
.bg(surface)
.rounded_sm()
.text_right()
.text_color(text)
.child("Right aligned"),
),
)
}
// Text Decoration Examples
fn text_decoration_example(colors: &Colors) -> impl IntoElement {
let text = colors.text;
let text_muted = colors.text_muted;
let accent = colors.accent;
let error = colors.error;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Decorations: underline, strikethrough, italic"),
)
.child(
div()
.flex()
.flex_wrap()
.gap_4()
.child(
div()
.text_color(text)
.text_decoration_1()
.text_decoration_color(accent)
.child("Underlined text"),
)
.child(
div()
.text_color(text)
.line_through()
.text_decoration_color(error)
.child("Strikethrough text"),
)
.child(div().text_color(text).italic().child("Italic text")),
)
}
// Text Overflow Examples
fn text_overflow_example(colors: &Colors) -> impl IntoElement {
let text = colors.text;
let text_muted = colors.text_muted;
let surface = colors.surface;
let border = colors.border;
let long_text = "The quick brown fox jumps over the lazy dog. This is a long sentence that will overflow its container.";
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Overflow handling: ellipsis, truncate, line_clamp"),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("text_ellipsis (single line):"),
)
.child(
div()
.p_2()
.bg(surface)
.border_1()
.border_color(border)
.rounded_sm()
.text_color(text)
.overflow_hidden()
.text_ellipsis()
.child(long_text),
),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("line_clamp(2):"),
)
.child(
div()
.p_2()
.bg(surface)
.border_1()
.border_color(border)
.rounded_sm()
.text_color(text)
.overflow_hidden()
.text_ellipsis()
.line_clamp(2)
.child(long_text),
),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("truncate (hard cut):"),
)
.child(
div()
.p_2()
.bg(surface)
.border_1()
.border_color(border)
.rounded_sm()
.text_color(text)
.overflow_hidden()
.text_overflow(TextOverflow::Truncate("".into()))
.child(long_text),
),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("whitespace_nowrap:"),
)
.child(
div()
.p_2()
.bg(surface)
.border_1()
.border_color(border)
.rounded_sm()
.text_color(text)
.overflow_hidden()
.whitespace_nowrap()
.child(long_text),
),
),
)
}
// Styled Text Examples
fn styled_text_example(colors: &Colors) -> impl IntoElement {
let text = colors.text;
let text_muted = colors.text_muted;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("StyledText with inline highlights"),
)
.child(div().text_lg().text_color(text).child(
StyledText::new("Bold Italic Normal Semibold").with_highlights([
(0..4, FontWeight::BOLD.into()),
(5..11, FontStyle::Italic.into()),
(19..27, FontWeight::SEMIBOLD.into()),
]),
))
}
// Character Grid Example
fn character_grid_example(colors: &Colors) -> impl IntoElement {
let text = colors.text;
let text_muted = colors.text_muted;
let surface = colors.surface;
let border = colors.border;
let characters = [
// Latin
"A", "B", "C", "D", "E", "a", "b", "c", "d", "e", // Numbers
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", // Greek
"α", "β", "γ", "δ", "ε", "θ", "λ", "π", "σ", "ω", // Cyrillic
"Д", "Ж", "И", "Л", "Ф", "Ц", "Ш", "Щ", "Ы", "Я", // CJK
"你", "好", "世", "界", "日", "本", "語", "中", "文", "字", // Symbols
"→", "←", "↑", "↓", "•", "★", "♠", "♥", "♦", "♣", // Emoji
"😀", "🎉", "🚀", "💡", "🔥", "✨", "🎨", "📚", "🎵", "❤️",
];
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Unicode and emoji support"),
)
.child(
div()
.p_2()
.bg(surface)
.border_1()
.border_color(border)
.rounded_md()
.child(
div()
.grid()
.grid_cols(10)
.gap_1()
.children(characters.iter().map(|c| {
div()
.flex()
.items_center()
.justify_center()
.size_8()
.text_lg()
.text_color(text)
.line_height(relative(1.0))
.child(*c)
})),
),
)
}
// Line Height Example
fn line_height_example(colors: &Colors) -> impl IntoElement {
let text = colors.text;
let text_muted = colors.text_muted;
let surface = colors.surface;
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_xs()
.text_color(text_muted)
.child("Line height: relative(1.0), relative(1.5), relative(2.0)"),
)
.child(
div()
.flex()
.gap_3()
.child(
div()
.flex_1()
.p_2()
.bg(surface)
.rounded_sm()
.text_color(text)
.text_sm()
.line_height(relative(1.0))
.child("Tight\nline\nheight"),
)
.child(
div()
.flex_1()
.p_2()
.bg(surface)
.rounded_sm()
.text_color(text)
.text_sm()
.line_height(relative(1.5))
.child("Normal\nline\nheight"),
)
.child(
div()
.flex_1()
.p_2()
.bg(surface)
.rounded_sm()
.text_color(text)
.text_sm()
.line_height(relative(2.0))
.child("Loose\nline\nheight"),
),
)
}
// Main Application View
struct TextExample;
impl Render for TextExample {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
div()
.id("main")
.size_full()
.p_4()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_4()
.max_w(px(600.))
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(FontWeight::BOLD)
.text_color(colors.text)
.child("Text & Typography"),
)
.child(
div().text_sm().text_color(colors.text_muted).child(
"Font styling, alignment, overflow, and unicode support",
),
),
)
.child(section(&colors, "Font Sizes", text_sizes_example(&colors)))
.child(section(
&colors,
"Font Weights",
text_weights_example(&colors),
))
.child(section(
&colors,
"Text Alignment",
text_alignment_example(&colors),
))
.child(section(
&colors,
"Text Decoration",
text_decoration_example(&colors),
))
.child(section(
&colors,
"Line Height",
line_height_example(&colors),
))
.child(section(
&colors,
"Styled Text",
styled_text_example(&colors),
))
.child(section(
&colors,
"Text Overflow",
text_overflow_example(&colors),
))
.child(section(
&colors,
"Character Grid",
character_grid_example(&colors),
)),
)
}
}
fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
let surface: Hsla = colors.surface.into();
div()
.flex()
.flex_col()
.gap_2()
.p_3()
.bg(surface.opacity(0.5))
.rounded_lg()
.child(
div()
.text_sm()
.font_weight(FontWeight::SEMIBOLD)
.text_color(colors.text)
.child(title),
)
.child(content)
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(650.), px(900.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| TextExample),
)
.expect("Failed to open window");
init_example(cx, "Text");
});
}
+214
View File
@@ -0,0 +1,214 @@
use gpui::{
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
};
actions!(example, [Tab, TabPrev, Quit]);
struct Example {
focus_handle: FocusHandle,
items: Vec<(FocusHandle, &'static str)>,
message: SharedString,
}
impl Example {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let items = vec![
(
cx.focus_handle().tab_index(1).tab_stop(true),
"Button with .focus() - always shows border when focused",
),
(
cx.focus_handle().tab_index(2).tab_stop(true),
"Button with .focus_visible() - only shows border with keyboard",
),
(
cx.focus_handle().tab_index(3).tab_stop(true),
"Button with both .focus() and .focus_visible()",
),
];
let focus_handle = cx.focus_handle();
window.focus(&focus_handle);
Self {
focus_handle,
items,
message: SharedString::from(
"Try clicking vs tabbing! Click shows no border, Tab shows border.",
),
}
}
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
self.message = SharedString::from("Pressed Tab - focus-visible border should appear!");
}
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
window.focus_prev();
self.message =
SharedString::from("Pressed Shift-Tab - focus-visible border should appear!");
}
fn on_quit(&mut self, _: &Quit, _window: &mut Window, cx: &mut Context<Self>) {
cx.quit();
}
}
impl Render for Example {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn button_base(id: impl Into<ElementId>, label: &'static str) -> Stateful<Div> {
div()
.id(id)
.h_16()
.w_full()
.flex()
.justify_center()
.items_center()
.bg(gpui::rgb(0x2563eb))
.text_color(gpui::white())
.rounded_md()
.cursor_pointer()
.hover(|style| style.bg(gpui::rgb(0x1d4ed8)))
.child(label)
}
div()
.id("app")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.on_action(cx.listener(Self::on_quit))
.size_full()
.flex()
.flex_col()
.p_8()
.gap_6()
.bg(gpui::rgb(0xf3f4f6))
.child(
div()
.text_2xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(gpui::rgb(0x111827))
.child("CSS focus-visible Demo"),
)
.child(
div()
.p_4()
.rounded_md()
.bg(gpui::rgb(0xdbeafe))
.text_color(gpui::rgb(0x1e3a8a))
.child(self.message.clone()),
)
.child(
div()
.flex()
.flex_col()
.gap_4()
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::BOLD)
.text_color(gpui::rgb(0x374151))
.child("1. Regular .focus() - always visible:"),
)
.child(
button_base("button1", self.items[0].1)
.track_focus(&self.items[0].0)
.focus(|style| {
style.border_4().border_color(gpui::rgb(0xfbbf24))
})
.on_click(cx.listener(|this, _, _, cx| {
this.message =
"Clicked button 1 - focus border is visible!".into();
cx.notify();
})),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::BOLD)
.text_color(gpui::rgb(0x374151))
.child("2. New .focus_visible() - only keyboard:"),
)
.child(
button_base("button2", self.items[1].1)
.track_focus(&self.items[1].0)
.focus_visible(|style| {
style.border_4().border_color(gpui::rgb(0x10b981))
})
.on_click(cx.listener(|this, _, _, cx| {
this.message =
"Clicked button 2 - no border! Try Tab instead.".into();
cx.notify();
})),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::BOLD)
.text_color(gpui::rgb(0x374151))
.child(
"3. Both .focus() (yellow) and .focus_visible() (green):",
),
)
.child(
button_base("button3", self.items[2].1)
.track_focus(&self.items[2].0)
.focus(|style| {
style.border_4().border_color(gpui::rgb(0xfbbf24))
})
.focus_visible(|style| {
style.border_4().border_color(gpui::rgb(0x10b981))
})
.on_click(cx.listener(|this, _, _, cx| {
this.message =
"Clicked button 3 - yellow border. Tab shows green!"
.into();
cx.notify();
})),
),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.bind_keys([
KeyBinding::new("tab", Tab, None),
KeyBinding::new("shift-tab", TabPrev, None),
KeyBinding::new("cmd-q", Quit, None),
]);
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| Example::new(window, cx)),
)
.unwrap();
cx.activate(true);
});
}
+41
View File
@@ -0,0 +1,41 @@
use gpui::{App, Application, Context, Render, Window, WindowOptions, div, img, prelude::*};
use std::path::PathBuf;
struct GifViewer {
gif_path: PathBuf,
}
impl GifViewer {
fn new(gif_path: PathBuf) -> Self {
Self { gif_path }
}
}
impl Render for GifViewer {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(
img(self.gif_path.clone())
.size_full()
.object_fit(gpui::ObjectFit::Contain)
.id("gif"),
)
}
}
fn main() {
env_logger::init();
Application::new().run(|cx: &mut App| {
let gif_path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif");
cx.open_window(
WindowOptions {
focus: true,
..Default::default()
},
|_, cx| cx.new(|_| GifViewer::new(gif_path)),
)
.unwrap();
cx.activate(true);
});
}
+257
View File
@@ -0,0 +1,257 @@
use gpui::{
App, Application, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas,
div, linear_color_stop, linear_gradient, point, prelude::*, px, size,
};
struct GradientViewer {
color_space: ColorSpace,
}
impl GradientViewer {
fn new() -> Self {
Self {
color_space: ColorSpace::default(),
}
}
}
impl Render for GradientViewer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let color_space = self.color_space;
div()
.bg(gpui::white())
.size_full()
.p_4()
.flex()
.flex_col()
.gap_3()
.child(
div()
.flex()
.gap_2()
.justify_between()
.items_center()
.child("Gradient Examples")
.child(
div().flex().gap_2().items_center().child(
div()
.id("method")
.flex()
.px_3()
.py_1()
.text_sm()
.bg(gpui::black())
.text_color(gpui::white())
.child(format!("{}", color_space))
.active(|this| this.opacity(0.8))
.on_click(cx.listener(move |this, _, _, cx| {
this.color_space = match this.color_space {
ColorSpace::Oklab => ColorSpace::Srgb,
ColorSpace::Srgb => ColorSpace::Oklab,
};
cx.notify();
})),
),
),
)
.child(
div()
.flex()
.flex_1()
.gap_3()
.child(
div()
.size_full()
.rounded_xl()
.flex()
.items_center()
.justify_center()
.bg(gpui::red())
.text_color(gpui::white())
.child("Solid Color"),
)
.child(
div()
.size_full()
.rounded_xl()
.flex()
.items_center()
.justify_center()
.bg(gpui::blue())
.text_color(gpui::white())
.child("Solid Color"),
),
)
.child(
div()
.flex()
.flex_1()
.gap_3()
.h_24()
.text_color(gpui::white())
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
45.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::blue(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
135.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::green(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
225.,
linear_color_stop(gpui::green(), 0.),
linear_color_stop(gpui::blue(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
315.,
linear_color_stop(gpui::green(), 0.),
linear_color_stop(gpui::yellow(), 1.),
)
.color_space(color_space)),
),
)
.child(
div()
.flex()
.flex_1()
.gap_3()
.h_24()
.text_color(gpui::white())
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
0.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::white(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
90.,
linear_color_stop(gpui::blue(), 0.),
linear_color_stop(gpui::white(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
180.,
linear_color_stop(gpui::green(), 0.),
linear_color_stop(gpui::white(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
360.,
linear_color_stop(gpui::yellow(), 0.),
linear_color_stop(gpui::white(), 1.),
)
.color_space(color_space)),
),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
0.,
linear_color_stop(gpui::green(), 0.05),
linear_color_stop(gpui::yellow(), 0.95),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
90.,
linear_color_stop(gpui::blue(), 0.05),
linear_color_stop(gpui::red(), 0.95),
)
.color_space(color_space)),
)
.child(
div()
.flex()
.flex_1()
.gap_3()
.child(
div().flex().flex_1().gap_3().child(
div().flex_1().rounded_xl().bg(linear_gradient(
90.,
linear_color_stop(gpui::blue(), 0.5),
linear_color_stop(gpui::red(), 0.5),
)
.color_space(color_space)),
),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
180.,
linear_color_stop(gpui::green(), 0.),
linear_color_stop(gpui::blue(), 0.5),
)
.color_space(color_space)),
),
)
.child(div().h_24().child(canvas(
move |_, _, _| {},
move |bounds, _, window, _| {
let size = size(bounds.size.width * 0.8, px(80.));
let square_bounds = Bounds {
origin: point(
bounds.size.width.half() - size.width.half(),
bounds.origin.y,
),
size,
};
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut builder = gpui::PathBuilder::fill();
builder.move_to(square_bounds.bottom_left());
builder
.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
builder.line_to(
square_bounds.top_right() + point(-horizontal_offset, vertical_offset),
);
builder.line_to(square_bounds.bottom_right());
builder.line_to(square_bounds.bottom_left());
let path = builder.build().unwrap();
window.paint_path(
path,
linear_gradient(
180.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::blue(), 1.),
)
.color_space(color_space),
);
},
)))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.open_window(
WindowOptions {
focus: true,
..Default::default()
},
|_, cx| cx.new(|_| GradientViewer::new()),
)
.unwrap();
cx.activate(true);
});
}
+106
View File
@@ -0,0 +1,106 @@
use gpui::{
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
prelude::*, px, rgb, size,
};
struct HelloWorld {
text: SharedString,
}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.bg(rgb(0x505050))
.size(px(500.0))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(format!("Hello, {}!", &self.text))
.child(
div()
.flex()
.gap_2()
.child(
div()
.size_8()
.bg(gpui::red())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::green())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::blue())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::yellow())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::black())
.border_1()
.border_dashed()
.rounded_md()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::white())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::black()),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| {
cx.new(|_| HelloWorld {
text: "World".into(),
})
},
)
.unwrap();
cx.activate(true);
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" fill="none"
stroke="currentColor" stroke-width="40" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-grip">
<circle cx="240" cy="240" r="50" stroke="#44403c" />
<circle cx="100" cy="100" r="20" stroke="#65a30d" />
<circle cx="240" cy="100" r="30" stroke="#dc2626" />
<circle cx="380" cy="100" r="20" stroke="#d97706" />
<circle cx="380" cy="240" r="30" stroke="#06b6d4" />
<circle cx="100" cy="240" r="30" stroke="#3b82f666" />
<circle cx="240" cy="380" r="30" stroke="#7c3aed" />
<circle cx="380" cy="380" r="20" stroke="#c026d3" />
<circle cx="100" cy="380" r="20" stroke="#e11d4866" />
</svg>

After

Width:  |  Height:  |  Size: 746 B

+194
View File
@@ -0,0 +1,194 @@
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use gpui::{
App, AppContext, Application, AssetSource, Bounds, Context, ImageSource, KeyBinding, Menu,
MenuItem, Point, SharedString, SharedUri, TitlebarOptions, Window, WindowBounds, WindowOptions,
actions, div, img, prelude::*, px, rgb, size,
};
use reqwest_client::ReqwestClient;
struct Assets {
base: PathBuf,
}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
fs::read(self.base.join(path))
.map(|data| Some(std::borrow::Cow::Owned(data)))
.map_err(|e| e.into())
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
fs::read_dir(self.base.join(path))
.map(|entries| {
entries
.filter_map(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().into_string().ok())
.map(SharedString::from)
})
.collect()
})
.map_err(|e| e.into())
}
}
#[derive(IntoElement)]
struct ImageContainer {
text: SharedString,
src: ImageSource,
}
impl ImageContainer {
pub fn new(text: impl Into<SharedString>, src: impl Into<ImageSource>) -> Self {
Self {
text: text.into(),
src: src.into(),
}
}
}
impl RenderOnce for ImageContainer {
fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement {
div().child(
div()
.flex_row()
.size_full()
.gap_4()
.child(self.text)
.child(img(self.src).size(px(256.0))),
)
}
}
struct ImageShowcase {
local_resource: Arc<std::path::Path>,
remote_resource: SharedUri,
asset_resource: SharedString,
}
impl Render for ImageShowcase {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("main")
.bg(gpui::white())
.overflow_y_scroll()
.p_5()
.size_full()
.child(
div()
.flex()
.flex_col()
.justify_center()
.items_center()
.gap_8()
.child(img(
"https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg",
))
.child(
div()
.flex()
.flex_row()
.justify_center()
.items_center()
.gap_8()
.child(ImageContainer::new(
"Image loaded from a local file",
self.local_resource.clone(),
))
.child(ImageContainer::new(
"Image loaded from a remote resource",
self.remote_resource.clone(),
))
.child(ImageContainer::new(
"Image loaded from an asset",
self.asset_resource.clone(),
)),
)
.child(
div()
.flex()
.flex_row()
.gap_8()
.child(
div()
.flex_col()
.child("Auto Width")
.child(img("https://picsum.photos/800/400").h(px(180.))),
)
.child(
div()
.flex_col()
.child("Auto Height")
.child(img("https://picsum.photos/800/400").w(px(180.))),
),
)
.child(
div()
.flex()
.flex_col()
.justify_center()
.items_center()
.w_full()
.border_1()
.border_color(rgb(0xC0C0C0))
.child("image with max width 100%")
.child(img("https://picsum.photos/800/400").max_w_full()),
),
)
}
}
actions!(image, [Quit]);
fn main() {
env_logger::init();
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
Application::new()
.with_assets(Assets {
base: manifest_dir.join("examples"),
})
.run(move |cx: &mut App| {
let http_client = ReqwestClient::user_agent("gpui example").unwrap();
cx.set_http_client(Arc::new(http_client));
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.set_menus(vec![Menu {
name: "Image".into(),
items: vec![MenuItem::action("Quit", Quit)],
}]);
let window_options = WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some(SharedString::from("Image Example")),
appears_transparent: false,
..Default::default()
}),
window_bounds: Some(WindowBounds::Windowed(Bounds {
size: size(px(1100.), px(600.)),
origin: Point::new(px(200.), px(200.)),
})),
..Default::default()
};
cx.open_window(window_options, |_, cx| {
cx.new(|_| ImageShowcase {
// Relative path to your root project path
local_resource: manifest_dir.join("examples/image/app-icon.png").into(),
remote_resource: "https://picsum.photos/800/400".into(),
asset_resource: "image/color.svg".into(),
})
})
.unwrap();
});
}
+213
View File
@@ -0,0 +1,213 @@
use std::{path::Path, sync::Arc, time::Duration};
use gpui::{
Animation, AnimationExt, App, Application, Asset, AssetLogger, AssetSource, Bounds, Context,
Hsla, ImageAssetLoader, ImageCacheError, ImgResourceLoader, LOADING_DELAY, Length, RenderImage,
Resource, SharedString, Window, WindowBounds, WindowOptions, black, div, img, prelude::*,
pulsating_between, px, red, size,
};
struct Assets {}
impl AssetSource for Assets {
fn load(&self, path: &str) -> anyhow::Result<Option<std::borrow::Cow<'static, [u8]>>> {
std::fs::read(path)
.map(Into::into)
.map_err(Into::into)
.map(Some)
}
fn list(&self, path: &str) -> anyhow::Result<Vec<SharedString>> {
Ok(std::fs::read_dir(path)?
.filter_map(|entry| {
Some(SharedString::from(
entry.ok()?.path().to_string_lossy().into_owned(),
))
})
.collect::<Vec<_>>())
}
}
const IMAGE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/image/app-icon.png");
#[derive(Copy, Clone, Hash)]
struct LoadImageParameters {
timeout: Duration,
fail: bool,
}
struct LoadImageWithParameters {}
impl Asset for LoadImageWithParameters {
type Source = LoadImageParameters;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
parameters: Self::Source,
cx: &mut App,
) -> impl std::future::Future<Output = Self::Output> + Send + 'static {
let timer = cx.background_executor().timer(parameters.timeout);
let data = AssetLogger::<ImageAssetLoader>::load(
Resource::Path(Path::new(IMAGE).to_path_buf().into()),
cx,
);
async move {
timer.await;
if parameters.fail {
log::error!("Intentionally failed to load image");
Err(anyhow::anyhow!("Failed to load image").into())
} else {
data.await
}
}
}
}
struct ImageLoadingExample {}
impl ImageLoadingExample {
fn loading_element() -> impl IntoElement {
div().size_full().flex_none().p_0p5().rounded_xs().child(
div().size_full().with_animation(
"loading-bg",
Animation::new(Duration::from_secs(3))
.repeat()
.with_easing(pulsating_between(0.04, 0.24)),
move |this, delta| this.bg(black().opacity(delta)),
),
)
}
fn fallback_element() -> impl IntoElement {
let fallback_color: Hsla = black().opacity(0.5);
div().size_full().flex_none().p_0p5().child(
div()
.size_full()
.flex()
.items_center()
.justify_center()
.rounded_xs()
.text_sm()
.text_color(fallback_color)
.border_1()
.border_color(fallback_color)
.child("?"),
)
}
}
impl Render for ImageLoadingExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().flex().flex_col().size_full().justify_around().child(
div().flex().flex_row().w_full().justify_around().child(
div()
.flex()
.bg(gpui::white())
.size(Length::Definite(px(300.0).into()))
.justify_center()
.items_center()
.child({
let image_source = LoadImageParameters {
timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)),
fail: false,
};
// Load within the 'loading delay', should not show loading fallback
img(move |window: &mut Window, cx: &mut App| {
window.use_asset::<LoadImageWithParameters>(&image_source, cx)
})
.id("image-1")
.border_1()
.size_12()
.with_fallback(|| Self::fallback_element().into_any_element())
.border_color(red())
.with_loading(|| Self::loading_element().into_any_element())
.on_click(move |_, _, cx| {
cx.remove_asset::<LoadImageWithParameters>(&image_source);
})
})
.child({
// Load after a long delay
let image_source = LoadImageParameters {
timeout: Duration::from_secs(5),
fail: false,
};
img(move |window: &mut Window, cx: &mut App| {
window.use_asset::<LoadImageWithParameters>(&image_source, cx)
})
.id("image-2")
.with_fallback(|| Self::fallback_element().into_any_element())
.with_loading(|| Self::loading_element().into_any_element())
.size_12()
.border_1()
.border_color(red())
.on_click(move |_, _, cx| {
cx.remove_asset::<LoadImageWithParameters>(&image_source);
})
})
.child({
// Fail to load image after a long delay
let image_source = LoadImageParameters {
timeout: Duration::from_secs(5),
fail: true,
};
// Fail to load after a long delay
img(move |window: &mut Window, cx: &mut App| {
window.use_asset::<LoadImageWithParameters>(&image_source, cx)
})
.id("image-3")
.with_fallback(|| Self::fallback_element().into_any_element())
.with_loading(|| Self::loading_element().into_any_element())
.size_12()
.border_1()
.border_color(red())
.on_click(move |_, _, cx| {
cx.remove_asset::<LoadImageWithParameters>(&image_source);
})
})
.child({
// Ensure that the normal image loader doesn't spam logs
let image_source = Path::new(
"this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope",
)
.to_path_buf();
img(image_source.clone())
.id("image-4")
.border_1()
.size_12()
.with_fallback(|| Self::fallback_element().into_any_element())
.border_color(red())
.with_loading(|| Self::loading_element().into_any_element())
.on_click(move |_, _, cx| {
cx.remove_asset::<ImgResourceLoader>(&image_source.clone().into());
})
}),
),
)
}
}
fn main() {
env_logger::init();
Application::new()
.with_assets(Assets {})
.run(|cx: &mut App| {
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(300.), px(300.)),
cx,
))),
..Default::default()
};
cx.open_window(options, |_, cx| {
cx.activate(false);
cx.new(|_| ImageLoadingExample {})
})
.unwrap();
});
}
+746
View File
@@ -0,0 +1,746 @@
use std::ops::Range;
use gpui::{
App, Application, Bounds, ClipboardItem, Context, CursorStyle, ElementId, ElementInputHandler,
Entity, EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, Keystroke,
LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, WindowBounds,
WindowOptions, actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative,
rgb, rgba, size, white, yellow,
};
use unicode_segmentation::*;
actions!(
text_input,
[
Backspace,
Delete,
Left,
Right,
SelectLeft,
SelectRight,
SelectAll,
Home,
End,
ShowCharacterPalette,
Paste,
Cut,
Copy,
Quit,
]
);
struct TextInput {
focus_handle: FocusHandle,
content: SharedString,
placeholder: SharedString,
selected_range: Range<usize>,
selection_reversed: bool,
marked_range: Option<Range<usize>>,
last_layout: Option<ShapedLine>,
last_bounds: Option<Bounds<Pixels>>,
is_selecting: bool,
}
impl TextInput {
fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
} else {
self.move_to(self.selected_range.start, cx)
}
}
fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
self.move_to(self.next_boundary(self.selected_range.end), cx);
} else {
self.move_to(self.selected_range.end, cx)
}
}
fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
}
fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(self.next_boundary(self.cursor_offset()), cx);
}
fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(0, cx);
self.select_to(self.content.len(), cx)
}
fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(0, cx);
}
fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(self.content.len(), cx);
}
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
}
self.replace_text_in_range(None, "", window, cx)
}
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
self.select_to(self.next_boundary(self.cursor_offset()), cx)
}
self.replace_text_in_range(None, "", window, cx)
}
fn on_mouse_down(
&mut self,
event: &MouseDownEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.is_selecting = true;
if event.modifiers.shift {
self.select_to(self.index_for_mouse_position(event.position), cx);
} else {
self.move_to(self.index_for_mouse_position(event.position), cx)
}
}
fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context<Self>) {
self.is_selecting = false;
}
fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
if self.is_selecting {
self.select_to(self.index_for_mouse_position(event.position), cx);
}
}
fn show_character_palette(
&mut self,
_: &ShowCharacterPalette,
window: &mut Window,
_: &mut Context<Self>,
) {
window.show_character_palette();
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
self.replace_text_in_range(None, &text.replace("\n", " "), window, cx);
}
}
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
self.content[self.selected_range.clone()].to_string(),
));
}
}
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
self.content[self.selected_range.clone()].to_string(),
));
self.replace_text_in_range(None, "", window, cx)
}
}
fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
self.selected_range = offset..offset;
cx.notify()
}
fn cursor_offset(&self) -> usize {
if self.selection_reversed {
self.selected_range.start
} else {
self.selected_range.end
}
}
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
if self.content.is_empty() {
return 0;
}
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
else {
return 0;
};
if position.y < bounds.top() {
return 0;
}
if position.y > bounds.bottom() {
return self.content.len();
}
line.closest_index_for_x(position.x - bounds.left())
}
fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
if self.selection_reversed {
self.selected_range.start = offset
} else {
self.selected_range.end = offset
};
if self.selected_range.end < self.selected_range.start {
self.selection_reversed = !self.selection_reversed;
self.selected_range = self.selected_range.end..self.selected_range.start;
}
cx.notify()
}
fn offset_from_utf16(&self, offset: usize) -> usize {
let mut utf8_offset = 0;
let mut utf16_count = 0;
for ch in self.content.chars() {
if utf16_count >= offset {
break;
}
utf16_count += ch.len_utf16();
utf8_offset += ch.len_utf8();
}
utf8_offset
}
fn offset_to_utf16(&self, offset: usize) -> usize {
let mut utf16_offset = 0;
let mut utf8_count = 0;
for ch in self.content.chars() {
if utf8_count >= offset {
break;
}
utf8_count += ch.len_utf8();
utf16_offset += ch.len_utf16();
}
utf16_offset
}
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
}
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
}
fn previous_boundary(&self, offset: usize) -> usize {
self.content
.grapheme_indices(true)
.rev()
.find_map(|(idx, _)| (idx < offset).then_some(idx))
.unwrap_or(0)
}
fn next_boundary(&self, offset: usize) -> usize {
self.content
.grapheme_indices(true)
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(self.content.len())
}
fn reset(&mut self) {
self.content = "".into();
self.selected_range = 0..0;
self.selection_reversed = false;
self.marked_range = None;
self.last_layout = None;
self.last_bounds = None;
self.is_selecting = false;
}
}
impl EntityInputHandler for TextInput {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
actual_range: &mut Option<Range<usize>>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<String> {
let range = self.range_from_utf16(&range_utf16);
actual_range.replace(self.range_to_utf16(&range));
Some(self.content[range].to_string())
}
fn selected_text_range(
&mut self,
_ignore_disabled_input: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<UTF16Selection> {
Some(UTF16Selection {
range: self.range_to_utf16(&self.selected_range),
reversed: self.selection_reversed,
})
}
fn marked_text_range(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Range<usize>> {
self.marked_range
.as_ref()
.map(|range| self.range_to_utf16(range))
}
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
self.marked_range = None;
}
fn replace_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
_: &mut Window,
cx: &mut Context<Self>,
) {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.content =
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
.into();
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
self.marked_range.take();
cx.notify();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range_utf16: Option<Range<usize>>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.content =
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
.into();
if !new_text.is_empty() {
self.marked_range = Some(range.start..range.start + new_text.len());
} else {
self.marked_range = None;
}
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
cx.notify();
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let last_layout = self.last_layout.as_ref()?;
let range = self.range_from_utf16(&range_utf16);
Some(Bounds::from_corners(
point(
bounds.left() + last_layout.x_for_index(range.start),
bounds.top(),
),
point(
bounds.left() + last_layout.x_for_index(range.end),
bounds.bottom(),
),
))
}
fn character_index_for_point(
&mut self,
point: gpui::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let line_point = self.last_bounds?.localize(&point)?;
let last_layout = self.last_layout.as_ref()?;
assert_eq!(last_layout.text, self.content);
let utf8_index = last_layout.index_for_x(point.x - line_point.x)?;
Some(self.offset_to_utf16(utf8_index))
}
}
struct TextElement {
input: Entity<TextInput>,
}
struct PrepaintState {
line: Option<ShapedLine>,
cursor: Option<PaintQuad>,
selection: Option<PaintQuad>,
}
impl IntoElement for TextElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = window.line_height().into();
(window.request_layout(style, [], cx), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
let input = self.input.read(cx);
let content = input.content.clone();
let selected_range = input.selected_range.clone();
let cursor = input.cursor_offset();
let style = window.text_style();
let (display_text, text_color) = if content.is_empty() {
(input.placeholder.clone(), hsla(0., 0., 0., 0.2))
} else {
(content, style.color)
};
let run = TextRun {
len: display_text.len(),
font: style.font(),
color: text_color,
background_color: None,
underline: None,
strikethrough: None,
};
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
vec![
TextRun {
len: marked_range.start,
..run.clone()
},
TextRun {
len: marked_range.end - marked_range.start,
underline: Some(UnderlineStyle {
color: Some(run.color),
thickness: px(1.0),
wavy: false,
}),
..run.clone()
},
TextRun {
len: display_text.len() - marked_range.end,
..run
},
]
.into_iter()
.filter(|run| run.len > 0)
.collect()
} else {
vec![run]
};
let font_size = style.font_size.to_pixels(window.rem_size());
let line = window
.text_system()
.shape_line(display_text, font_size, &runs, None);
let cursor_pos = line.x_for_index(cursor);
let (selection, cursor) = if selected_range.is_empty() {
(
None,
Some(fill(
Bounds::new(
point(bounds.left() + cursor_pos, bounds.top()),
size(px(2.), bounds.bottom() - bounds.top()),
),
gpui::blue(),
)),
)
} else {
(
Some(fill(
Bounds::from_corners(
point(
bounds.left() + line.x_for_index(selected_range.start),
bounds.top(),
),
point(
bounds.left() + line.x_for_index(selected_range.end),
bounds.bottom(),
),
),
rgba(0x3311ff30),
)),
None,
)
};
PrepaintState {
line: Some(line),
cursor,
selection,
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let focus_handle = self.input.read(cx).focus_handle.clone();
window.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.input.clone()),
cx,
);
if let Some(selection) = prepaint.selection.take() {
window.paint_quad(selection)
}
let line = prepaint.line.take().unwrap();
line.paint(bounds.origin, window.line_height(), window, cx)
.unwrap();
if focus_handle.is_focused(window)
&& let Some(cursor) = prepaint.cursor.take()
{
window.paint_quad(cursor);
}
self.input.update(cx, |input, _cx| {
input.last_layout = Some(line);
input.last_bounds = Some(bounds);
});
}
}
impl Render for TextInput {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.key_context("TextInput")
.track_focus(&self.focus_handle(cx))
.cursor(CursorStyle::IBeam)
.on_action(cx.listener(Self::backspace))
.on_action(cx.listener(Self::delete))
.on_action(cx.listener(Self::left))
.on_action(cx.listener(Self::right))
.on_action(cx.listener(Self::select_left))
.on_action(cx.listener(Self::select_right))
.on_action(cx.listener(Self::select_all))
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::show_character_palette))
.on_action(cx.listener(Self::paste))
.on_action(cx.listener(Self::cut))
.on_action(cx.listener(Self::copy))
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_move(cx.listener(Self::on_mouse_move))
.bg(rgb(0xeeeeee))
.line_height(px(30.))
.text_size(px(24.))
.child(
div()
.h(px(30. + 4. * 2.))
.w_full()
.p(px(4.))
.bg(white())
.child(TextElement { input: cx.entity() }),
)
}
}
impl Focusable for TextInput {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
struct InputExample {
text_input: Entity<TextInput>,
recent_keystrokes: Vec<Keystroke>,
focus_handle: FocusHandle,
}
impl Focusable for InputExample {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl InputExample {
fn on_reset_click(&mut self, _: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
self.recent_keystrokes.clear();
self.text_input
.update(cx, |text_input, _cx| text_input.reset());
cx.notify();
}
}
impl Render for InputExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.bg(rgb(0xaaaaaa))
.track_focus(&self.focus_handle(cx))
.flex()
.flex_col()
.size_full()
.child(
div()
.bg(white())
.border_b_1()
.border_color(black())
.flex()
.flex_row()
.justify_between()
.child(format!("Keyboard {}", cx.keyboard_layout().name()))
.child(
div()
.border_1()
.border_color(black())
.px_2()
.bg(yellow())
.child("Reset")
.hover(|style| {
style
.bg(yellow().blend(opaque_grey(0.5, 0.5)))
.cursor_pointer()
})
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
),
)
.child(self.text_input.clone())
.children(self.recent_keystrokes.iter().rev().map(|ks| {
format!(
"{:} {}",
ks.unparse(),
if let Some(key_char) = ks.key_char.as_ref() {
format!("-> {:?}", key_char)
} else {
"".to_owned()
}
)
}))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.bind_keys([
KeyBinding::new("backspace", Backspace, None),
KeyBinding::new("delete", Delete, None),
KeyBinding::new("left", Left, None),
KeyBinding::new("right", Right, None),
KeyBinding::new("shift-left", SelectLeft, None),
KeyBinding::new("shift-right", SelectRight, None),
KeyBinding::new("cmd-a", SelectAll, None),
KeyBinding::new("cmd-v", Paste, None),
KeyBinding::new("cmd-c", Copy, None),
KeyBinding::new("cmd-x", Cut, None),
KeyBinding::new("home", Home, None),
KeyBinding::new("end", End, None),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
]);
let window = cx
.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| {
let text_input = cx.new(|cx| TextInput {
focus_handle: cx.focus_handle(),
content: "".into(),
placeholder: "Type here...".into(),
selected_range: 0..0,
selection_reversed: false,
marked_range: None,
last_layout: None,
last_bounds: None,
is_selecting: false,
});
cx.new(|cx| InputExample {
text_input,
recent_keystrokes: vec![],
focus_handle: cx.focus_handle(),
})
},
)
.unwrap();
let view = window.update(cx, |_, _, cx| cx.entity()).unwrap();
cx.observe_keystrokes(move |ev, _, cx| {
view.update(cx, |view, cx| {
view.recent_keystrokes.push(ev.keystroke.clone());
cx.notify();
})
})
.detach();
cx.on_keyboard_layout_change({
move |cx| {
window.update(cx, |_, _, cx| cx.notify()).ok();
}
})
.detach();
window
.update(cx, |view, window, cx| {
window.focus(&view.text_input.focus_handle(cx));
cx.activate(true);
})
.unwrap();
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
});
}
+87
View File
@@ -0,0 +1,87 @@
fn main() {
#[cfg(all(target_os = "linux", feature = "wayland"))]
example::main();
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
panic!("This example requires the `wayland` feature and a linux system.");
}
#[cfg(all(target_os = "linux", feature = "wayland"))]
mod example {
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use gpui::{
App, Application, Bounds, Context, FontWeight, Size, Window, WindowBackgroundAppearance,
WindowBounds, WindowKind, WindowOptions, div, layer_shell::*, point, prelude::*, px, rems,
rgba, white,
};
struct LayerShellExample;
impl LayerShellExample {
fn new(cx: &mut Context<Self>) -> Self {
cx.spawn(async move |this, cx| {
loop {
let _ = this.update(cx, |_, cx| cx.notify());
cx.background_executor()
.timer(Duration::from_millis(500))
.await;
}
})
.detach();
LayerShellExample
}
}
impl Render for LayerShellExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let hours = (now / 3600) % 24;
let minutes = (now / 60) % 60;
let seconds = now % 60;
div()
.size_full()
.flex()
.items_center()
.justify_center()
.text_size(rems(4.5))
.font_weight(FontWeight::EXTRA_BOLD)
.text_color(white())
.bg(rgba(0x0000044))
.rounded_xl()
.child(format!("{:02}:{:02}:{:02}", hours, minutes, seconds))
}
}
pub fn main() {
Application::new().run(|cx: &mut App| {
cx.open_window(
WindowOptions {
titlebar: None,
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: point(px(0.), px(0.)),
size: Size::new(px(500.), px(200.)),
})),
app_id: Some("gpui-layer-shell-example".to_string()),
window_background: WindowBackgroundAppearance::Transparent,
kind: WindowKind::LayerShell(LayerShellOptions {
namespace: "gpui".to_string(),
anchor: Anchor::LEFT | Anchor::RIGHT | Anchor::BOTTOM,
margin: Some((px(0.), px(0.), px(40.), px(0.))),
keyboard_interactivity: KeyboardInteractivity::None,
..Default::default()
}),
..Default::default()
},
|_, cx| cx.new(LayerShellExample::new),
)
.unwrap();
});
}
}
+82
View File
@@ -0,0 +1,82 @@
use gpui::{
App, Application, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds,
WindowOptions, actions, div, prelude::*, px, rgb, size,
};
actions!(example, [CloseWindow]);
struct ExampleWindow {
focus_handle: FocusHandle,
}
impl Render for ExampleWindow {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.on_action(|_: &CloseWindow, window, _| {
window.remove_window();
})
.track_focus(&self.focus_handle)
.flex()
.flex_col()
.gap_3()
.bg(rgb(0x505050))
.size(px(500.0))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(
"Closing this window with cmd-w or the traffic lights should quit the application!",
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let mut bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.bind_keys([KeyBinding::new("cmd-w", CloseWindow, None)]);
cx.on_window_closed(|cx| {
if cx.windows().is_empty() {
cx.quit();
}
})
.detach();
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| {
cx.activate(false);
cx.new(|cx| {
let focus_handle = cx.focus_handle();
focus_handle.focus(window);
ExampleWindow { focus_handle }
})
},
)
.unwrap();
bounds.origin.x += bounds.size.width;
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
focus_handle.focus(window);
ExampleWindow { focus_handle }
})
},
)
.unwrap();
});
}
+175
View File
@@ -0,0 +1,175 @@
use std::{fs, path::PathBuf};
use anyhow::Result;
use gpui::{
App, Application, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task,
Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size, svg,
};
struct Assets {
base: PathBuf,
}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
fs::read(self.base.join(path))
.map(|data| Some(std::borrow::Cow::Owned(data)))
.map_err(|e| e.into())
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
fs::read_dir(self.base.join(path))
.map(|entries| {
entries
.filter_map(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().into_string().ok())
.map(SharedString::from)
})
.collect()
})
.map_err(|e| e.into())
}
}
struct HelloWorld {
_task: Option<Task<()>>,
opacity: f32,
animating: bool,
}
impl HelloWorld {
fn new(_window: &mut Window, _: &mut Context<Self>) -> Self {
Self {
_task: None,
opacity: 0.5,
animating: false,
}
}
fn start_animation(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
self.opacity = 0.0;
self.animating = true;
cx.notify();
}
}
impl Render for HelloWorld {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.animating {
self.opacity += 0.005;
if self.opacity >= 1.0 {
self.animating = false;
self.opacity = 1.0;
} else {
window.request_animation_frame();
}
}
div()
.flex()
.flex_row()
.size_full()
.bg(rgb(0xe0e0e0))
.text_xl()
.child(
div()
.flex()
.size_full()
.justify_center()
.items_center()
.border_1()
.text_color(gpui::blue())
.child(div().child("This is background text.")),
)
.child(
div()
.id("panel")
.on_click(cx.listener(Self::start_animation))
.absolute()
.top_8()
.left_8()
.right_8()
.bottom_8()
.opacity(self.opacity)
.flex()
.justify_center()
.items_center()
.bg(gpui::white())
.border_3()
.border_color(gpui::red())
.text_color(gpui::yellow())
.child(
div()
.flex()
.flex_col()
.gap_2()
.justify_center()
.items_center()
.size(px(300.))
.bg(gpui::blue())
.border_3()
.border_color(gpui::black())
.shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.5),
blur_radius: px(1.0),
spread_radius: px(5.0),
offset: point(px(10.0), px(10.0)),
}])
.child(img("image/app-icon.png").size_8())
.child("Opacity Panel (Click to test)")
.child(
div()
.id("deep-level-text")
.flex()
.justify_center()
.items_center()
.p_4()
.bg(gpui::black())
.text_color(gpui::white())
.text_decoration_2()
.text_decoration_wavy()
.text_decoration_color(gpui::red())
.child(format!("opacity: {:.1}", self.opacity)),
)
.child(
svg()
.path("image/arrow_circle.svg")
.text_color(gpui::black())
.text_2xl()
.size_8(),
)
.child(
div()
.flex()
.children(["🎊", "✈️", "🎉", "🎈", "🎁", "🎂"].map(|emoji| {
div()
.child(emoji.to_string())
.hover(|style| style.opacity(0.5))
})),
)
.child(img("image/black-cat-typing.gif").size_12()),
),
)
}
}
fn main() {
Application::new()
.with_assets(Assets {
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
})
.run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| HelloWorld::new(window, cx)),
)
.unwrap();
cx.activate(true);
});
}
+60
View File
@@ -0,0 +1,60 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
};
struct Scrollable {}
impl Render for Scrollable {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.id("vertical")
.p_4()
.overflow_scroll()
.bg(gpui::white())
.child("Example for test 2 way scroll in nested layout")
.child(
div()
.h(px(5000.))
.border_1()
.border_color(gpui::blue())
.bg(gpui::blue().opacity(0.05))
.p_4()
.child(
div()
.mb_5()
.w_full()
.id("horizontal")
.overflow_scroll()
.child(
div()
.w(px(2000.))
.h(px(150.))
.bg(gpui::green().opacity(0.1))
.hover(|this| this.bg(gpui::green().opacity(0.2)))
.border_1()
.border_color(gpui::green())
.p_4()
.child("Scroll Horizontal"),
),
)
.child("Scroll Vertical"),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Scrollable {}),
)
.unwrap();
cx.activate(true);
});
}
+240
View File
@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<!-- Retrieved from https://commons.wikimedia.org/wiki/Category:SVG_files#/media/File:Dragon_clip_art.svg -->
<!-- License: CC BY-SA 3.0 -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="273.55759"
height="297.81952"
id="svg2"
version="1.1"
inkscape:version="0.48.2 r9819"
sodipodi:docname="New document 1">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.78061715"
inkscape:cx="112.41895"
inkscape:cy="87.896202"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1024"
inkscape:window-height="712"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(12.737105,20.750754)">
<path
style="fill:#ff0000;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 73.44622,98.502944 95.574248,123.45583 103.10719,106.5067 73.44622,75.904107 c -3.686689,-4.753593 -11.648074,-6.667124 -21.18641,-7.532946 2.554639,2.715713 6.322156,2.39923 6.591328,10.82861 0.824915,3.302369 -2.23752,5.956846 -8.945373,8.003754 C 38.790859,84.969312 29.325417,85.209295 20.7156,86.732716 10.937191,88.437808 3.3459273,92.876831 -1.4124273,100.85699 L 14.595082,96.148898 c 7.184903,-0.279702 11.921181,1.889221 10.3578,10.357802 -1.994238,5.38177 -5.541365,9.21065 -11.299418,10.82861 -13.89013312,3.69644 -14.20560941,10.40948 -17.4199368,16.47832 2.98162616,-2.35421 5.619737,-5.05194 12.2410366,-3.76648 C 12.049969,137.75238 6.2740844,144.2887 0,150.76275 c -8.2557969,11.86874 -11.815365,18.89318 -11.299418,29.19017 -3.600096,18.58594 5.3801474,23.59338 8.4745634,34.36906 C -3.7567158,203.36083 -1.3904441,200.64501 0,195.48962 l 7.0621365,-10.3578 c -0.8666538,6.37974 -2.2779903,12.39636 0.4708091,21.18641 12.3493154,22.89597 28.0783714,46.43569 15.0658914,64.50084 l 15.065891,-22.59883 c 5.315347,9.96887 6.398134,20.64317 25.894501,28.24854 l 26.836118,-1.41242 c 0,0 -66.854892,-105.46124 -67.79651,-107.34448 -0.941618,-1.88323 53.201428,-70.621364 53.201428,-70.621364 z"
id="path3790"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccccccccscc" />
<path
style="fill:#b4daff;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 142.47916,274.20076 160.07509,273.64393 96.045057,235.03758 65.913274,202.08095 58.851138,160.64975 90.866157,124.86825 68.26732,97.561326 c -12.595581,1.14506 -19.978043,4.375364 -26.36531,8.003754 -10.473991,7.25982 -17.726182,17.74144 -24.482073,28.71936 -6.479109,23.36435 -7.280245,49.16212 3.766473,80.03754 11.380348,21.89016 28.33967,38.20135 48.493337,51.31819 l 18.835517,9.59325 z"
id="path3788"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccc" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 156.77943,275.05636 17.41994,-9.41619 0.94162,-16.47831 -25.4237,-4.70809 -12.71184,21.1864 z"
id="path2998"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 149.24649,255.75318 c 0,0 6.12051,-17.41993 4.23728,-17.41993 -1.88324,0 -22.59884,-3.76648 -22.59884,-3.76648 l -17.89075,17.41994 24.95289,13.65346 z"
id="path3000"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 95.103438,237.39163 21.186412,16.47832 16.00751,-9.88699 0.47081,-18.36156 -23.54046,-5.64971 0.47081,10.82861 z"
id="path3002"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 90.866157,216.67603 2.824854,-11.77023 17.419939,12.71185 -1.88324,15.06589 -16.007508,5.1789 -20.244791,-18.36156 z"
id="path3004"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 62.61761,202.55176 18.832364,-8.47457 13.653464,8.47457 -2.824854,16.0075 -19.303173,0 z"
id="path3006"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 57.909519,180.42373 18.832364,0.47081 9.416183,10.3578 -5.178901,9.88699 -21.186409,-0.47081 z"
id="path3008"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 56.497092,159.70813 18.361555,2.82485 6.591327,13.18266 -8.474563,9.88699 -16.949128,-5.1789 z"
id="path3780"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 89.924538,126.28068 c -9.664322,8.18228 -14.377332,17.78125 -10.357799,33.42745 l -13.182656,8.00375 -9.886991,-8.47457 c 3.769665,-18.09309 15.869815,-31.15434 33.427446,-32.95663 z"
id="path3786"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#80afb8;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 133.77127,121.15445 C 102.29445,108.16921 71.200271,103.60211 38.733769,68.843895 58.122127,89.497163 77.85045,109.22179 101.42657,118.35923 L 69.081877,100.38995 c 11.499182,15.61467 23.598373,27.80948 42.327623,31.94538 -11.24421,1.21671 -20.380767,-7.051 -29.549472,-15.17405 24.733762,29.06649 40.923142,28.22765 55.105772,20.36517 -8.16571,-3.14828 -15.52424,-7.10373 -21.56313,-12.37883 z"
id="path3798"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#ffaf29;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 138.56307,117.95991 128.58014,122.3524 87.051152,98.792686 C 67.454225,70.758132 70.300802,54.846161 60.696216,35.301249 68.447691,43.355436 74.77626,52.358226 85.453883,58.461647 79.819169,46.517664 80.927543,37.270916 80.262759,27.314905 c 3.166784,15.761986 9.719966,24.751173 15.972689,34.34128 -1.47816,-6.276965 -3.350278,-13.144867 -2.795221,-16.372006 5.304337,8.158007 10.295783,16.503749 20.764493,21.56313 3.59095,0.562069 10.55437,5.339728 18.36859,11.180882 7.24446,7.245148 12.72056,16.848114 16.77133,28.351519 z"
id="path3808"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccc" />
<path
style="fill:#00d7d6;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 113.8054,124.34899 15.57338,-3.59386 12.37883,-4.7918 6.78839,-10.78157 -0.79863,-6.389074 17.17064,13.976104 14.37542,7.18771 9.58361,13.9761 14.77474,-1.99659 c 4.39249,1.57017 8.78497,2.92164 13.17746,7.58703 5.76188,6.36887 11.20761,14.0023 17.96928,16.37201 4.23518,-1.76668 8.10347,-4.0837 14.37542,-2.79522 5.61406,0.5029 9.02747,3.75662 10.38225,9.58361 2.60388,11.36945 -3.06132,11.71345 -6.38908,15.17405 0.037,9.48753 -3.51318,15.38783 -8.78497,19.56654 -1.67328,-6.90259 -4.18558,-12.12712 -7.98635,-14.77473 -13.4304,-10.55772 -26.90659,-1.42587 -40.33104,-8.38566 -17.65083,-10.24469 -27.78384,-26.45774 -37.93513,-31.14674 -9.67041,-1.16902 -16.20698,-3.38266 -22.76109,-5.59045 z"
id="path3810"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 154.53576,133.53328 c 7.83618,0.93458 14.84203,2.00755 16.37201,3.99317 12.61186,7.01602 21.03384,26.20016 31.54606,30.34811 -3.00497,-5.51683 -3.48439,-10.71796 1.99658,-15.17405 7.22535,-3.03546 10.55836,-0.45702 14.77474,2.3959 6.02262,6.9248 11.74338,16.2645 18.76791,15.17406"
id="path3814"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 209.24222,167.47525 c 0.33375,-1.59661 0.12171,-3.55709 1.59727,-4.39249 0.98845,-1.35075 2.83593,-0.98344 4.39249,-1.19796 4.8086,3.66257 4.22462,5.91709 5.98975,8.78498"
id="path3816"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 219.22515,144.31485 11.18088,11.18088 13.57679,8.78498"
id="path3818"
inkscape:connector-curvature="0" />
<path
style="fill:#80b7c4;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 146.94873,99.59132 c -8.40512,-14.108233 -7.32446,-24.976745 -9.98293,-38.334452 2.28959,7.959558 5.31902,15.17927 10.78157,19.96586 -5.86678,-10.515353 -3.24744,-21.030706 -1.99659,-31.546059 1.01072,9.17552 3.11766,18.229239 13.57679,26.354936 5.88809,4.549002 13.0362,8.257998 15.97269,14.774736 6.35347,8.175649 4.50901,17.892639 8.78497,23.160399 -1.99075,-8.86593 -0.24555,-12.75061 0.79864,-17.569958 0.68276,7.697158 4.15451,11.675658 7.98634,15.174058 l 0.39932,-15.174058 c 8.25592,15.027878 19.79844,10.971398 23.95903,28.750838 l -6.78839,8.38566 -9.58361,-1.19795 -6.38908,4.39249 -8.38566,-2.79522 -9.58361,-13.17747 z"
id="path3820"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccc" />
<path
style="fill:#333333;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 163.10163,110.79268 c -0.23573,2.97072 13.46423,4.4681 19.60954,10.89419 2.70861,3.95943 5.16753,8.73304 8.71536,13.07303 -5.87363,0.0346 -11.0482,1.74698 -18.05324,-0.93378 -7.43617,-4.47099 -6.44315,-11.29641 -9.33788,-18.98702 z"
id="path3822"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#ffcd71;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 169.40234,116.31052 c 0.19359,4.0262 -0.47444,12.71073 6.79118,16.41203 3.5528,1.27665 7.10559,1.14632 10.65839,0.94322 -2.15752,-3.14407 -3.90555,-6.28813 -5.565,-9.4322 -2.11396,-2.77299 -6.25107,-5.40146 -11.88457,-7.92305 z"
id="path3824"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#009d80;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 181.19259,123.29035 c -3.74814,1.60851 -4.45804,5.32041 -4.99907,9.14923 2.28898,-1.15726 4.81391,-2.06949 5.65932,-8.11169 z"
id="path3826"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#de4000;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 137.41517,138.64674 15.74549,16.46119 -4.77136,20.03971 c -5.06394,19.66324 4.09464,25.78123 11.9284,33.16095 10.22958,3.59957 20.40736,7.66534 31.25241,5.72563 -6.95999,-6.56919 -15.51616,-8.88191 -20.75542,-20.03971 -6.49636,-7.22625 -3.74543,-16.18636 -0.23857,-25.28821 4.60728,-11.28719 1.0716,-14.4314 -2.38568,-17.65403 -7.16108,-6.52548 -8.39426,-6.27621 -11.45126,-8.11131 -6.42685,-0.0251 -14.02518,-3.48676 -19.32401,-4.29422 z"
id="path3828"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 153.87636,142.94096 c 3.90631,-1.87096 9.69299,16.23198 6.67991,19.80114 -5.63668,6.67696 -2.80544,19.95511 -0.23857,16.22262"
id="path3830"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
<path
style="fill:#ececec;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 147.43503,184.21322 c 5.98074,4.73827 16.11122,1.17708 25.76534,-1.43141 -7.03019,7.0566 -13.84727,16.88359 -21.47112,16.22263 l -4.77136,-7.87275 z"
id="path3832"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#f2f2f2;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 173.67751,213.31852 8.11131,1.43141 c 6.06788,-12.85113 5.3157,-19.63998 5.9642,-27.67389 -3.126,10.99543 -11.45068,18.09185 -22.18683,23.37966 l 7.87275,4.5328 z"
id="path3834"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#ff6c00;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 134.31379,139.12387 5.48706,0.47714 13.12124,8.58844 -1.90854,21.70969 -3.10139,26.71962 20.51685,15.02978 24.09537,2.14712 c -7.59703,15.51076 -21.44479,13.73677 -24.33394,26.24248 1.27084,-6.44195 0.86471,-13.55469 5.00993,-18.84688 -8.47582,5.06966 -15.09323,4.5641 -22.42539,6.20277 4.22385,-2.68547 8.10205,-6.06224 11.68983,-10.01985 -5.80515,-0.60962 -11.61031,-2.01583 -17.41546,-4.5328 -8.64382,-4.18289 -9.62981,-5.93913 -11.9284,-8.11131 7.01942,0.86252 9.473,-1.31884 12.88267,-2.86281 -26.93647,-7.55398 -26.92199,-14.26574 -35.0695,-21.23256 l 20.51685,7.39561 c 4.4984,0.68777 6.63439,-1.32438 6.6799,-5.72563 -16.33876,-9.8522 -21.88964,-21.17548 -24.33393,-32.92239 0.59372,8.94131 30.81833,14.94779 28.38959,14.79122 -3.07926,-0.19851 -18.61897,-12.87576 -17.1769,-16.22262 4.13518,3.60847 8.27036,6.74347 12.40554,5.9642 4.34471,1.92284 4.85595,-1.2656 5.48706,-4.29423 -6.58133,-6.79299 -11.82628,-7.57225 -17.17689,-8.82701 z"
id="path3836"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccsccccc" />
<path
style="fill:#f2f2f2;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 203.49851,177.05618 c -1.12284,5.42482 -3.73922,9.99619 -9.30416,12.88267 6.07286,-0.34494 18.50532,-3.68719 24.09537,-12.88267 z"
id="path3838"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#ececec;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 230.21812,178.96473 c 3.65804,1.99083 7.31609,3.07314 10.97413,9.30415 -6.98529,5.73834 -13.78235,11.6649 -26.95818,11.2127 8.7089,-1.5798 12.95652,-10.09936 15.98405,-20.51685 z"
id="path3840"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#999999;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 186.79875,168.22917 c -1.59045,3.42787 -3.18091,5.13539 -4.77136,7.3956 4.18355,-0.76297 7.96823,-1.79187 10.25842,-3.81708 z"
id="path3842"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#ca0000;fill-opacity:1;stroke:#130e13;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 155.78491,120.51557 c 0,0 -0.47714,-10.73556 -2.14712,-12.64411 -1.66997,-1.90854 -21.94825,-15.506918 -21.94825,-15.506918 -9.7295,-4.220532 -14.38334,-6.731213 -20.51685,-15.745488 2.90484,10.265925 7.92673,20.108438 17.1769,29.105296 -5.32802,-3.09071 -10.65604,-6.298919 -15.98406,-10.019856 2.26988,6.838946 5.33355,13.677896 15.74549,20.516846 10.6125,6.05766 18.85054,4.20042 27.67389,4.29423 z"
id="path3846"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscccccc" />
<path
style="fill:#000000;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 139.45292,84.438938 125.20773,71.693241 C 119.15983,58.804456 109.97182,43.346495 95.217856,23.334566 86.780901,17.513373 80.270569,10.150883 68.226968,7.2150074 60.058839,6.6898637 52.035767,7.0350652 43.110447,1.9667792 34.99844,-5.3656363 30.6958,-11.065466 28.115509,-16.027146 34.01411,-10.45225 40.197961,-4.0216063 45.359687,-0.65733498 52.482283,2.4482615 59.604878,2.589719 66.727474,3.466273 c 14.767928,1.2266725 22.683537,9.305664 32.239116,15.744685 16.18998,15.07105 25.9637,31.746167 35.98785,48.358675 -1.05571,-9.882878 -3.6074,-20.064949 -8.62209,-30.739623 -6.0246,-9.94338 -14.00687,-17.755415 -20.7603,-26.241142 C 95.75565,5.1501111 90.473401,1.2722747 75.724437,-6.2804367 70.112554,-8.4271577 66.785373,-15.143282 62.603866,-20.150754 l 13.120571,10.121583 c 12.287926,4.1420139 32.155153,15.4397069 38.237093,21.367786 12.3045,10.369959 16.369,17.443923 20.99291,24.741648 l 8.99696,25.116521 0.37488,16.494432 c 0,0 -7.87234,-18.743673 -7.87234,-17.244179 0,1.499494 2.99898,23.991901 2.99898,23.991901 z"
id="path3848"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccsc" />
<path
style="fill:#1a1a1a;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 99.012729,134.48743 11.893421,6.24404 c -25.631941,9.87193 -51.204911,20.45152 -77.604572,21.11082 -8.62273,0.0124 -17.24546,-2.90897 -25.86819,-6.83871 L -8.3253946,139.83947 9.5147367,151.13822 c 7.9289473,3.73642 15.8578943,4.42414 23.7868413,5.94671 16.056118,-2.54274 32.112237,-4.68742 48.168355,-13.3801 z"
id="path3850"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

+87
View File
@@ -0,0 +1,87 @@
use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use gpui::{
App, Application, AssetSource, Bounds, Context, SharedString, Window, WindowBounds,
WindowOptions, div, prelude::*, px, rgb, size, svg,
};
struct Assets {
base: PathBuf,
}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
fs::read(self.base.join(path))
.map(|data| Some(std::borrow::Cow::Owned(data)))
.map_err(|err| err.into())
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
fs::read_dir(self.base.join(path))
.map(|entries| {
entries
.filter_map(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().into_string().ok())
.map(SharedString::from)
})
.collect()
})
.map_err(|err| err.into())
}
}
struct SvgExample;
impl Render for SvgExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_row()
.size_full()
.justify_center()
.items_center()
.gap_8()
.bg(rgb(0xffffff))
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0xff0000)),
)
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0x00ff00)),
)
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0x0000ff)),
)
}
}
fn main() {
Application::new()
.with_assets(Assets {
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
})
.run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| SvgExample),
)
.unwrap();
cx.activate(true);
});
}
+155
View File
@@ -0,0 +1,155 @@
use gpui::{
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
};
actions!(example, [Tab, TabPrev]);
struct Example {
focus_handle: FocusHandle,
items: Vec<FocusHandle>,
message: SharedString,
}
impl Example {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let items = vec![
cx.focus_handle().tab_index(1).tab_stop(true),
cx.focus_handle().tab_index(2).tab_stop(true),
cx.focus_handle().tab_index(3).tab_stop(true),
cx.focus_handle(),
cx.focus_handle().tab_index(2).tab_stop(true),
];
let focus_handle = cx.focus_handle();
window.focus(&focus_handle);
Self {
focus_handle,
items,
message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."),
}
}
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
self.message = SharedString::from("You have pressed `Tab`.");
}
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
window.focus_prev();
self.message = SharedString::from("You have pressed `Shift-Tab`.");
}
}
impl Render for Example {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn tab_stop_style<T: Styled>(this: T) -> T {
this.border_3().border_color(gpui::blue())
}
fn button(id: impl Into<ElementId>) -> Stateful<Div> {
div()
.id(id)
.h_10()
.flex_1()
.flex()
.justify_center()
.items_center()
.border_1()
.border_color(gpui::black())
.bg(gpui::black())
.text_color(gpui::white())
.focus(tab_stop_style)
.shadow_sm()
}
div()
.id("app")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.size_full()
.flex()
.flex_col()
.p_4()
.gap_3()
.bg(gpui::white())
.text_color(gpui::black())
.child(self.message.clone())
.children(
self.items
.clone()
.into_iter()
.enumerate()
.map(|(ix, item_handle)| {
div()
.id(("item", ix))
.track_focus(&item_handle)
.h_10()
.w_full()
.flex()
.justify_center()
.items_center()
.border_1()
.border_color(gpui::black())
.when(
item_handle.tab_stop && item_handle.is_focused(window),
tab_stop_style,
)
.map(|this| match item_handle.tab_stop {
true => this
.hover(|this| this.bg(gpui::black().opacity(0.1)))
.child(format!("tab_index: {}", item_handle.tab_index)),
false => this.opacity(0.4).child("tab_stop: false"),
})
}),
)
.child(
div()
.flex()
.flex_row()
.gap_3()
.items_center()
.child(
button("el1")
.tab_index(4)
.child("Button 1")
.on_click(cx.listener(|this, _, _, cx| {
this.message = "You have clicked Button 1.".into();
cx.notify();
})),
)
.child(
button("el2")
.tab_index(5)
.child("Button 2")
.on_click(cx.listener(|this, _, _, cx| {
this.message = "You have clicked Button 2.".into();
cx.notify();
})),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.bind_keys([
KeyBinding::new("tab", Tab, None),
KeyBinding::new("shift-tab", TabPrev, None),
]);
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| Example::new(window, cx)),
)
.unwrap();
cx.activate(true);
});
}
+46
View File
@@ -0,0 +1,46 @@
//! Renders a div with deep children hierarchy. This example is useful to exemplify that Zed can
//! handle deep hierarchies (even though it cannot just yet!).
use std::sync::LazyLock;
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
};
struct Tree {}
static DEPTH: LazyLock<u64> = LazyLock::new(|| {
std::env::var("GPUI_TREE_DEPTH")
.ok()
.and_then(|depth| depth.parse().ok())
.unwrap_or_else(|| 50)
});
impl Render for Tree {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let mut depth = *DEPTH;
static COLORS: [gpui::Hsla; 4] = [gpui::red(), gpui::blue(), gpui::green(), gpui::yellow()];
let mut colors = COLORS.iter().cycle().copied();
let mut next_div = || div().p_0p5().bg(colors.next().unwrap());
let mut innermost_node = next_div();
while depth > 0 {
innermost_node = next_div().child(innermost_node);
depth -= 1;
}
innermost_node
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Tree {}),
)
.unwrap();
});
}
+50
View File
@@ -0,0 +1,50 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size, uniform_list,
};
struct UniformListExample {}
impl Render for UniformListExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().bg(rgb(0xffffff)).child(
uniform_list(
"entries",
50,
cx.processor(|_this, range, _window, _cx| {
let mut items = Vec::new();
for ix in range {
let item = ix + 1;
items.push(
div()
.id(ix)
.px_2()
.cursor_pointer()
.on_click(move |_event, _window, _cx| {
println!("clicked Item {item:?}");
})
.child(format!("Item {item}")),
);
}
items
}),
)
.h_full(),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| UniformListExample {}),
)
.unwrap();
});
}
+269
View File
@@ -0,0 +1,269 @@
use gpui::{
App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Timer, Window,
WindowBounds, WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
};
struct SubWindow {
custom_titlebar: bool,
}
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
div()
.id(text.to_string())
.flex_none()
.px_2()
.bg(rgb(0xf7f7f7))
.active(|this| this.opacity(0.85))
.border_1()
.border_color(rgb(0xe0e0e0))
.rounded_sm()
.cursor_pointer()
.child(text.to_string())
.on_click(move |_, window, cx| on_click(window, cx))
}
impl Render for SubWindow {
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.bg(rgb(0xffffff))
.size_full()
.gap_2()
.when(self.custom_titlebar, |cx| {
cx.child(
div()
.flex()
.h(px(32.))
.px_4()
.bg(gpui::blue())
.text_color(gpui::white())
.w_full()
.child(
div()
.flex()
.items_center()
.justify_center()
.size_full()
.child("Custom Titlebar"),
),
)
})
.child(
div()
.p_8()
.gap_2()
.child("SubWindow")
.child(button("Close", |window, _| {
window.remove_window();
})),
)
}
}
struct WindowDemo {}
impl Render for WindowDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_bounds =
WindowBounds::Windowed(Bounds::centered(None, size(px(300.0), px(300.0)), cx));
div()
.p_4()
.flex()
.flex_wrap()
.bg(rgb(0xffffff))
.size_full()
.justify_center()
.content_center()
.gap_2()
.child(button("Normal", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Popup", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
kind: WindowKind::PopUp,
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Custom Titlebar", move |_, cx| {
cx.open_window(
WindowOptions {
titlebar: None,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: true,
})
},
)
.unwrap();
}))
.child(button("Invisible", move |_, cx| {
cx.open_window(
WindowOptions {
show: false,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Unmovable", move |_, cx| {
cx.open_window(
WindowOptions {
is_movable: false,
titlebar: None,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Unresizable", move |_, cx| {
cx.open_window(
WindowOptions {
is_resizable: false,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Unminimizable", move |_, cx| {
cx.open_window(
WindowOptions {
is_minimizable: false,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Hide Application", |window, cx| {
cx.hide();
// Restore the application after 3 seconds
window
.spawn(cx, async move |cx| {
Timer::after(std::time::Duration::from_secs(3)).await;
cx.update(|_, cx| {
cx.activate(false);
})
})
.detach();
}))
.child(button("Resize", |window, _| {
let content_size = window.bounds().size;
window.resize(size(content_size.height, content_size.width));
}))
.child(button("Prompt", |window, cx| {
let answer = window.prompt(
PromptLevel::Info,
"Are you sure?",
None,
&["Ok", "Cancel"],
cx,
);
cx.spawn(async move |_| {
if answer.await.unwrap() == 0 {
println!("You have clicked Ok");
} else {
println!("You have clicked Cancel");
}
})
.detach();
}))
.child(button("Prompt (non-English)", |window, cx| {
let answer = window.prompt(
PromptLevel::Info,
"Are you sure?",
None,
&[PromptButton::ok("确定"), PromptButton::cancel("取消")],
cx,
);
cx.spawn(async move |_| {
if answer.await.unwrap() == 0 {
println!("You have clicked Ok");
} else {
println!("You have clicked Cancel");
}
})
.detach();
}))
}
}
actions!(window, [Quit]);
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| {
cx.new(|cx| {
cx.observe_window_bounds(window, move |_, window, _| {
println!("Window bounds changed: {:?}", window.bounds());
})
.detach();
WindowDemo {}
})
},
)
.unwrap();
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
});
}
+219
View File
@@ -0,0 +1,219 @@
use gpui::{
App, Application, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window,
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*,
px, rgb,
};
struct WindowContent {
text: SharedString,
bounds: Bounds<Pixels>,
bg: Hsla,
}
impl Render for WindowContent {
fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let window_bounds = window.bounds();
div()
.flex()
.flex_col()
.bg(self.bg)
.size_full()
.items_center()
.text_color(rgb(0xffffff))
.child(self.text.clone())
.child(
div()
.flex()
.flex_col()
.text_sm()
.items_center()
.size_full()
.child(format!(
"origin: {}, {} size: {}, {}",
self.bounds.origin.x,
self.bounds.origin.y,
self.bounds.size.width,
self.bounds.size.height
))
.child(format!(
"cx.bounds() origin: {}, {} size {}, {}",
window_bounds.origin.x,
window_bounds.origin.y,
window_bounds.size.width,
window_bounds.size.height
)),
)
}
}
fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> WindowOptions {
WindowOptions {
// Set the bounds of the window in screen coordinates
window_bounds: Some(WindowBounds::Windowed(bounds)),
// Specify the display_id to ensure the window is created on the correct screen
display_id: Some(display_id),
titlebar: None,
window_background: WindowBackgroundAppearance::Transparent,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
app_id: None,
window_min_size: None,
window_decorations: None,
tabbing_identifier: None,
..Default::default()
}
}
fn main() {
Application::new().run(|cx: &mut App| {
// Create several new windows, positioned in the top right corner of each screen
let size = Size {
width: px(350.),
height: px(75.),
};
let margin_offset = px(150.);
for screen in cx.displays() {
let bounds = Bounds {
origin: point(margin_offset, margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Top Left {:?}", screen.id()).into(),
bg: gpui::red(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().top_right()
- point(size.width + margin_offset, -margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Top Right {:?}", screen.id()).into(),
bg: gpui::red(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().bottom_left()
- point(-margin_offset, size.height + margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Bottom Left {:?}", screen.id()).into(),
bg: gpui::blue(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().bottom_right()
- point(size.width + margin_offset, size.height + margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Bottom Right {:?}", screen.id()).into(),
bg: gpui::blue(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(screen.bounds().center().x - size.center().x, margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Top Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(margin_offset, screen.bounds().center().y - size.center().y),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Left Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().center().x - size.center().x,
screen.bounds().center().y - size.center().y,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().size.width - size.width - margin_offset,
screen.bounds().center().y - size.center().y,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Right Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().center().x - size.center().x,
screen.bounds().size.height - size.height - margin_offset,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Bottom Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
}
});
}
+227
View File
@@ -0,0 +1,227 @@
use gpui::{
App, Application, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton,
Pixels, Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds,
WindowDecorations, WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size,
transparent_black, white,
};
struct WindowShadow {}
// Things to do:
// 1. We need a way of calculating which edge or corner the mouse is on,
// and then dispatch on that
// 2. We need to improve the shadow rendering significantly
// 3. We need to implement the techniques in here in Zed
impl Render for WindowShadow {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let decorations = window.window_decorations();
let rounding = px(10.0);
let shadow_size = px(10.0);
let border_size = px(1.0);
let grey = rgb(0x808080);
window.set_client_inset(shadow_size);
div()
.id("window-backdrop")
.bg(transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div
.bg(gpui::transparent_black())
.child(
canvas(
|_bounds, window, _cx| {
window.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
window.window_bounds().get_bounds().size,
),
HitboxBehavior::Normal,
)
},
move |_bounds, hitbox, window, _cx| {
let mouse = window.mouse_position();
let size = window.window_bounds().get_bounds().size;
let Some(edge) = resize_edge(mouse, shadow_size, size) else {
return;
};
window.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => {
CursorStyle::ResizeUpDown
}
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(rounding)
})
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
.when(!tiling.top, |div| div.pt(shadow_size))
.when(!tiling.bottom, |div| div.pb(shadow_size))
.when(!tiling.left, |div| div.pl(shadow_size))
.when(!tiling.right, |div| div.pr(shadow_size))
.on_mouse_move(|_e, window, _cx| window.refresh())
.on_mouse_down(MouseButton::Left, move |e, window, _cx| {
let size = window.window_bounds().get_bounds().size;
let pos = e.position;
match resize_edge(pos, shadow_size, size) {
Some(edge) => window.start_window_resize(edge),
None => window.start_window_move(),
};
}),
})
.size_full()
.child(
div()
.cursor(CursorStyle::Arrow)
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.border_color(grey)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(rounding)
})
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
.when(!tiling.top, |div| div.border_t(border_size))
.when(!tiling.bottom, |div| div.border_b(border_size))
.when(!tiling.left, |div| div.border_l(border_size))
.when(!tiling.right, |div| div.border_r(border_size))
.when(!tiling.is_tiled(), |div| {
div.shadow(vec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.4,
},
blur_radius: shadow_size / 2.,
spread_radius: px(0.),
offset: point(px(0.0), px(0.0)),
}])
}),
})
.on_mouse_move(|_e, _, cx| {
cx.stop_propagation();
})
.bg(gpui::rgb(0xCCCCFF))
.size_full()
.flex()
.flex_col()
.justify_around()
.child(
div().w_full().flex().flex_row().justify_around().child(
div()
.flex()
.bg(white())
.size(px(300.0))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(
div()
.id("hello")
.w(px(200.0))
.h(px(100.0))
.bg(green())
.shadow(vec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 1.0,
},
blur_radius: px(20.0),
spread_radius: px(0.0),
offset: point(px(0.0), px(0.0)),
}])
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { .. } => div
.on_mouse_down(
MouseButton::Left,
|_e, window, _| {
window.start_window_move();
},
)
.on_click(|e, window, _| {
if e.is_right_click() {
window.show_window_menu(e.position());
}
})
.text_color(black())
.child("this is the custom titlebar"),
}),
),
),
),
)
}
}
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
let edge = if pos.y < shadow_size && pos.x < shadow_size {
ResizeEdge::TopLeft
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::TopRight
} else if pos.y < shadow_size {
ResizeEdge::Top
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
ResizeEdge::BottomLeft
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::BottomRight
} else if pos.y > size.height - shadow_size {
ResizeEdge::Bottom
} else if pos.x < shadow_size {
ResizeEdge::Left
} else if pos.x > size.width - shadow_size {
ResizeEdge::Right
} else {
return None;
};
Some(edge)
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
window_background: WindowBackgroundAppearance::Opaque,
window_decorations: Some(WindowDecorations::Client),
..Default::default()
},
|window, cx| {
cx.new(|cx| {
cx.observe_window_appearance(window, |_, window, _| {
window.refresh();
})
.detach();
WindowShadow {}
})
},
)
.unwrap();
});
}
+41
View File
@@ -0,0 +1,41 @@
//! Example Prelude
//!
//! Common helpers for GPUI examples. Import with:
//!
//! ```ignore
//! #[path = "../prelude.rs"]
//! mod example_prelude;
//! use example_prelude::init_example;
//! ```
use gpui::{App, KeyBinding, Menu, MenuItem, SharedString, actions};
actions!(example, [Quit, CloseWindow]);
/// Sets up common example boilerplate:
/// - Activates the application
/// - Sets up an app menu with the example name and a Quit action (cmd-q)
/// - Configures the app to quit when all windows are closed
pub fn init_example(cx: &mut App, name: impl Into<SharedString>) {
// Bring the example window to the front
cx.activate(true);
// Define the quit action...
cx.on_action(|_: &Quit, cx| cx.quit());
// ...then bind it to cmd+q
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
// Set up an app menu with the example name and a Quit action (cmd-q)
cx.set_menus(vec![Menu {
name: name.into(),
items: vec![MenuItem::action("Quit", Quit)],
}]);
// Quit the app when all windows are closed
cx.on_window_closed(|cx| {
if cx.windows().is_empty() {
cx.quit();
}
})
.detach();
}