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,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");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user