Start reorganizing examples (#10)

* Update crate name

* wip

* reduce to just the gpui crate

* Fix misisng example images

* Delete ownership_post.rs

* Update Cargo.toml

* Update README.md

* bump to 0.3.3

* organize

* organize

* Add more learn examples

* Delete _ownership_and_data_flow.rs

* wip learn examples

* Fix colors

* tidy

* Move _template up

* add animation example

* Update examples

* remove legacy examples from toml

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