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:
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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, ¤t_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");
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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)]);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 |
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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)]);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user