Compare commits

...
4 Commits
Author SHA1 Message Date
harry bcdeab1aa3 Merge feat/background-tray: tray, background running, autostart, audio-loss warning
Rust checks / windows-cross-check (push) Successful in 1m31s
Rust checks / linux (push) Failing after 7m25s
2026-10-13 19:05:43 +02:00
harry 072a4ab7ca fix: preserve GPUI lifetime and avoid WASAPI silence warnings 2026-10-13 18:58:37 +02:00
harry d065ad7775 build: vendor published GPUI 0.2.2 for lifecycle patch 2026-10-13 18:56:54 +02:00
harry f1c31d6c90 feat: keep Clipbin recording in the tray
Add native Linux SNI and Windows notification-area trays, single-instance reopening, background startup and login entries. Preserve recording and discovery after close, with clean Quit and reachable no-tray fallbacks.

Track capture-source audio delivery independently of silent output tracks and warn without stopping video. Give clip metadata its own row and shape labels at final bounds to avoid GPUI 0.2.2 truncation cache and text-run bugs.

Validated with strict workspace checks, 57 tests, Windows core/OBS/probe cross-clippy, all ten capture scenarios, tray/session/clips/playback rigs and narrow/wide screenshots. Document runtime validation gaps.
2026-10-13 18:29:37 +02:00
204 changed files with 92122 additions and 157 deletions
Generated
+109 -3
View File
@@ -462,7 +462,7 @@ dependencies = [
"log",
"num-rational",
"num-traits",
"pastey",
"pastey 0.1.1",
"rayon",
"thiserror 2.0.21",
"v_frame",
@@ -890,13 +890,16 @@ dependencies = [
"dirs 5.0.1",
"futures-lite 2.6.1",
"futures-util",
"ksni",
"rustix 1.1.5",
"serde",
"serde_json",
"tempfile",
"tray-icon",
"windows-sys 0.61.2",
"winreg 0.55.0",
"x11rb",
"zbus",
]
[[package]]
@@ -1333,6 +1336,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
@@ -1545,6 +1557,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dpi"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "dtor"
version = "0.0.6"
@@ -2274,8 +2292,6 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "979b45cfa6ec723b6f42330915a1b3769b930d02b2d505f9697f8ca602bee707"
dependencies = [
"anyhow",
"as-raw-xcb-connection",
@@ -3103,6 +3119,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keyboard-types"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
dependencies = [
"bitflags 2.13.2",
"serde",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -3113,6 +3139,24 @@ dependencies = [
"libloading",
]
[[package]]
name = "ksni"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "814b44c24cd2cb236c3b8a41c7f08237b452a8e76ecaa81f1cec40b5b678215b"
dependencies = [
"async-executor",
"async-io",
"async-lock",
"futures-channel",
"futures-lite 2.6.1",
"futures-util",
"pastey 0.2.3",
"serde",
"task-local",
"zbus",
]
[[package]]
name = "kurbo"
version = "0.11.3"
@@ -3468,6 +3512,24 @@ dependencies = [
"pxfm",
]
[[package]]
name = "muda"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
dependencies = [
"crossbeam-channel",
"dpi",
"keyboard-types",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"png 0.18.1",
"thiserror 2.0.21",
"windows-sys 0.61.2",
]
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
@@ -3849,6 +3911,16 @@ dependencies = [
"objc2",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.13.2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
@@ -4047,6 +4119,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pastey"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
[[package]]
name = "pathfinder_geometry"
version = "0.5.1"
@@ -5722,6 +5800,15 @@ dependencies = [
"objc",
]
[[package]]
name = "task-local"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2972044a9e5e448a506a7ff6f0d03b566d8ef4cd6918a58fc59835a0f8666626"
dependencies = [
"pin-project-lite",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -6060,6 +6147,25 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tray-icon"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5cc5858ecf9574ddd8f0032a2ff7ffd6f7724d2b81e48b9374984b3fd6ddc9"
dependencies = [
"crossbeam-channel",
"muda",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"once_cell",
"png 0.18.1",
"thiserror 2.0.21",
"windows-sys 0.61.2",
]
[[package]]
name = "try-lock"
version = "0.2.5"
+4
View File
@@ -1,4 +1,5 @@
[workspace]
exclude = ["vendor/gpui"]
members = [
"crates/clipbin-core",
"crates/clipbin-desktop",
@@ -42,3 +43,6 @@ opt-level = 2
lto = "thin"
codegen-units = 1
strip = "debuginfo"
[patch.crates-io]
gpui = { path = "vendor/gpui" }
+6 -6
View File
@@ -6,11 +6,11 @@ An open-source game recorder for Windows and Linux, inspired by Plays.tv. Rust a
## Current state
The recording foundation is being validated (see [docs/validation.md](docs/validation.md)). `clipbin-obs` records official OBS capture sources through one hardware encoder into keyframe-aligned segments and reports honest status; `clipbin-probe` exercises it. It passes the Linux headless rig and the hardware sign-off on CachyOS/Hyprland/RX 9070 XT; all Windows validation is still to do.
The recording foundation is being validated (see [docs/validation.md](docs/validation.md)). `clipbin-obs` records official OBS capture sources through one hardware encoder into keyframe-aligned segments and reports honest status; `clipbin-probe` exercises it. It passes the Linux headless rig and the hardware sign-off on CachyOS/Hyprland/RX 9070 XT; Windows runtime validation is still to do.
The desktop app detects Steam games, owns the recorder on a background thread, persists settings and sessions, and loads recorded sessions from disk. Linux detects Steam’s `reaper` processes and watches their pidfds; Windows reads Steam’s registry state and watches native process handles (cross-checked, not run). Game exit finalises a session; closing the window keeps the current session recording until the game exits, then exits the app. Ctrl+Q or Quit stops the output cleanly.
The desktop app detects Steam games, owns the recorder on a background thread, persists settings and sessions, and loads recorded sessions from disk. Linux detects Steam’s `reaper` processes and watches their pidfds; Windows reads Steam’s registry state and watches native process handles (cross-checked, not run). Game exit finalises a session; closing the window leaves recording, game detection, hotkeys and cleanup running in the tray. Open Clipbin from the tray or launch it again to reopen the same process and restore its last screen. Tray Quit, the app-menu Quit action or Ctrl+Q finalises the current session and exits. Without an available tray, closing quits cleanly so the process stays reachable.
The desktop interface follows the web Clipbin's look (zinc dark theme, purple accent, Geist Mono): a session library, a session timeline with bookmarks and a clip selection, clips, and settings. It shows sessions from disk; `CLIPBIN_SAMPLE_LIBRARY=1` fills it with example sessions for interface work. Playback and frame seeking work across completed segments, including active sessions and missing footage, with combined audio, mute/volume, Space/K to play/pause and J/L or arrows to step frames. Sessions and clips use cached frame thumbnails. Exports copy the recorded codec to MP4 in a background job, save local clip metadata, and support opening/deleting clips. Ctrl+Shift+F9 clips the last configured interval; Ctrl+Shift+F10 protects a bookmark. Both also have sidebar buttons. Wayland uses the GlobalShortcuts portal (first-use approval, portal rebinding); X11 uses native key grabs and Windows uses RegisterHotKey. Settings offer independent alternate function keys. Storage cleanup keeps unprotected footage within the budget. Autostart and a tray are not implemented yet; after closing the only window, reopening it without a tray is a follow-up.
The desktop interface follows the web Clipbin's look (zinc dark theme, purple accent, Geist Mono): a session library, a session timeline with bookmarks and a clip selection, clips, and settings. It shows sessions from disk; `CLIPBIN_SAMPLE_LIBRARY=1` fills it with example sessions for interface work. Playback and frame seeking work across completed segments, including active sessions and missing footage, with combined audio, mute/volume, Space/K to play/pause and J/L or arrows to step frames. Sessions and clips use cached frame thumbnails. Exports copy the recorded codec to MP4 in a background job, save local clip metadata, and support opening/deleting clips. Ctrl+Shift+F9 clips the last configured interval; Ctrl+Shift+F10 protects a bookmark. Both also have sidebar buttons. Wayland uses the GlobalShortcuts portal (first-use approval, portal rebinding); X11 uses native key grabs and Windows uses RegisterHotKey. Settings offer independent alternate function keys. Storage cleanup keeps unprotected footage within the budget. The tray shows idle, recording, failed and degraded audio states and offers clip/bookmark actions. Start on login writes an XDG autostart entry on Linux or an HKCU Run value on Windows. `--background` starts with only a tray; without a tray it opens the window. Linux capture delivery loss or Windows WASAPI device errors produce an “audio capture lost” warning while video continues.
Windows is a first-class target. Every change must keep the Windows code paths building (`cargo clippy --target x86_64-pc-windows-msvc`) and list what still needs validating on Windows.
@@ -35,7 +35,7 @@ Keep capture and session operations out of the UI thread. Core uses serde/JSON,
## Development
Use Rust **1.98.0**, as pinned in rust-toolchain.toml. GPUI is pinned to **0.2.2**, with Wayland and X11 enabled. Cargo.lock pins transitive dependencies.
Use Rust **1.98.0**, as pinned in rust-toolchain.toml. GPUI is pinned to **0.2.2**, with Wayland and X11 enabled. Its vendored [background-loop patch](vendor/gpui/CLIPBIN.md) keeps one application alive while its windows close and reopen. Cargo.lock pins transitive dependencies.
Keep the lockfile checked in and use `--locked`. It currently pins `libc` to 0.2.186 because GPUI’s transitive `xattr` 0.2.3 dependency fails to compile against 0.2.190 on Linux.
@@ -78,7 +78,7 @@ cargo run --locked -p clipbin-desktop
The desktop app needs a working Wayland or X11 session on Linux, or a Windows desktop. It does not start a web server. On this remote development machine, bind any future development server to its tailnet IP.
Use the Quit button or Ctrl+Q to quit.
Use tray Quit or Ctrl+Q to quit. Closing the window keeps Clipbin in the background when a tray is available. Start on login is enabled by default and can be disabled in Settings. On Linux a session D-Bus and an SNI host (for example Waybar or KDE) provide the tray; bare Sway falls back to close=Quit.
Checks:
@@ -92,7 +92,7 @@ cargo clippy --locked --target x86_64-pc-windows-msvc -p clipbin-core -p clipbin
Clips/bookmarks/cleanup validation: `scripts/validate/linux-clips.sh`. Native X11 hotkey validation: `scripts/validate/linux-hotkeys.sh`. Both run in the headless rig. Real Hyprland portal hotkeys and Windows runtime behaviour still need validation.
Recording validation on Linux, headless: `scripts/validate/linux-rig.sh`. Automatic desktop sessions: `scripts/validate/linux-session.sh` (fake Steam reaper, recording and library screenshots, close-window survival, game exit and explicit Quit). Playback fixtures: `scripts/validate/playback-fixture.sh`. Playback/audio e2e: `scripts/validate/linux-playback.sh [output-dir]` (moving preview, active recording, gaps, thumbnails, audio monitor, timing and CPU report). Fake Steam rigs must run serially because discovery reads the machine’s `/proc`. Interface screenshots, headless: `scripts/rig/screenshot-ui.sh` (screens: sessions, session, clips, settings, failed, empty). The rig has no input devices; `rig-pointer` drives a virtual pointer (`move`, `click`, `down`/`up`, `scroll`, `sleep` on stdin) and `wtype` types. `CLIPBIN_TRACE_FRAMES=1` logs frame times.
Background/tray/autostart validation: `scripts/validate/linux-background.sh` uses a minimal SNI host and activates the real D-Bus menu. Recording validation on Linux, headless: `scripts/validate/linux-rig.sh`. Automatic desktop sessions: `scripts/validate/linux-session.sh` (fake Steam reaper, recording and library screenshots, close-window survival, game exit and explicit Quit). Playback fixtures: `scripts/validate/playback-fixture.sh`. Playback/audio e2e: `scripts/validate/linux-playback.sh [output-dir]` (moving preview, active recording, gaps, thumbnails, audio monitor, timing and CPU report). Fake Steam rigs must run serially because discovery reads the machine’s `/proc`. Interface screenshots, headless: `scripts/rig/screenshot-ui.sh` (screens: sessions, session, clips, settings, failed, empty). The rig has no input devices; `rig-pointer` drives a virtual pointer (`move`, `click`, `down`/`up`, `scroll`, `sleep` on stdin) and `wtype` types. `CLIPBIN_TRACE_FRAMES=1` logs frame times.
Development builds optimise dependencies (`[profile.dev.package."*"]`): unoptimised GPUI made screens noticeably slow, with first frames of 200–300 ms.
+4 -1
View File
@@ -18,6 +18,8 @@ serde_json = "1"
tempfile = "3"
[target.'cfg(target_os = "linux")'.dependencies]
ksni = { version = "0.3.6", default-features = false, features = ["async-io", "blocking"] }
zbus = { version = "5", default-features = false, features = ["async-io", "blocking-api"] }
ashpd = { version = "0.12.3", default-features = false, features = ["async-std"] }
async-io = "2"
futures-lite = "2"
@@ -26,5 +28,6 @@ x11rb = { version = "0.13", features = ["xkb"] }
rustix = { version = "1", features = ["process", "event"] }
[target.'cfg(windows)'.dependencies]
tray-icon = { version = "0.26", default-features = false }
winreg = "0.55"
windows-sys = { version = "0.61", features = ["Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", "Win32_Foundation", "Win32_System_Threading", "Win32_System_Diagnostics_ToolHelp"] }
windows-sys = { version = "0.61", features = ["Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", "Win32_Foundation", "Win32_Security", "Win32_System_LibraryLoader", "Win32_Graphics_Gdi", "Win32_System_Threading", "Win32_System_Diagnostics_ToolHelp"] }
+37
View File
@@ -0,0 +1,37 @@
//! Minimal SNI watcher/host for the headless validation rig (no desktop bar).
#[cfg(target_os = "linux")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::sync::Mutex;
struct Watcher(Mutex<Vec<String>>);
#[zbus::interface(name = "org.kde.StatusNotifierWatcher")]
impl Watcher {
fn register_status_notifier_item(&self, service: String) {
eprintln!("registered={service}");
self.0.lock().unwrap().push(service);
}
fn register_status_notifier_host(&self, service: String) {
let _ = service;
}
#[zbus(property)]
fn is_status_notifier_host_registered(&self) -> bool {
true
}
#[zbus(property)]
fn protocol_version(&self) -> i32 {
0
}
#[zbus(property)]
fn registered_status_notifier_items(&self) -> Vec<String> {
self.0.lock().unwrap().clone()
}
}
let _connection = zbus::blocking::connection::Builder::session()?
.name("org.kde.StatusNotifierWatcher")?
.serve_at("/StatusNotifierWatcher", Watcher(Mutex::new(Vec::new())))?
.build()?;
loop {
std::thread::park();
}
}
#[cfg(not(target_os = "linux"))]
fn main() {}
+60
View File
@@ -0,0 +1,60 @@
//! Reconcile the OS login entry with the saved setting.
use std::{io, path::Path};
pub fn reconcile(enabled: bool) -> io::Result<()> {
apply(enabled, &std::env::current_exe()?)
}
#[cfg(target_os = "linux")]
fn apply(enabled: bool, executable: &Path) -> io::Result<()> {
let path = dirs::config_dir()
.ok_or_else(|| io::Error::other("No config directory"))?
.join("autostart/clipbin.desktop");
if enabled {
std::fs::create_dir_all(path.parent().unwrap())?;
let executable = executable
.to_str()
.ok_or_else(|| io::Error::other("Executable path is not UTF-8"))?;
// Desktop Entry Exec quoting includes reserved characters and doubles %.
let quoted = executable
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('`', "\\`")
.replace('$', "\\$")
.replace('%', "%%");
let escaped = quoted
.replace('\\', "\\\\")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
std::fs::write(
path,
format!(
"[Desktop Entry]\nType=Application\nName=Clipbin\nExec=\"{escaped}\" --background\nTerminal=false\n"
),
)
} else {
match std::fs::remove_file(path) {
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
result => result,
}
}
}
#[cfg(windows)]
fn apply(enabled: bool, executable: &Path) -> io::Result<()> {
use winreg::{RegKey, enums::HKEY_CURRENT_USER};
let (key, _) = RegKey::predef(HKEY_CURRENT_USER)
.create_subkey(r"Software\Microsoft\Windows\CurrentVersion\Run")?;
if enabled {
key.set_value(
"Clipbin",
&format!("\"{}\" --background", executable.display()),
)
} else {
match key.delete_value("Clipbin") {
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
result => result,
}
}
}
+201
View File
@@ -0,0 +1,201 @@
//! Background actions shared by the native tray and single-instance endpoint.
use crate::RecordingStatus;
#[derive(Clone, Copy, Debug)]
pub enum Action {
Open,
Clip,
Bookmark,
Quit,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct State {
pub status: RecordingStatus,
pub game: Option<String>,
}
impl State {
pub fn tooltip(&self) -> String {
let mut text = format!("Clipbin · {}", self.status.label());
if let Some(game) = &self.game {
text.push_str(&format!(" · {game}"));
}
match &self.status {
RecordingStatus::Failed { reason } | RecordingStatus::Degraded { reason } => {
text.push_str(&format!(" · {reason}"));
}
_ => {}
}
text
}
pub fn colour(&self) -> [u8; 3] {
match self.status {
RecordingStatus::Recording => [239, 68, 68],
RecordingStatus::Failed { .. } | RecordingStatus::Degraded { .. } => [245, 158, 11],
_ => [168, 85, 247],
}
}
}
#[cfg(target_os = "linux")]
mod linux {
use super::*;
use ksni::blocking::TrayMethods;
use std::sync::mpsc::Sender;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
struct Endpoint(Sender<Action>);
#[zbus::interface(name = "dev.clipbin.Clipbin")]
impl Endpoint {
fn open(&self) {
let _ = self.0.send(Action::Open);
}
}
pub struct Instance {
_connection: zbus::blocking::Connection,
}
impl Instance {
pub fn acquire(actions: Sender<Action>) -> Result<Option<Self>, String> {
let connection = zbus::blocking::connection::Builder::session()
.map_err(|e| e.to_string())?
.serve_at("/dev/clipbin/Clipbin", Endpoint(actions))
.map_err(|e| e.to_string())?
.build()
.map_err(|e| e.to_string())?;
match connection.request_name_with_flags(
"dev.clipbin.Clipbin",
zbus::fdo::RequestNameFlags::DoNotQueue.into(),
) {
Ok(zbus::fdo::RequestNameReply::PrimaryOwner) => Ok(Some(Self {
_connection: connection,
})),
Err(zbus::Error::NameTaken) => {
let proxy = zbus::blocking::Proxy::new(
&connection,
"dev.clipbin.Clipbin",
"/dev/clipbin/Clipbin",
"dev.clipbin.Clipbin",
)
.map_err(|e| e.to_string())?;
proxy
.call::<_, _, ()>("Open", &())
.map_err(|e| e.to_string())?;
Ok(None)
}
Err(error) => Err(error.to_string()),
Ok(reply) => Err(format!("Unexpected D-Bus name response: {reply:?}")),
}
}
}
struct Item {
state: State,
actions: Sender<Action>,
available: Arc<AtomicBool>,
}
impl ksni::Tray for Item {
fn id(&self) -> String {
"clipbin".into()
}
fn title(&self) -> String {
self.state.tooltip()
}
fn icon_pixmap(&self) -> Vec<ksni::Icon> {
let [r, g, b] = self.state.colour();
let mut data = Vec::with_capacity(32 * 32 * 4);
for y in 0_i32..32 {
for x in 0_i32..32 {
data.extend_from_slice(&[
if (x - 16).pow(2) + (y - 16).pow(2) < 196 {
255
} else {
0
},
r,
g,
b,
]);
}
}
vec![ksni::Icon {
width: 32,
height: 32,
data,
}]
}
fn tool_tip(&self) -> ksni::ToolTip {
ksni::ToolTip {
title: self.state.tooltip(),
..Default::default()
}
}
fn activate(&mut self, _: i32, _: i32) {
let _ = self.actions.send(Action::Open);
}
fn watcher_offline(&self, _: ksni::OfflineReason) -> bool {
self.available.store(false, Ordering::Relaxed);
let _ = self.actions.send(Action::Open);
true
}
fn watcher_online(&self) {
self.available.store(true, Ordering::Relaxed);
}
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
[
("Open Clipbin", Action::Open),
("Save clip", Action::Clip),
("Add bookmark", Action::Bookmark),
("Quit", Action::Quit),
]
.into_iter()
.map(|(label, action)| {
ksni::menu::StandardItem {
label: label.into(),
activate: Box::new(move |item: &mut Self| {
let _ = item.actions.send(action);
}),
..Default::default()
}
.into()
})
.collect()
}
}
pub struct Tray {
handle: ksni::blocking::Handle<Item>,
available: Arc<AtomicBool>,
}
impl Tray {
pub fn start(actions: Sender<Action>) -> Result<Self, String> {
let available = Arc::new(AtomicBool::new(true));
let handle = Item {
actions,
available: available.clone(),
state: State {
status: RecordingStatus::Idle,
game: None,
},
}
.spawn()
.map_err(|e| e.to_string())?;
Ok(Self { handle, available })
}
pub fn available(&self) -> bool {
self.available.load(Ordering::Relaxed) && !self.handle.is_closed()
}
pub fn update(&self, state: State) {
self.handle.update(move |item| item.state = state);
}
}
impl Drop for Tray {
fn drop(&mut self) {
self.handle.shutdown().wait();
}
}
}
#[cfg(windows)]
pub use crate::windows_background::{Instance, Tray};
#[cfg(target_os = "linux")]
pub use linux::{Instance, Tray};
+5
View File
@@ -20,3 +20,8 @@ pub mod cleanup;
pub mod clips;
pub mod hotkeys;
pub mod autostart;
pub mod background;
#[cfg(windows)]
mod windows_background;
+6 -1
View File
@@ -1,4 +1,4 @@
/// The recorder will own these transitions once capture is implemented.
/// The recorder's externally visible health.
///
/// A game being detected or permission being granted is insufficient to enter
/// Recording: a usable capture source and successful output must be confirmed.
@@ -9,6 +9,10 @@ pub enum RecordingStatus {
WaitingForCapture,
Starting,
Recording,
/// Video continues while a configured audio source is no longer delivering.
Degraded {
reason: String,
},
Failed {
reason: String,
},
@@ -21,6 +25,7 @@ impl RecordingStatus {
Self::WaitingForCapture => "Waiting for capture",
Self::Starting => "Starting",
Self::Recording => "Recording",
Self::Degraded { .. } => "Recording degraded",
Self::Failed { .. } => "Recording failed",
}
}
@@ -0,0 +1,295 @@
//! Win32 message pumps stay off GPUI's main thread.
#![allow(unsafe_code)]
use crate::background::{Action, State};
use std::{
ptr,
sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering},
mpsc::{self, Sender},
},
thread,
time::Duration,
};
use windows_sys::Win32::{
Foundation::{
CloseHandle, ERROR_ALREADY_EXISTS, GetLastError, HANDLE, HWND, LPARAM, LRESULT, WPARAM,
},
System::{LibraryLoader::GetModuleHandleW, Threading::CreateMutexW},
UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, FindWindowW, GetMessageW,
PM_REMOVE, PeekMessageW, PostMessageW, PostQuitMessage, RegisterClassW, TranslateMessage,
UnregisterClassW, WM_APP, WM_CLOSE, WM_DESTROY, WNDCLASSW,
},
};
fn wide(text: &str) -> Vec<u16> {
text.encode_utf16().chain(Some(0)).collect()
}
static ACTIONS: OnceLock<Sender<Action>> = OnceLock::new();
const OPEN: u32 = WM_APP + 1;
unsafe extern "system" fn procedure(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if message == OPEN {
if let Some(actions) = ACTIONS.get() {
let _ = actions.send(Action::Open);
}
return 0;
}
// SAFETY: this is a registered window procedure with Win32-provided arguments.
unsafe {
if message == WM_CLOSE {
DestroyWindow(window);
return 0;
}
if message == WM_DESTROY {
PostQuitMessage(0);
return 0;
}
DefWindowProcW(window, message, wparam, lparam)
}
}
pub struct Instance {
mutex: HANDLE,
window: usize,
worker: Option<thread::JoinHandle<()>>,
}
impl Instance {
pub fn acquire(actions: Sender<Action>) -> Result<Option<Self>, String> {
let name = wide(&format!(
"Local\\Clipbin-{}",
std::env::var("USERNAME").unwrap_or_default()
));
let class = wide("ClipbinSingleInstance");
// SAFETY: names are NUL-terminated, no security attributes or initial ownership.
let mutex = unsafe { CreateMutexW(ptr::null(), 0, name.as_ptr()) };
if mutex.is_null() {
return Err(std::io::Error::last_os_error().to_string());
}
// SAFETY: GetLastError is read immediately after CreateMutexW.
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
// Startup can race the first instance's window registration.
for _ in 0..100 {
// SAFETY: class string is live; FindWindow and PostMessage accept borrowed handles.
let window = unsafe { FindWindowW(class.as_ptr(), ptr::null()) };
if !window.is_null() {
// SAFETY: send a private message to the live endpoint window.
let sent = unsafe { PostMessageW(window, OPEN, 0, 0) };
let error = std::io::Error::last_os_error();
// SAFETY: release this launch's mutex reference exactly once.
unsafe {
CloseHandle(mutex);
}
return if sent != 0 {
Ok(None)
} else {
Err(error.to_string())
};
}
thread::sleep(Duration::from_millis(50));
}
// SAFETY: release our reference, leaving the first instance's mutex intact.
unsafe {
CloseHandle(mutex);
}
return Err("Running Clipbin did not expose its reopen endpoint".into());
}
let _ = ACTIONS.set(actions);
let (ready, result) = mpsc::sync_channel(1);
let worker = thread::spawn(move || {
// SAFETY: window and message loop are owned by this thread; the class
// and instance handle remain valid through DestroyWindow.
unsafe {
let instance = GetModuleHandleW(ptr::null());
let definition = WNDCLASSW {
lpfnWndProc: Some(procedure),
hInstance: instance,
lpszClassName: class.as_ptr(),
..std::mem::zeroed()
};
RegisterClassW(&definition);
let window = CreateWindowExW(
0,
class.as_ptr(),
class.as_ptr(),
0,
0,
0,
0,
0,
ptr::null_mut(),
ptr::null_mut(),
instance,
ptr::null(),
);
let _ = ready.send(if window.is_null() {
Err(std::io::Error::last_os_error().to_string())
} else {
Ok(window as usize)
});
if window.is_null() {
return;
}
let mut message = std::mem::zeroed();
while GetMessageW(&mut message, ptr::null_mut(), 0, 0) > 0 {
TranslateMessage(&message);
DispatchMessageW(&message);
}
UnregisterClassW(class.as_ptr(), instance);
}
});
match result.recv().map_err(|e| e.to_string())? {
Ok(window) => Ok(Some(Self {
mutex,
window,
worker: Some(worker),
})),
Err(error) => {
// SAFETY: endpoint creation failed; release the acquired mutex.
unsafe {
CloseHandle(mutex);
}
let _ = worker.join();
Err(error)
}
}
}
}
impl Drop for Instance {
fn drop(&mut self) {
// SAFETY: the endpoint thread owns this HWND; posting close lets it clean up.
unsafe {
PostMessageW(self.window as HWND, WM_CLOSE, 0, 0);
}
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
// SAFETY: release the mutex reference acquired above.
unsafe {
CloseHandle(self.mutex);
}
}
}
enum Command {
Update(State),
Stop,
}
pub struct Tray {
commands: Sender<Command>,
available: Arc<AtomicBool>,
worker: Option<thread::JoinHandle<()>>,
}
impl Tray {
pub fn start(actions: Sender<Action>) -> Result<Self, String> {
let (commands, receiver) = mpsc::channel();
let (ready, result) = mpsc::sync_channel(1);
let available = Arc::new(AtomicBool::new(true));
let alive = available.clone();
let worker = thread::spawn(move || {
use tray_icon::{
Icon, TrayIconBuilder, TrayIconEvent,
menu::{Menu, MenuEvent, MenuItem},
};
let run = || -> Result<(), String> {
let menu = Menu::new();
let entries: Vec<_> = [
("Open Clipbin", Action::Open),
("Save clip", Action::Clip),
("Add bookmark", Action::Bookmark),
("Quit", Action::Quit),
]
.into_iter()
.map(|(label, action)| (MenuItem::new(label, true, None), action))
.collect();
for (item, _) in &entries {
menu.append(item).map_err(|e| e.to_string())?;
}
let icon = |state: &State| {
let [r, g, b] = state.colour();
Icon::from_rgba([r, g, b, 255].repeat(32 * 32), 32, 32)
.map_err(|e| e.to_string())
};
let state = State {
status: crate::RecordingStatus::Idle,
game: None,
};
let tray = TrayIconBuilder::new()
.with_menu(Box::new(menu))
.with_tooltip(state.tooltip())
.with_icon(icon(&state)?)
.build()
.map_err(|e| e.to_string())?;
let _ = ready.send(Ok(()));
loop {
// SAFETY: the tray is created and dispatched on this thread.
unsafe {
let mut message = std::mem::zeroed();
while PeekMessageW(&mut message, ptr::null_mut(), 0, 0, PM_REMOVE) != 0 {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
while let Ok(event) = MenuEvent::receiver().try_recv() {
if let Some((_, action)) =
entries.iter().find(|(item, _)| item.id() == &event.id)
{
let _ = actions.send(*action);
}
}
while let Ok(event) = TrayIconEvent::receiver().try_recv() {
if matches!(event, TrayIconEvent::DoubleClick { .. }) {
let _ = actions.send(Action::Open);
}
}
match receiver.recv_timeout(Duration::from_millis(20)) {
Ok(Command::Update(state)) => {
tray.set_tooltip(Some(state.tooltip()))
.map_err(|e| e.to_string())?;
tray.set_icon(Some(icon(&state)?))
.map_err(|e| e.to_string())?;
}
Ok(Command::Stop) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::RecvTimeoutError::Timeout) => {}
}
}
Ok(())
};
if let Err(error) = run() {
let _ = ready.send(Err(error.clone()));
eprintln!("Windows tray unavailable: {error}");
let _ = actions.send(Action::Open);
}
alive.store(false, Ordering::Relaxed);
});
result.recv().map_err(|e| e.to_string())??;
Ok(Self {
commands,
available,
worker: Some(worker),
})
}
pub fn available(&self) -> bool {
self.available.load(Ordering::Relaxed)
&& self
.worker
.as_ref()
.is_some_and(|worker| !worker.is_finished())
}
pub fn update(&self, state: State) {
let _ = self.commands.send(Command::Update(state));
}
}
impl Drop for Tray {
fn drop(&mut self) {
let _ = self.commands.send(Command::Stop);
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
+1
View File
@@ -517,6 +517,7 @@ impl ClipbinApp {
theme::destructive().into(),
self.recorder.game.clone().unwrap_or_default(),
),
RecordingStatus::Degraded { reason } => (theme::warning().into(), reason.clone()),
RecordingStatus::Starting | RecordingStatus::WaitingForCapture => (
theme::warning().into(),
self.recorder.game.clone().unwrap_or_default(),
+137 -47
View File
@@ -3,6 +3,9 @@ use gpui::{
actions, px, size,
};
use clipbin_core::background::{Action, Instance, Tray};
use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
mod app;
mod assets;
mod components;
@@ -25,65 +28,152 @@ fn main() {
Some(format!("Could not load settings: {error}")),
),
};
let (actions, receiver) = std::sync::mpsc::channel();
let _instance = match Instance::acquire(actions.clone()) {
Ok(Some(instance)) => instance,
Ok(None) => return,
Err(error) => {
eprintln!("Could not establish single instance: {error}");
return;
}
};
let tray = match Tray::start(actions) {
Ok(tray) => Some(tray),
Err(error) => {
eprintln!("Tray unavailable; closing the window will quit: {error}");
None
}
};
let initial_error = clipbin_core::autostart::reconcile(settings.start_on_login)
.err()
.map(|e| format!("Could not configure start on login: {e}"))
.or(initial_error);
let (service, worker) = recorder_service::Client::start(settings.clone(), initial_error);
let interface_service = service.clone();
let tray = tray.map(Arc::new);
let background = std::env::args().any(|arg| arg == "--background");
Application::new()
.with_assets(assets::Assets)
.run(|cx: &mut App| {
.run(move |cx: &mut App| {
cx.set_quit_on_last_window_close(false);
if let Err(error) = assets::load_fonts(cx) {
eprintln!("Could not load the interface font: {error}");
}
let quitter = interface_service.clone();
cx.on_action(move |_: &Quit, cx| {
quitter.quit();
let service = quitter.clone();
cx.spawn(async move |cx| {
loop {
gpui::Timer::after(std::time::Duration::from_millis(100)).await;
if service.snapshot().exited {
let _ = cx.update(|cx| cx.quit());
break;
}
}
})
.detach();
});
cx.on_action(move |_: &Quit, _| quitter.quit());
cx.bind_keys([KeyBinding::new("ctrl-q", Quit, None)]);
cx.set_menus(vec![gpui::Menu {
name: "Clipbin".into(),
items: vec![gpui::MenuItem::action("Quit Clipbin", Quit)],
}]);
text_field::bind_keys(cx);
playback::bind_keys(cx);
let bounds = Bounds::centered(None, size(px(1360.), px(860.)), cx);
let result = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
window_min_size: Some(size(px(1040.), px(640.))),
titlebar: Some(TitlebarOptions {
title: Some("Clipbin".into()),
..Default::default()
}),
app_id: Some("dev.clipbin.Clipbin".into()),
..Default::default()
},
|window, cx| {
cx.new(|cx| app::ClipbinApp::new(window, cx, settings, interface_service))
},
);
match result {
Ok(window) => playback::bind_actions(window, cx),
Err(error) => {
eprintln!("Could not open Clipbin: {error}");
cx.quit();
return;
}
let ui = Rc::new(RefCell::new(Interface {
window: None,
route: app::Route::Sessions,
}));
if !background || tray.is_none() {
open_window(cx, &interface_service, &ui, &tray);
}
cx.activate(true);
cx.spawn(async move |cx| {
let mut last_state = None;
loop {
gpui::Timer::after(Duration::from_millis(100)).await;
let state = interface_service.background_state();
if last_state.as_ref() != Some(&state) {
if let Some(tray) = &tray {
tray.update(state.clone());
}
last_state = Some(state);
}
while let Ok(action) = receiver.try_recv() {
match action {
Action::Open => {
let _ =
cx.update(|cx| open_window(cx, &interface_service, &ui, &tray));
}
Action::Clip => interface_service.clip(),
Action::Bookmark => interface_service.bookmark(),
Action::Quit => interface_service.quit(),
}
}
if interface_service.exited() {
let _ = cx.update(|cx| cx.quit());
break;
}
}
})
.detach();
});
// GPUI 0.2.2 ends its Linux event loop after the last window closes.
// Finish the current session after closing; stop discovery and exit when
// its game ends. Reopening the window needs the future tray integration.
service.window_closed();
service.quit();
let _ = worker.join();
drop(service);
}
struct Interface {
window: Option<gpui::WindowHandle<app::ClipbinApp>>,
route: app::Route,
}
fn open_window(
cx: &mut App,
service: &recorder_service::Client,
ui: &Rc<RefCell<Interface>>,
tray: &Option<Arc<Tray>>,
) {
let current = ui.borrow().window;
if let Some(window) = current
&& window
.update(cx, |_, window, _| window.activate_window())
.is_ok()
{
return;
}
let settings = clipbin_core::store::load_settings().unwrap_or_default();
let bounds = Bounds::centered(None, size(px(1360.), px(860.)), cx);
let window_service = service.clone();
let result = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
window_min_size: Some(size(px(1040.), px(640.))),
titlebar: Some(TitlebarOptions {
title: Some("Clipbin".into()),
..Default::default()
}),
app_id: Some("dev.clipbin.Clipbin".into()),
..Default::default()
},
move |window, cx| cx.new(|cx| app::ClipbinApp::new(window, cx, settings, window_service)),
);
let window = match result {
Ok(window) => window,
Err(error) => {
eprintln!("Could not open Clipbin: {error}");
service.quit();
return;
}
};
let route = ui.borrow().route;
ui.borrow_mut().window = Some(window);
let close_service = service.clone();
let close_tray = tray.clone();
let close_ui = ui.clone();
let _ = window.update(cx, |app, window, cx| {
if route != app::Route::Sessions {
app.navigate(route, cx);
}
let entity = cx.entity();
window.on_window_should_close(cx, move |_, cx| {
if !close_tray.as_ref().is_some_and(|tray| tray.available()) {
close_service.quit();
return false;
}
let mut ui = close_ui.borrow_mut();
ui.route = entity.read(cx).route;
ui.window = None;
eprintln!("Window closed: Clipbin is running in the tray");
true
});
});
playback::bind_actions(window, cx);
cx.activate(true);
}
+27 -18
View File
@@ -38,7 +38,6 @@ pub struct Client {
enum Command {
Settings(Settings),
ChooseSource,
WindowClosed,
Quit,
Clip(Instant),
Bookmark(Instant),
@@ -85,6 +84,16 @@ impl Client {
pub fn snapshot(&self) -> Snapshot {
self.snapshot.lock().unwrap().clone()
}
pub fn background_state(&self) -> clipbin_core::background::State {
let snapshot = self.snapshot.lock().unwrap();
clipbin_core::background::State {
status: snapshot.status.clone(),
game: snapshot.game.clone(),
}
}
pub fn exited(&self) -> bool {
self.snapshot.lock().unwrap().exited
}
pub fn settings(&self, settings: Settings) {
let _ = self.commands.send(Command::Settings(settings));
}
@@ -132,9 +141,6 @@ impl Client {
pub fn choose_source(&self) {
let _ = self.commands.send(Command::ChooseSource);
}
pub fn window_closed(&self) {
let _ = self.commands.send(Command::WindowClosed);
}
pub fn quit(&self) {
if self.commands.send(Command::Quit).is_err() {
self.snapshot.lock().unwrap().exited = true;
@@ -385,7 +391,6 @@ fn worker(
let mut hotkeys = Some(register(&settings));
let mut active: Option<Active> = None;
let mut quitting = false;
let mut finish_after_session = false;
let mut next_scan = Instant::now();
let mut next_poll = Instant::now();
let mut next_save = Instant::now();
@@ -395,10 +400,6 @@ fn worker(
let mut next_cleanup = Instant::now();
loop {
match commands.recv_timeout(Duration::from_millis(200)) {
Ok(Command::WindowClosed) => {
finish_after_session = true;
eprintln!("Window closed: finishing the current game session, then exiting");
}
Ok(Command::Quit) => {
quitting = true;
if let Some(a) = &mut active {
@@ -424,6 +425,10 @@ fn worker(
if let Err(e) = store::save_settings(&settings) {
fail(snapshot, format!("Could not save settings: {e}"));
}
if let Err(e) = clipbin_core::autostart::reconcile(settings.start_on_login) {
snapshot.lock().unwrap().notice =
Some(format!("Could not configure start on login: {e}"));
}
snapshot.lock().unwrap().restore_token = settings.portal_restore_token.clone();
}
Ok(Command::Clip(pressed)) => {
@@ -548,11 +553,11 @@ fn worker(
Err(mpsc::RecvTimeoutError::Disconnected) => quitting = true,
Err(mpsc::RecvTimeoutError::Timeout) => {}
}
if (quitting || finish_after_session) && active.is_none() && !exports.busy() {
if quitting && active.is_none() && !exports.busy() {
break;
}
let now = Instant::now();
if active.is_none() && !quitting && !finish_after_session && now >= next_scan {
if active.is_none() && !quitting && now >= next_scan {
next_scan = now + Duration::from_secs(1);
match games::discover() {
Ok(games) => {
@@ -650,16 +655,20 @@ fn worker(
}
if !a.ending && !a.stopping {
let status = a.tracker.sample(&health, now).clone();
if status == RecordingStatus::Recording {
if matches!(
status,
RecordingStatus::Recording | RecordingStatus::Degraded { .. }
) {
a.retries = 0;
}
// Keep a failure visible throughout recovery until footage is proven.
if status == RecordingStatus::Recording
|| !matches!(
snapshot.lock().unwrap().status,
RecordingStatus::Failed { .. }
)
{
if matches!(
status,
RecordingStatus::Recording | RecordingStatus::Degraded { .. }
) || !matches!(
snapshot.lock().unwrap().status,
RecordingStatus::Failed { .. }
) {
snapshot.lock().unwrap().status = status.clone();
}
if matches!(status, RecordingStatus::Failed { .. }) {
+77 -37
View File
@@ -1,12 +1,14 @@
//! Exported clips: local MP4 files that never expire automatically.
use clipbin_core::library::{format_ago, format_bytes, format_length};
use gpui::{AnyElement, ClickEvent, Context, FontWeight, IntoElement, div, prelude::*};
use gpui::{
AnyElement, ClickEvent, Context, FontWeight, IntoElement, Pixels, canvas, div, prelude::*, px,
};
use crate::{
app::ClipbinApp,
components::{ButtonStyle, button, chip, count, empty_state, page_header},
screens::{corner, muted_line, thumbnail},
screens::{corner, thumbnail},
theme,
};
@@ -90,6 +92,7 @@ pub fn render(app: &ClipbinApp, cx: &mut Context<ClipbinApp>) -> AnyElement {
div()
.flex()
.flex_col()
.min_w_0()
.gap_3()
.child(
thumbnail(&clip.game, app.playback.clip_thumbnails.get(&clip.id)).child(
@@ -102,14 +105,14 @@ pub fn render(app: &ClipbinApp, cx: &mut Context<ClipbinApp>) -> AnyElement {
.child(
div()
.flex()
.items_start()
.flex_col()
.min_w_0()
.gap_2()
.px_1()
.child(
div()
.flex()
.flex_col()
.flex_1()
.min_w_0()
.gap_0p5()
.child(
@@ -117,43 +120,48 @@ pub fn render(app: &ClipbinApp, cx: &mut Context<ClipbinApp>) -> AnyElement {
.text_base()
.font_weight(FontWeight::SEMIBOLD)
.text_color(theme::foreground())
.truncate()
.child(clip.game.clone()),
.child(label(clip.game.clone(), px(24.))),
)
.child(muted_line(format!(
"{} · {}",
format_ago(clip.created, now),
format_bytes(clip.size_bytes)
.child(div().text_xs().text_color(theme::subtle()).child(label(
format!(
"{} · {}",
format_ago(clip.created, now),
format_bytes(clip.size_bytes)
),
px(18.),
))),
)
.child(
button(
("open-clip", clip.id),
ButtonStyle::Ghost,
Some("play"),
None,
)
.rounded_full()
.on_click(cx.listener(
move |app, _: &ClickEvent, _, cx| {
app.service.open_clip(Some(id));
cx.notify();
},
)),
)
.child(
button(
("delete-clip", id),
ButtonStyle::Ghost,
None,
Some("Delete"),
)
.on_click(cx.listener(
move |app, _: &ClickEvent, _, cx| {
app.service.delete_clip(id);
cx.notify();
},
)),
div()
.flex()
.items_center()
.justify_end()
.gap_2()
.flex_shrink_0()
.child(
button(("open-clip", id), ButtonStyle::Ghost, Some("play"), None)
.flex_shrink_0()
.rounded_full()
.on_click(cx.listener(move |app, _: &ClickEvent, _, cx| {
app.service.open_clip(Some(id));
cx.notify();
})),
)
.child(
button(
("delete-clip", id),
ButtonStyle::Ghost,
None,
Some("Delete"),
)
.flex_shrink_0()
.on_click(cx.listener(
move |app, _: &ClickEvent, _, cx| {
app.service.delete_clip(id);
cx.notify();
},
)),
),
),
)
});
@@ -174,3 +182,35 @@ pub fn render(app: &ClipbinApp, cx: &mut Context<ClipbinApp>) -> AnyElement {
)
.into_any_element()
}
// GPUI 0.2.2 caches nowrap widths and mutates text runs during remeasurement.
// Shape once at the final card bounds to retain both ellipsis and font styling.
fn label(text: String, height: Pixels) -> impl IntoElement {
canvas(
move |bounds, window, cx| {
let style = window.text_style();
let font_size = style.font_size.to_pixels(window.rem_size());
let line_height = style
.line_height
.to_pixels(font_size.into(), window.rem_size());
let text: gpui::SharedString = text.replace(['\n', '\r'], " ").into();
let mut runs = vec![style.to_run(text.len())];
let text = cx
.text_system()
.line_wrapper(style.font(), font_size)
.truncate_line(text, bounds.size.width, "…", &mut runs);
(
window
.text_system()
.shape_line(text, font_size, &runs, None),
line_height,
)
},
|bounds, (line, line_height), window, cx| {
let _ = line.paint(bounds.origin, line_height, window, cx);
},
)
.w_full()
.h(height)
.overflow_hidden()
}
+148 -1
View File
@@ -13,6 +13,7 @@ use std::{
atomic::{AtomicU8, AtomicU64, Ordering},
mpsc::Sender,
},
time::Instant,
};
use libobs as sys;
@@ -152,6 +153,8 @@ unsafe extern "C" fn log_to_stderr(
if let Some(state) = pipewire_state(&text) {
PIPEWIRE_STATE.store(state as u8, Ordering::Relaxed);
}
#[cfg(windows)]
record_wasapi_event(&text);
eprintln!("obs {label}: {text}");
}
// SAFETY: frees the buffer dstr_vprintf allocated (null is allowed).
@@ -611,6 +614,108 @@ pub(crate) fn segment_index(path: &str) -> Option<u64> {
stem.strip_prefix("segment-")?.parse().ok()
}
#[cfg(windows)]
static AUDIO_FAULTS: Mutex<Vec<(String, std::sync::Weak<std::sync::atomic::AtomicBool>)>> =
Mutex::new(Vec::new());
#[cfg(any(windows, test))]
fn wasapi_event(message: &str) -> Option<(&str, bool)> {
let (message, source) = message.rsplit_once(" (source: ")?;
let source = source.strip_suffix(')')?;
if !source.starts_with("clipbin-") {
return None;
}
if message.starts_with("WASAPI: Device '") && message.contains(" initialized") {
Some((source, false))
} else if (message.starts_with("WASAPI: Device '") && message.contains(" failed to start"))
|| (message.starts_with("Device '") && message.contains(" invalidated. Retrying"))
{
Some((source, true))
} else {
None
}
}
#[cfg(windows)]
fn record_wasapi_event(message: &str) {
if let Some((source, failed)) = wasapi_event(message)
&& let Ok(faults) = AUDIO_FAULTS.lock()
&& let Some(flag) = faults
.iter()
.find(|(name, _)| name == source)
.and_then(|(_, flag)| flag.upgrade())
{
flag.store(failed, Ordering::Relaxed);
}
}
struct AudioClock {
started: Instant,
last_millis: AtomicU64,
#[cfg(windows)]
source_name: String,
#[cfg(windows)]
failed: Arc<std::sync::atomic::AtomicBool>,
}
impl AudioClock {
fn new(source_name: &str) -> Self {
#[cfg(windows)]
let failed = Arc::new(std::sync::atomic::AtomicBool::new(false));
#[cfg(windows)]
AUDIO_FAULTS
.lock()
.unwrap()
.push((source_name.into(), Arc::downgrade(&failed)));
#[cfg(not(windows))]
let _ = source_name;
Self {
started: Instant::now(),
last_millis: AtomicU64::new(0),
#[cfg(windows)]
source_name: source_name.into(),
#[cfg(windows)]
failed,
}
}
fn lost(&self) -> bool {
#[cfg(windows)]
{
self.failed.load(Ordering::Relaxed)
}
#[cfg(not(windows))]
{
(self.started.elapsed().as_millis() as u64)
.saturating_sub(self.last_millis.load(Ordering::Relaxed))
>= 5_000
}
}
}
#[cfg(windows)]
impl Drop for AudioClock {
fn drop(&mut self) {
if let Ok(mut faults) = AUDIO_FAULTS.lock() {
faults.retain(|(name, _)| name != &self.source_name);
}
}
}
unsafe extern "C" fn audio_captured(
param: *mut c_void,
_: *mut sys::obs_source_t,
_: *const sys::audio_data,
_: bool,
) {
// SAFETY: each callback holds the stable AudioClock allocation until removal;
// OBS serialises removal against callbacks with source->audio_cb_mutex.
let clock = unsafe { &*param.cast::<AudioClock>() };
#[cfg(windows)]
clock.failed.store(false, Ordering::Relaxed);
clock.last_millis.store(
clock.started.elapsed().as_millis() as u64,
Ordering::Relaxed,
);
}
/// The live OBS objects for one recording. Capture starts at once; encoders
/// and the segment output are created only when the source has a picture,
/// so nothing is written while the user picks a source or capture is down.
@@ -622,6 +727,7 @@ pub(crate) struct Pipeline {
sources: Vec<(Option<u32>, *mut sys::obs_source_t)>,
video_source: *mut sys::obs_source_t,
microphone: bool,
audio_clocks: Vec<(*mut sys::obs_source_t, Box<AudioClock>)>,
pending: Mutex<Option<OutputSpec>>,
output: Mutex<Option<Output>>,
}
@@ -648,6 +754,7 @@ impl Pipeline {
sources: Vec::new(),
video_source: ptr::null_mut(),
microphone: capture.microphone.is_some(),
audio_clocks: Vec::new(),
pending: Mutex::new(None),
output: Mutex::new(None),
name_prefix: name_prefix.clone(),
@@ -670,8 +777,16 @@ impl Pipeline {
(&capture.microphone, MICROPHONE_CHANNEL, MICROPHONE_TRACK),
] {
if let Some(source_spec) = source_spec {
let source = create_source(source_spec, &name(&format!("audio-{channel}")))?;
let source_name = format!("{name_prefix}-audio-{channel}");
let mut clock = Box::new(AudioClock::new(&source_name));
let source = create_source(source_spec, &cstring(&source_name))?;
pipeline.sources.push((Some(channel), source));
sys::obs_source_add_audio_capture_callback(
source,
Some(audio_captured),
(&mut *clock as *mut AudioClock).cast(),
);
pipeline.audio_clocks.push((source, clock));
let mixers = tracks
.iter()
.filter(|t| **t == track || **t == COMBINED_TRACK)
@@ -769,6 +884,7 @@ impl Pipeline {
lagged_frames: unsafe { sys::obs_get_lagged_frames() } as u64,
written_bytes: 0,
disk_available_bytes: None,
audio_lost: self.audio_clocks.iter().any(|(_, clock)| clock.lost()),
}
}
@@ -797,6 +913,13 @@ impl Drop for Pipeline {
drop(self.output.get_mut().ok().and_then(Option::take));
// SAFETY: runs on the OBS thread (Recording drops the Pipeline there).
unsafe {
for (source, clock) in &mut self.audio_clocks {
sys::obs_source_remove_audio_capture_callback(
*source,
Some(audio_captured),
(&mut **clock as *mut AudioClock).cast(),
);
}
sys::obs_set_output_source(VIDEO_CHANNEL, ptr::null_mut());
for (channel, source) in &self.sources {
if let Some(channel) = channel {
@@ -988,6 +1111,30 @@ unsafe fn create_source(spec: &SourceSpec, name: &CStr) -> Result<*mut sys::obs_
mod tests {
use super::*;
#[test]
fn wasapi_events_are_scoped_and_distinguish_failure_from_silence() {
assert_eq!(
wasapi_event(
"WASAPI: Device 'Speakers' [48000 Hz] initialized (source: clipbin-7-audio-1)"
),
Some(("clipbin-7-audio-1", false))
);
assert_eq!(
wasapi_event("Device 'Speakers' invalidated. Retrying (source: clipbin-7-audio-1)"),
Some(("clipbin-7-audio-1", true))
);
assert_eq!(
wasapi_event("WASAPI: Device 'default' failed to start (source: clipbin-7-audio-3)"),
Some(("clipbin-7-audio-3", true))
);
assert_eq!(
wasapi_event("Device 'Speakers' invalidated. Retrying (source: another-app)"),
None
);
assert_eq!(wasapi_event("WASAPI: Device 'Speakers' Terminated"), None);
assert_eq!(wasapi_event("WASAPI: Default output device changed"), None);
}
#[test]
fn pipewire_state_is_read_from_the_plugin_log() {
let state = |line| pipewire_state(line);
+45 -10
View File
@@ -304,6 +304,8 @@ pub struct Health {
/// PipeWire capture's portal and stream state. `None` for other sources,
/// which report loss through their size, and before the portal answers.
pub pipewire: Option<PipeWireState>,
/// A configured capture source lost delivery (Linux) or reported a device error (Windows).
pub audio_lost: bool,
pub output_active: bool,
pub total_frames: u64,
pub dropped_frames: u64,
@@ -409,17 +411,27 @@ impl StatusTracker {
(_, true, true)
if health.output_active && health.total_frames > 0 && self.last_written > 0 =>
{
RecordingStatus::Recording
if health.audio_lost {
RecordingStatus::Degraded {
reason: "audio capture lost".into(),
}
} else {
RecordingStatus::Recording
}
}
(RecordingStatus::Recording | RecordingStatus::Degraded { .. }, false, _) => {
RecordingStatus::Failed {
reason: "the capture source stopped providing a picture".into(),
}
}
(RecordingStatus::Recording | RecordingStatus::Degraded { .. }, true, false) => {
RecordingStatus::Failed {
reason: format!(
"no footage was written for {} seconds",
self.stall_limit.as_secs()
),
}
}
(RecordingStatus::Recording, false, _) => RecordingStatus::Failed {
reason: "the capture source stopped providing a picture".into(),
},
(RecordingStatus::Recording, true, false) => RecordingStatus::Failed {
reason: format!(
"no footage was written for {} seconds",
self.stall_limit.as_secs()
),
},
(_, false, _) => RecordingStatus::WaitingForCapture,
(status, _, _) => status.clone(),
};
@@ -511,10 +523,33 @@ mod tests {
total_bytes: bytes,
lagged_frames: 0,
written_bytes: bytes,
audio_lost: false,
disk_available_bytes: Some(u64::MAX),
}
}
#[test]
fn audio_loss_degrades_and_recovers_without_masking_video_loss() {
let now = Instant::now();
let mut tracker = StatusTracker::new(now, 0, SEGMENT);
let mut health = healthy(10_000);
assert_eq!(tracker.sample(&health, now), &RecordingStatus::Recording);
health.audio_lost = true;
assert!(matches!(
tracker.sample(&health, now),
RecordingStatus::Degraded { .. }
));
health.audio_lost = false;
assert_eq!(tracker.sample(&health, now), &RecordingStatus::Recording);
health.audio_lost = true;
tracker.sample(&health, now);
health.source_width = 0;
assert!(matches!(
tracker.sample(&health, now),
RecordingStatus::Failed { .. }
));
}
#[test]
fn low_disk_space_fails_before_the_disk_is_full() {
let start = Instant::now();
+7 -1
View File
@@ -367,7 +367,10 @@ fn record(options: &Options) -> Result<bool, Box<dyn std::error::Error>> {
health.written_bytes
);
report_status(&mut status, tracker.sample(&health, now), &elapsed());
recorded |= status == RecordingStatus::Recording;
recorded |= matches!(
status,
RecordingStatus::Recording | RecordingStatus::Degraded { .. }
);
}
if !stopping && (now >= stop_at || matches!(status, RecordingStatus::Failed { .. })) {
stopping = true;
@@ -414,6 +417,9 @@ fn report_status(shown: &mut RecordingStatus, current: &RecordingStatus, elapsed
if shown != current {
*shown = current.clone();
match current {
RecordingStatus::Degraded { reason } => {
println!("t={elapsed} status=degraded reason={reason}")
}
RecordingStatus::Failed { reason } => {
println!("t={elapsed} status=failed reason={reason}")
}
+253 -14
View File
@@ -8,9 +8,9 @@ pass/fail results.
| Where | Script | Status |
| --- | --- | --- |
| Linux, headless rig (Wayland portal + Xwayland) | `scripts/validate/linux-rig.sh` | All 9 scenarios pass on devbox (Intel UHD 770, VA-API) |
| Linux, headless rig (Wayland portal + Xwayland) | `scripts/validate/linux-rig.sh` | All 12 scenarios pass on devbox (Intel UHD 770, VA-API), 2026-10-13 |
| Linux, real session (CachyOS, Hyprland, RX 9070 XT, 1440p60) | `scripts/validate/linux-hardware.sh` | Passed 2026-10-08 (run 4, dc9cbd6) |
| Linux desktop automatic sessions (headless Wayland) | `scripts/validate/linux-session.sh` | Passed 2026-10-08: Recording sidebar, game exit, library reload, window close and active Quit |
| Linux desktop automatic sessions (headless Wayland) | `scripts/validate/linux-session.sh` | Passed 2026-10-13: recording sidebar, close-to-tray, game exit, second-launch reopen and active Quit |
| Windows (Game Capture, NVENC/AMF/QSV) | `scripts/validate/windows.ps1` | Not run yet; script untested |
## Probe commands
@@ -174,8 +174,12 @@ Windows status:
monitor's own size, set when capture first has a picture (portrait monitors
record portrait). If the window is resized mid-session the size stays and
the picture is scaled to fit.
- **Audio capture failures are not detected yet.** When the PulseAudio server
was unreachable, recording continued with a silent track.
- **Audio delivery health.** The configured system/microphone capture sources
are monitored through OBS audio capture callbacks on Linux (five seconds
without delivery) and source-scoped WASAPI device errors on Windows. Either
degrades recording with “audio capture lost”; video continues. The silent
muxer track cannot mask capture failure. See
Background operation below for validation and limits.
- **Linux runtime dependencies.** The Linux OBS build links the system's
FFmpeg, PipeWire and other libraries, so it is tied to the distribution it
was built on. Packaging (for example Flatpak, or bundling FFmpeg) is an open
@@ -251,17 +255,20 @@ Real Steam validated 2026-10-08 on CachyOS/Hyprland (native Steam): Escape
from Duckov (AppId 3167020, Proton Experimental under SteamLinuxRuntime_4)
runs under `reaper SteamLaunch AppId=3167020 -- …/_v2-entry-point … proton
waitforexitandrun …/Duckov.exe`. Clipbin detected it, named it from its
manifest ("Escape from Duckov") and recorded the session. Audio-source
failure detection remains limited as described above.
manifest ("Escape from Duckov") and recorded the session. Audio delivery loss
is now detected as described below; the new warning still needs real hardware
and Windows validation.
GPUI 0.2.2 normally ends its Linux and Windows event loops when the last
window is removed. Clipbin now keeps one Application and native loop alive
until explicit Quit, with a small vendored backend policy patch (see
[vendor/gpui/CLIPBIN.md](../vendor/gpui/CLIPBIN.md)). The visible window is
removed on close and recreated on Open, restoring its previous route and
reloading the library/settings; unavailable sessions fall back to the library.
Playback resources are released while closed. No invisible GPUI window is
needed. Bounds, scroll offsets and unsaved editor selection are not restored.
Quit waits for the existing worker's clean session finish before joining/exiting.
GPUI 0.2.2 ends its Linux event loop when the last window is removed, even
without an application close callback. Main keeps the recorder worker alive
after that return. Closing the window continues the current game session;
when that game exits, it finalises and the process exits. If no session is
active, it exits immediately. It stops discovering new games once closed,
so there is no indefinitely running invisible app. Reopening the window and
persistent background discovery need the future tray integration. Explicit
Quit/Ctrl+Q stops and finalises the current recording immediately.
## Clips, bookmarks and storage cleanup
@@ -497,3 +504,235 @@ Logs are `/tmp/clipbin-clips-quit-fixed.log` and
output directories. `bash -n`, `git diff --check` and `cargo fmt --check` also
passed. This follow-up changes only the Linux rig scripts and documentation;
Windows runtime and real Hyprland validation limits above remain unchanged.
## Background operation (2026-10-13)
Linux uses [ksni 0.3.6](https://github.com/iovxw/ksni) with the blocking,
async-io API. It implements SNI and com.canonical.dbusmenu on its own executor,
works on Wayland and X11, and requires no GTK main loop. Using tray-icon on
Linux would require GTK alongside GPUI's main-thread loop, so it is only a
Windows dependency. Windows uses [tray-icon 0.26.1](https://github.com/tauri-apps/tray-icon)
with its menu and notification-area icon on a dedicated Win32 message thread;
its documented Windows event loop does not need the main thread. GPUI owns the
main thread on both platforms. Platform services live in clipbin-core and are
included in Windows cross-clippy. The new raw Win32 calls are confined to
windows_background.rs, with a module unsafe allowance, SAFETY comments and
crate-level unsafe_code=deny; desktop retains the workspace forbid policy.
The tray has purple idle, red recording and amber failed/degraded variants,
with status, game and error detail in its tooltip. Open Clipbin, Save clip,
Add bookmark and Quit use the recorder's existing actions. Linux requires an
SNI watcher with a registered host: startup failure opens the interface even
with --background, logs the reason and makes close finish/quit. A watcher that
disappears later requests Open and disables close-to-tray until it returns.
This avoids leaving an unreachable process after a bar/watcher disappears.
Linux owns dev.clipbin.Clipbin on the session bus and exposes Open at
/dev/clipbin/Clipbin. A second process requests the same name without queueing,
calls Open on the owner and exits before starting OBS or reconciling autostart.
A session bus is required; endpoint acquisition errors are logged and startup
exits rather than starting a duplicate recorder. Windows holds a per-user
Local named mutex and posts a private message to the first instance's hidden
endpoint window, with a bounded five-second startup race wait. The hidden
endpoint owns its message thread independently of the GPUI windows.
Start on login reconciles on startup and each settings save. Linux writes or
removes $XDG_CONFIG_HOME/autostart/clipbin.desktop (default ~/.config/autostart),
with a quoted executable path and --background; Windows writes/removes the
Clipbin value in HKCU\Software\Microsoft\Windows\CurrentVersion\Run. Changing
login settings does not interrupt capture. The executable must stay at its
installed path; development builds point the entry at the build executable.
Actual desktop login dispatch remains unvalidated.
Audio health uses [OBS 32.0.4's source capture callback API](https://github.com/obsproject/obs-studio/blob/32.0.4/libobs/obs-source.c)
on every configured system/microphone source, independently of the encoder,
mixed audio counters and silent output track. A boxed callback context stores
an atomic monotonic delivery timestamp; it performs no allocation or locking.
The context is removed before source release: OBS's audio_cb_mutex serialises
callback removal with callbacks. On Linux, five seconds without delivery
(including a source that never starts) is a warning-level RecordingStatus::Degraded,
with “audio capture lost” in the sidebar and tray. A new callback clears the
warning; video/source/output/disk failures still take precedence and stop
recording. Disabled audio has no callback and no warning. Valid silent samples
are healthy; an endless synthetic silence stream cannot reveal a driver failure.
Windows does not infer loss from absent buffers. In the pinned
[OBS 32.0.4 win-wasapi source](https://github.com/obsproject/obs-studio/blob/32.0.4/plugins/win-wasapi/win-wasapi.cpp),
WASAPISource::Initialize calls ClearBuffer before InitCapture for DeviceOutput. ClearBuffer
creates a temporary shared renderer and fills its buffer with silence, but
neither starts nor retains that renderer. ProcessCaptureData accepts silent
packets and exits normally when GetNextPacketSize reports no packets. This is
not evidence of uninterrupted callbacks during idle loopback capture. Instead,
Clipbin tracks the plugin's source-scoped “failed to start” and device
“invalidated. Retrying” logs; successful initialization or a delivered capture
buffer clears the warning. The callback remains allocation/lock free; the log
handler separately updates weak registered fault flags. Flags are registered
before source creation and unregistered on cleanup, so unrelated/stale sources
cannot affect a recording. The parser has a unit test. Windows failures that
neither deliver buffers nor produce these pinned log messages remain undetected;
this is preferable to reporting ordinary silence as lost capture. Actual WASAPI
silence, mute, unplug/recovery and changed-device behavior require Windows runtime
validation. No automatic source restart is added; OBS's recovery can clear the
warning. Source identity is not displayed separately for system vs mic loss.
The rig's audio failure injection kills its private pipewire-pulse daemon,
leaving PipeWire video and the portal alive. Unloading the null sink and
requesting kill-source-output did not reliably stop delivery in PipeWire 1.6.8;
killing that private audio server deterministically stops source callbacks.
The original nine scenarios are retained alongside audio loss, idle silence
and muted-microphone cases (twelve total). Silence cases omit the ffplay tone,
assert there are no playback streams, wait twelve seconds and require the null
sink to be SUSPENDED before capture. They then record for sixteen seconds with
no degraded/failed status. The muted-mic case selects the null sink monitor as
the default input and mutes it; physical microphone hardware remains untested.
The rig now uses an explicit private SWAYSOCK and waits for it; discovery by
the previous sway-ipc glob did not locate the installed Sway 1.12 endpoint.
CLIPBIN_RIG_KEEP_LOGS also preserves startup-failure logs.
Windows limitations for this step: core/OBS/probe cross-clippy covers the native
tray, message pump, named mutex/hidden-window endpoint, HKCU autostart and OBS
callback registration/removal. Nothing new has been linked or run on Windows.
GPUI desktop and clipbin-media remain excluded from that lint set because the
MSVC C toolchain/FFmpeg development import libraries are unavailable here.
Unvalidated: tray rendering/menu focus/Explorer restart, icon/tooltips, singleton
startup races and multi-user/desktop behaviour, foreground activation,
GPUI persistent-loop policy (including the vendored Windows backend, which is
not cross-linted), window recreation, clean Quit/file closure, --background,
registry writes and real login launch, WASAPI error-log/recovery/idle semantics, and the Clips layout on Windows.
Real Linux desktop trays (Hyprland
Waybar, KDE, GNOME extensions), login launch, SNI host replacement/re-registration
and native X11 tray lifecycle still need runtime validation.
Clips cards give title/subtitle their own row and keep fixed-size Play/Delete
controls below. The pinned GPUI text element caches nowrap widths independently
of truncation width and mutates its text runs during repeated measurements;
ordinary truncate produced hard clipping, ellipsis-only text, or fallback fonts
in the grid. A small private canvas label uses GPUI's line-wrapper and shaping
APIs once at the final bounds, retaining Geist Mono, colour, weight and proper
ellipsis without patching GPUI. Newlines in game names become spaces. Sample
screenshots at 1100x760 and 1600x1000 were inspected: all sample metadata is
readable and controls cannot steal its width. The amber audio warning was also
inspected in lifecycle-final/audio-lost.png. Screenshot validation exercises
the actual rendering path; no unit test can reproduce the GPU/grid text
measurement chain without the rig.
Validation commands, from the background-tray worktree (all passed):
```sh
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo fmt --all -- --check
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo clippy --locked --workspace --all-targets -- -D warnings
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo test --locked --workspace
RUSTUP_HOME=$HOME/.cache/clipbin/rustup CARGO_HOME=$HOME/.cache/clipbin/cargo-home PATH=$HOME/.cache/clipbin/cargo-home/bin:$PATH CARGO_TARGET_DIR=target/windows-check cargo clippy --locked --target x86_64-pc-windows-msvc -p clipbin-core -p clipbin-obs -p clipbin-probe --all-targets -- -D warnings
```
57 workspace tests passed (generated-media tests keep their existing opt-in
fixture behaviour). The added status test covers audio degradation, recovery
and video-loss precedence. The dependency's existing proc-macro-error2 future
incompatibility notice is still printed; strict current clippy passes.
/tmp is a user-quota tmpfs here: an earlier recording probe failed with
Disk quota exceeded despite df reporting free bytes. Successful serial rig
runs used repository output storage, with a short absolute TMPDIR so private
Unix socket paths fit their limits:
```sh
export PATH=/usr/bin:/bin:$HOME/.cargo/bin
export TMPDIR=/home/harry/Dev/clipbin-desktop/target/rig-tmp
out=/home/harry/Dev/clipbin-desktop/target/background-validation
mkdir -p "$TMPDIR"
scripts/validate/linux-rig.sh "$out/capture"
scripts/validate/linux-background.sh "$out/lifecycle-final"
scripts/validate/linux-session.sh "$out/sessions"
scripts/validate/linux-clips.sh "$out/clips"
scripts/validate/linux-playback.sh "$out/playback"
CLIPBIN_RIG_WIDTH=1100 CLIPBIN_RIG_HEIGHT=760 scripts/rig/screenshot-ui.sh "$out/screens-narrow-labels" clips
CLIPBIN_RIG_WIDTH=1600 CLIPBIN_RIG_HEIGHT=1000 scripts/rig/screenshot-ui.sh "$out/screens-wide-labels" clips
```
Capture: all original nine scenarios plus audio loss passed. Lifecycle: actual
watcher registration and D-Bus menu Open/Quit, recording tooltip, no-window
background start, autostart toggle removal/write and startup reconciliation,
segments growing after close, second launch reopening the same process,
preserved Settings route, degraded UI/tooltip, clean active-session Quit,
watcher-loss reveal and no-host close=Quit fallback passed. Sessions: game exit,
exact cumulative segment placement, continued background discovery/library
reload, second-launch Open and active Ctrl+Q passed. Clips: H.264 MP4 retained,
360 CFR frames, continuous audio, zero decode errors, original press range,
bookmarks, tiny-budget cleanup and bookmark re-export passed. Playback: moving
preview (6.1% changed pixels), device audio RMS 0.08027, thumbnails, continued
recording, gaps, cross-boundary seek, volume/mute and held-key controls passed.
Decode median/p95 was 6.335/10.472 ms; GPUI upload median/p95 was 0.208/0.404 ms.
Both old UI suites still use explicit Quit and completed successfully.
The simplify quick self-review covered reuse, clarity and efficiency before
commit: removed the obsolete window-close shutdown mode, reused existing clip,
bookmark and clean-finish paths, avoided copying the full library for tray
polling, limited icon updates to state changes, kept callbacks allocation/lock
free, and made native tray/thread cleanup explicit. The text helper is limited
to the demonstrated GPUI 0.2.2 rendering bug. The subsequent lifecycle review
required the small vendored GPUI policy patch described below. Native Windows
runtime and real desktop limits above remain unchanged.
## Background review follow-up (2026-10-13)
The first five-cycle close/Open test exposed a leak in repeated Application
creation: process thread counts grew 99, 114, 129, 144, 159, 174. GPUI's
LinuxDispatcher creates a worker pool and Timer per platform; the Timer ignores
channel closure and keeps running. This invalidates repeated-run ownership even
though each reopened window worked. Keeping one Application removes the repeated
platform construction. The final five-cycle test measured 71 threads initially
and 69 after every reopen; RSS was 243228, 242580, 247344, 243032, 243136,
243364 KiB. The rig saves reopen-metrics.tsv and asserts bounded thread/RSS growth.
The pinned Windows source was also examined: WindowsPlatform::new calls
OleInitialize; Drop destroys the message-only platform window and calls
OleUninitialize. The platform RegisterClassW result is ignored; the visible
window class uses Once, so repeated class registration itself is not a panic.
The VSync thread uses weak platform state and can exit on drop, and Windows
uses the OS thread pool rather than Linux's owned worker pool. Both dispatchers,
however, forget scheduled runnables when the main channel has closed, assuming
process shutdown. Repeated Application lifetimes are therefore not established
as safe on Windows. Clipbin avoids them entirely: fonts, dispatcher, OLE,
classes, DirectX state and native loop initialize once. The Windows last-window
policy is patched in the same backend decision as Linux; its runtime/build
remains unvalidated because the complete GPUI desktop cannot be cross-checked
here. The unmodified published dependency is retained with its Apache license;
clipbin-background.patch records the complete source delta. The only additional
compatibility change gives two taffy float literals explicit f32 types for
Rust 1.98's strict lint.
Follow-up artifacts live under target/background-review; reopen-before and
reopen-after contain lifecycle metrics. The silent-monitor experiment also
passed with the sink initially suspended. An initial muted-mic experiment used
an absent default input and correctly degraded; the committed fixture explicitly
selects a real monitor source before muting it.
Follow-up validation (all exit 0, 2026-10-13):
```sh
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo fmt --all -- --check
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo clippy --locked --workspace --all-targets -- -D warnings
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo test --locked --workspace
env RUSTUP_HOME=$HOME/.cache/clipbin/rustup CARGO_HOME=$HOME/.cache/clipbin/cargo-home PATH=$HOME/.cache/clipbin/cargo-home/bin:$PATH CARGO_TARGET_DIR=target/windows-check cargo clippy --locked --target x86_64-pc-windows-msvc -p clipbin-core -p clipbin-obs -p clipbin-probe --all-targets -- -D warnings
env PATH=/usr/bin:/bin:$HOME/.cargo/bin TMPDIR=/home/harry/Dev/clipbin-desktop/target/rig-tmp scripts/validate/linux-background.sh /home/harry/Dev/clipbin-desktop/target/background-review/reopen-after
env PATH=/usr/bin:/bin:$HOME/.cargo/bin TMPDIR=/home/harry/Dev/clipbin-desktop/target/rig-tmp scripts/validate/linux-rig.sh /home/harry/Dev/clipbin-desktop/target/background-review/capture
env PATH=/usr/bin:/bin:$HOME/.cargo/bin TMPDIR=/home/harry/Dev/clipbin-desktop/target/rig-tmp bash -e -c 'for suite in linux-session linux-clips linux-playback; do scripts/validate/${suite}.sh /home/harry/Dev/clipbin-desktop/target/background-review/${suite}; done'
```
All 58 workspace tests passed (fixture-dependent media tests retain their
existing opt-in behavior). All twelve capture scenarios passed; silence and
muted-mic logs show the initially suspended sink and respectively 650 and
650/645 captured packets, with no degradation. Audio-server loss still degrades
while video continues. Lifecycle passed SNI registration/menu actions,
--background, autostart write/remove, close survival, second launch, all five
reopen cycles, audio warning, clean Quit, watcher loss and no-host fallback.
Session finalization/library reload, clips/bookmarks/protected cleanup, and
playback all passed after the persistent-loop change. Playback changed 6.4% of
preview pixels, audio RMS was 0.08044; decode median/p95 was 4.951/11.467 ms and
GPUI upload median/p95 was 0.186/0.334 ms. The simplify quick self-review checked
reuse, clarity and efficiency; it retained existing actions/clean finish, one
application/dispatcher, weak fault registration with explicit cleanup, and no
new callback locks or allocations. The Windows policy flag uses a separate
Cell to avoid adding native state borrows during close handling.
+14 -6
View File
@@ -42,6 +42,10 @@ cleanup() {
wait 2>/dev/null || true
# The D-Bus-activated document portal mounts a FUSE filesystem here.
fusermount3 -u "${XDG_RUNTIME_DIR}/doc" 2>/dev/null || true
if [[ -n "${CLIPBIN_RIG_KEEP_LOGS:-}" ]]; then
mkdir -p "${CLIPBIN_RIG_KEEP_LOGS}"
cp "${rig_dir}"/*.log "${CLIPBIN_RIG_KEEP_LOGS}/" 2>/dev/null || true
fi
rm -rf "${rig_dir}"
}
trap cleanup EXIT
@@ -70,6 +74,7 @@ pipewire > "${rig_dir}/pipewire.log" 2>&1 & pids+=($!)
wait_for pipewire '[[ -S "${XDG_RUNTIME_DIR}/pipewire-0" ]]'
wireplumber > "${rig_dir}/wireplumber.log" 2>&1 & pids+=($!)
pipewire-pulse > "${rig_dir}/pipewire-pulse.log" 2>&1 & pids+=($!)
echo "${pids[-1]}" > "${rig_dir}/pulse.pid"
wait_for pipewire-pulse '[[ -S "${XDG_RUNTIME_DIR}/pulse/native" ]]'
# No sound card here: route playback to a virtual sink whose monitor OBS
# records as desktop audio.
@@ -89,11 +94,12 @@ printf '%s\n' \
"max_fps=60" \
> "${XDG_CONFIG_HOME}/xdg-desktop-portal-wlr/config"
export SWAYSOCK="${XDG_RUNTIME_DIR}/clipbin-sway.sock"
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_HEADLESS_OUTPUTS=1 \
sway --config "${XDG_CONFIG_HOME}/sway/config" > "${rig_dir}/sway.log" 2>&1 & pids+=($!)
wait_for sway 'compgen -G "${XDG_RUNTIME_DIR}/wayland-*" > /dev/null'
WAYLAND_DISPLAY="$(basename "$(compgen -G "${XDG_RUNTIME_DIR}/wayland-*" | grep -v lock | head -1)")"
SWAYSOCK="$(compgen -G "${XDG_RUNTIME_DIR}/sway-ipc.*" | head -1)"
wait_for "sway IPC" '[[ -S "${SWAYSOCK}" ]]'
export WAYLAND_DISPLAY SWAYSOCK
dbus-update-activation-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_RUNTIME_DIR XDG_CONFIG_HOME SWAYSOCK
/usr/lib/xdg-desktop-portal-wlr > "${rig_dir}/xdpw.log" 2>&1 & pids+=($!)
@@ -110,9 +116,13 @@ if [[ "${mode}" == x11 ]]; then
video_driver=x11
fi
# A moving 60 FPS picture with a 440 Hz tone, standing in for a game.
# A moving 60 FPS picture, optionally without any audio stream for idle tests.
pattern="testsrc2=size=${width}x${height}:rate=60,format=yuv420p[out0]"
if [[ "${CLIPBIN_RIG_NO_AUDIO:-0}" != 1 ]]; then
pattern+=';sine=frequency=440:sample_rate=48000[out1]'
fi
SDL_VIDEODRIVER="${video_driver}" ffplay -loglevel error -fs -window_title clipbin-test-game \
-f lavfi "testsrc2=size=${width}x${height}:rate=60,format=yuv420p[out0];sine=frequency=440:sample_rate=48000[out1]" \
-f lavfi "${pattern}" \
> "${rig_dir}/game.log" 2>&1 & pids+=($!)
echo "${pids[-1]}" > "${rig_dir}/game.pid"
if [[ "${mode}" == x11 ]]; then
@@ -126,7 +136,5 @@ export CLIPBIN_RIG_DIR="${rig_dir}"
echo "rig: ready (${mode}, logs in ${rig_dir})" >&2
status=0
"$@" || status=$?
if [[ -n "${CLIPBIN_RIG_KEEP_LOGS:-}" ]]; then
mkdir -p "${CLIPBIN_RIG_KEEP_LOGS}" && cp "${rig_dir}"/*.log "${CLIPBIN_RIG_KEEP_LOGS}/"
fi
exit "${status}"
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# Serial tray/window/autostart validation in the private SNI-host rig.
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
out="${1:-${TMPDIR:-/tmp}/clipbin-background-validation}"
mkdir -p "${out}"
cargo build --locked -p clipbin-desktop -p rig-pointer
cargo build --locked -p clipbin-core --example sni-host
export CLIPBIN_BACKGROUND_REPO="${repo}" CLIPBIN_BACKGROUND_OUT="${out}"
"${repo}/scripts/rig/headless.sh" bash <<'INNER'
set -euo pipefail
repo="${CLIPBIN_BACKGROUND_REPO}" out="${CLIPBIN_BACKGROUND_OUT}"
export XDG_CONFIG_HOME="${out}/config" XDG_DATA_HOME="${out}/data"
mkdir -p "${XDG_CONFIG_HOME}" "${XDG_DATA_HOME}"
source "${repo}/scripts/rig/keys.sh"
"${repo}/target/debug/examples/sni-host" > "${out}/host.log" 2>&1 & host=$!
app= game=
trap 'kill "${app:-}" "${game:-}" "${host:-}" 2>/dev/null || true' EXIT
sleep 1
swaymsg 'for_window [app_id="dev.clipbin.Clipbin"] floating enable' >/dev/null
swaymsg 'for_window [app_id="dev.clipbin.Clipbin"] resize set width 1360 px height 860 px' >/dev/null
swaymsg 'for_window [app_id="dev.clipbin.Clipbin"] move position 280 110' >/dev/null
windows() {
swaymsg -t get_tree | python3 -c 'import json,sys
nodes=[json.load(sys.stdin)]; total=0
while nodes:
n=nodes.pop(); nodes.extend(n.get("nodes",[])+n.get("floating_nodes",[])); total+=n.get("app_id")=="dev.clipbin.Clipbin"
print(total)'
}
wait_windows() {
for _ in $(seq 50); do [[ "$(windows)" == "$1" ]] && return 0; sleep .2; done
echo "FAIL expected $1 Clipbin windows" >&2; exit 1
}
wait_exit() {
for _ in $(seq 100); do if ! kill -0 "${app}" 2>/dev/null; then wait "${app}"; return; fi; sleep .2; done
echo 'FAIL Quit did not exit' >&2; exit 1
}
CLIPBIN_ROUTE=settings "${repo}/target/debug/clipbin-desktop" --background > "${out}/desktop.log" 2>&1 & app=$!
sleep 3
kill -0 "${app}"
[[ "$(windows)" == 0 ]]
[[ -f "${XDG_CONFIG_HOME}/autostart/clipbin.desktop" ]]
grep -q -- '--background' "${XDG_CONFIG_HOME}/autostart/clipbin.desktop"
item="$(sed -n 's/^registered=//p' "${out}/host.log" | tail -1)"
[[ -n "${item}" ]]
gdbus call --session --dest org.kde.StatusNotifierWatcher --object-path /StatusNotifierWatcher \
--method org.freedesktop.DBus.Properties.Get org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems > "${out}/registered.txt"
grep -q "${item}" "${out}/registered.txt"
gdbus call --session --dest "${item}" --object-path /MenuBar \
--method com.canonical.dbusmenu.GetLayout -- 0 -1 '[]' > "${out}/menu.txt"
grep -q 'Open Clipbin' "${out}/menu.txt"
menu() { gdbus call --session --dest "${item}" --object-path /MenuBar --method com.canonical.dbusmenu.Event "$1" clicked '<0>' 0 > /dev/null; }
menu 1
wait_windows 1
swaymsg '[title="clipbin-test-game"] fullscreen disable' >/dev/null
swaymsg '[app_id="dev.clipbin.Clipbin"] floating enable, resize set width 1360 px height 860 px, move position center, focus' >/dev/null
sleep 1
grim "${out}/settings.png"
# Coordinates within the centred 1360x860 settings window at 1920x1080.
printf 'move 1330 550\nsleep .2\nclick\nsleep .3\n' | "${repo}/target/debug/rig-pointer" 1920 1080
sleep 1
[[ ! -f "${XDG_CONFIG_HOME}/autostart/clipbin.desktop" ]]
printf 'move 1330 550\nsleep .2\nclick\nsleep .3\n' | "${repo}/target/debug/rig-pointer" 1920 1080
sleep 1
[[ -f "${XDG_CONFIG_HOME}/autostart/clipbin.desktop" ]]
echo 'PASS SNI registration, menu Open, background startup and autostart setting'
bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' & game=$!
for _ in $(seq 40); do grep -q 'recorder_status=Recording' "${out}/desktop.log" && break; sleep 1; done
grep -q 'recorder_status=Recording' "${out}/desktop.log"
gdbus call --session --dest "${item}" --object-path /StatusNotifierItem --method org.freedesktop.DBus.Properties.Get org.kde.StatusNotifierItem ToolTip > "${out}/recording-tooltip.txt"
grep -q Recording "${out}/recording-tooltip.txt"
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
wait_windows 0
before="$(find "${XDG_DATA_HOME}/clipbin/sessions" -name '*.mkv' | wc -l)"
sleep 5
after="$(find "${XDG_DATA_HOME}/clipbin/sessions" -name '*.mkv' | wc -l)"
(( after > before ))
"${repo}/target/debug/clipbin-desktop" > "${out}/second-launch.log" 2>&1
wait_windows 1
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
wait_windows 0
menu 1
wait_windows 1
# Repeated reopen must release each UI's threads and allocation state.
measure() {
python3 - "${app}" "$1" <<'PYMETRICS'
import pathlib,sys
status=dict(line.split(':',1) for line in pathlib.Path(f'/proc/{sys.argv[1]}/status').read_text().splitlines() if ':' in line)
print(f"{sys.argv[2]}\t{status['Threads'].strip()}\t{status['VmRSS'].split()[0]}")
PYMETRICS
}
measure 0 > "${out}/reopen-metrics.tsv"
for cycle in $(seq 5); do
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
wait_windows 0
sleep 1
menu 1
wait_windows 1
sleep 1
measure "${cycle}" >> "${out}/reopen-metrics.tsv"
done
cat "${out}/reopen-metrics.tsv"
python3 - "${out}/reopen-metrics.tsv" <<'PYBOUNDS'
import pathlib,sys
rows=[tuple(map(int,line.split())) for line in pathlib.Path(sys.argv[1]).read_text().splitlines()]
assert max(r[1] for r in rows[1:]) <= rows[0][1]+4, rows
# Allow the first reopen to warm caches; later windows must stay bounded.
rss=[r[2] for r in rows[1:]]
assert max(rss)-min(rss) <= 32*1024 and rows[-1][2] <= rows[0][2]+64*1024, rows
print('PASS five close/reopen cycles have bounded thread count and RSS')
PYBOUNDS
# Audio delivery loss must be visible in the application and real tray tooltip.
kill "$(cat "${CLIPBIN_RIG_DIR}/pulse.pid")"
for _ in $(seq 15); do grep -q 'recorder_status=Degraded' "${out}/desktop.log" && break; sleep 1; done
grep -q 'recorder_status=Degraded' "${out}/desktop.log"
sleep .3
gdbus call --session --dest "${item}" --object-path /StatusNotifierItem --method org.freedesktop.DBus.Properties.Get org.kde.StatusNotifierItem ToolTip > "${out}/audio-lost-tooltip.txt"
grep -q 'audio capture lost' "${out}/audio-lost-tooltip.txt"
grim "${out}/audio-lost.png"
echo 'PASS audio loss is degraded in the UI and tray while video continues'
# Quit through D-Bus menu while a game is still alive.
menu 4
wait_exit
python3 - "${XDG_DATA_HOME}" <<'PY'
import pathlib,json,sys
files=list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'))
assert files and all(not json.loads(p.read_text())['session']['active'] for p in files)
assert all(s['complete'] for p in files for s in json.loads(p.read_text())['segments'])
PY
kill "${game}"; wait "${game}" 2>/dev/null || true; game=
echo 'PASS close keeps segments growing, second launch reopens, tray Open reopens, tray Quit finalises'
# Losing the watcher must reveal the UI; closing then quits cleanly.
"${repo}/target/debug/clipbin-desktop" --background > "${out}/host-loss.log" 2>&1 & app=$!
sleep 2
wait_windows 0
kill "${host}"; wait "${host}" 2>/dev/null || true; host=
wait_windows 1
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
wait_exit
# Reconcile an externally disabled saved setting, removing the existing entry.
python3 - "${XDG_CONFIG_HOME}/clipbin/settings.json" <<'PYSETTINGS'
import json,pathlib,sys
p=pathlib.Path(sys.argv[1]); settings=json.loads(p.read_text()); settings['start_on_login']=False; p.write_text(json.dumps(settings))
PYSETTINGS
# With no watcher at startup --background must open a reachable window.
"${repo}/target/debug/clipbin-desktop" --background > "${out}/no-tray.log" 2>&1 & app=$!
wait_windows 1
grep -q 'Tray unavailable' "${out}/no-tray.log"
[[ ! -f "${XDG_CONFIG_HOME}/autostart/clipbin.desktop" ]]
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
wait_exit
echo 'PASS watcher loss reveals UI; no-tray background falls back to close=Quit'
INNER
+33
View File
@@ -115,6 +115,39 @@ else
result "${name}" 1
fi
# 9. Capture audio disappears while video/output remain healthy.
name=audio-source-lost
"${rig}" bash -c "'${probe}' record --seconds 15 --out '${out}/${name}' & p=\$!; sleep 6; kill \$(cat \"\${CLIPBIN_RIG_DIR}/pulse.pid\"); wait \$p" \
> "${out}/${name}.log" 2>&1
probe_exit=$?
if [[ "${probe_exit}" == 0 ]] && has "${out}/${name}.log" "status=Recording" \
&& has "${out}/${name}.log" "audio capture lost" \
&& ! has "${out}/${name}.log" "status=failed"; then
result "${name}" 0 "video continued; audio loss surfaced as degraded"
else
result "${name}" 1
fi
# 10. Silence is healthy: no playback streams, idle >10s before monitor capture.
for mode in silence muted-mic; do
name="audio-${mode}"
mic=""
setup=""
if [[ "${mode}" == muted-mic ]]; then
mic="--mic"
setup="pactl set-default-source clipbin-rig-sink.monitor; pactl set-source-mute clipbin-rig-sink.monitor 1;"
fi
CLIPBIN_RIG_NO_AUDIO=1 "${rig}" bash -c "sleep 12; test -z \"\$(pactl list short sink-inputs)\" || exit 1; pactl list short sinks | grep SUSPENDED || exit 1; ${setup} '${probe}' record ${mic} --seconds 16 --out '${out}/${name}'; status=\$?; pactl list short sinks; exit \$status" \
> "${out}/${name}.log" 2>&1
probe_exit=$?
if [[ "${probe_exit}" == 0 ]] && has "${out}/${name}.log" "status=Recording" \
&& ! has "${out}/${name}.log" "status=(degraded|failed)"; then
result "${name}" 0 "no playback for >10s, capture remains healthy"
else
result "${name}" 1
fi
done
echo
if [[ "${failures}" == 0 ]]; then
echo "All scenarios passed. Logs in ${out}"
+12 -12
View File
@@ -20,6 +20,7 @@ done
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
out="${1:-${TMPDIR:-/tmp}/clipbin-session-validation}"
mkdir -p "${out}"
cargo build --locked -p clipbin-core --example sni-host
cargo build --locked -p clipbin-desktop --manifest-path "${repo}/Cargo.toml"
export CLIPBIN_SESSION_REPO="${repo}" CLIPBIN_SESSION_OUT="${out}"
"${repo}/scripts/rig/headless.sh" bash <<'INNER'
@@ -46,13 +47,17 @@ wait_log() {
echo "FAIL timed out waiting for ${minimum} '${pattern}' entries; log: ${log}" >&2
exit 1
}
"${CLIPBIN_SESSION_REPO}/target/debug/examples/sni-host" > "${CLIPBIN_SESSION_OUT}/tray-host.log" 2>&1 &
host=$!
sleep 1
source "${CLIPBIN_SESSION_REPO}/scripts/rig/keys.sh"
out="${CLIPBIN_SESSION_OUT}"
portal_config="${XDG_CONFIG_HOME}"
export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config"
mkdir -p "${XDG_DATA_HOME}" "${XDG_CONFIG_HOME}"
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/desktop.log" 2>&1 &
app=$!
trap 'kill "${app}" "${game:-}" "${portal:-}" 2>/dev/null || true' EXIT
trap 'kill "${app}" "${game:-}" "${portal:-}" "${host}" 2>/dev/null || true' EXIT
sleep 3
swaymsg '[title="clipbin-test-game"] fullscreen disable' > /dev/null || true
swaymsg '[app_id="dev.clipbin.Clipbin"] floating enable, resize set 1360 860, move position center, focus' > /dev/null
@@ -147,25 +152,20 @@ import pathlib,json,sys
files=list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'))
assert files and all(not json.loads(p.read_text())['session']['active'] for p in files)
PY
for _ in $(seq 10); do
if ! kill -0 "${app}" 2>/dev/null; then break; fi
sleep 1
done
if kill -0 "${app}" 2>/dev/null; then echo 'FAIL closed app kept discovering after game exit' >&2; exit 1; fi
wait "${app}"
kill -0 "${app}"
kill "$(cat "${CLIPBIN_RIG_DIR}/game.pid")"
# Reopen to prove the library loads from disk, then Quit cleanly with Ctrl+Q.
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/reopened.log" 2>&1 &
app=$!
# A second launch signals the original instance and exits; background discovery survives.
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/second-launch.log" 2>&1
kill -0 "${app}"
sleep 4
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
sleep 0.5
grim "${out}/library.png"
bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' &
game=$!
wait_log 'recorder_status=Recording' "${out}/reopened.log" 1 30
wait_log 'recorder_status=Recording' "${out}/desktop.log" 2 30
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
wtype -M ctrl -k q -m ctrl
rig_key q ctrl
for _ in $(seq 20); do
if ! kill -0 "${app}" 2>/dev/null; then break; fi
sleep 1
+14
View File
@@ -0,0 +1,14 @@
# Clipbin GPUI 0.2.2 patch
Baseline: crates.io gpui 0.2.2, checksum
`979b45cfa6ec723b6f42330915a1b3769b930d02b2d505f9697f8ca602bee707`.
Original Apache-2.0 license retained. This dependency is excluded from Clipbin's
workspace; its existing third-party unsafe code is unchanged.
`clipbin-background.patch` is the complete source delta from the published
crate (zero-context unified diff; apply with `patch -p1` or
`git apply --unidiff-zero`). It adds App::set_quit_on_last_window_close and an initially true backend
flag on Wayland, X11 and Windows. Clipbin sets it false so one Application/loop
lives until explicit Quit, while visible windows can be destroyed/recreated.
No timers, dispatchers or FFI calls are changed. Two taffy float literals
are explicitly typed f32 to satisfy Rust 1.98 strict clippy.
+749
View File
@@ -0,0 +1,749 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "gpui"
version = "0.2.2"
authors = ["Nathan Sobo <nathan@zed.dev>"]
build = "build.rs"
publish = true
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Zed's GPU-accelerated UI framework"
homepage = "https://gpui.rs"
readme = "README.md"
keywords = [
"desktop",
"gui",
"immediate",
]
categories = ["gui"]
license = "Apache-2.0"
repository = "https://github.com/zed-industries/zed"
resolver = "2"
[features]
default = [
"font-kit",
"wayland",
"x11",
"windows-manifest",
]
inspector = ["gpui_macros/inspector"]
leak-detection = ["backtrace"]
macos-blade = [
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"objc2",
"objc2-metal",
]
runtime_shaders = []
screen-capture = ["scap"]
test-support = [
"leak-detection",
"collections/test-support",
"rand",
"util/test-support",
"http_client/test-support",
"wayland",
"x11",
]
wayland = [
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"ashpd/wayland",
"cosmic-text",
"font-kit",
"calloop-wayland-source",
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"filedescriptor",
"xkbcommon",
"open",
]
windows-manifest = []
x11 = [
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"ashpd",
"cosmic-text",
"font-kit",
"as-raw-xcb-connection",
"x11rb",
"xkbcommon",
"xim",
"x11-clipboard",
"filedescriptor",
"open",
"scap?/x11",
]
[lib]
name = "gpui"
path = "src/gpui.rs"
doctest = false
[[example]]
name = "animation"
path = "examples/animation.rs"
[[example]]
name = "data_table"
path = "examples/data_table.rs"
[[example]]
name = "drag_drop"
path = "examples/drag_drop.rs"
[[example]]
name = "gif_viewer"
path = "examples/gif_viewer.rs"
[[example]]
name = "gradient"
path = "examples/gradient.rs"
[[example]]
name = "grid_layout"
path = "examples/grid_layout.rs"
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"
[[example]]
name = "image"
path = "examples/image/image.rs"
[[example]]
name = "image_gallery"
path = "examples/image_gallery.rs"
[[example]]
name = "image_loading"
path = "examples/image_loading.rs"
[[example]]
name = "input"
path = "examples/input.rs"
[[example]]
name = "on_window_close_quit"
path = "examples/on_window_close_quit.rs"
[[example]]
name = "opacity"
path = "examples/opacity.rs"
[[example]]
name = "ownership_post"
path = "examples/ownership_post.rs"
[[example]]
name = "painting"
path = "examples/painting.rs"
[[example]]
name = "paths_bench"
path = "examples/paths_bench.rs"
[[example]]
name = "pattern"
path = "examples/pattern.rs"
[[example]]
name = "scrollable"
path = "examples/scrollable.rs"
[[example]]
name = "set_menus"
path = "examples/set_menus.rs"
[[example]]
name = "shadow"
path = "examples/shadow.rs"
[[example]]
name = "svg"
path = "examples/svg/svg.rs"
[[example]]
name = "tab_stop"
path = "examples/tab_stop.rs"
[[example]]
name = "text"
path = "examples/text.rs"
[[example]]
name = "text_layout"
path = "examples/text_layout.rs"
[[example]]
name = "text_wrapper"
path = "examples/text_wrapper.rs"
[[example]]
name = "tree"
path = "examples/tree.rs"
[[example]]
name = "uniform_list"
path = "examples/uniform_list.rs"
[[example]]
name = "window"
path = "examples/window.rs"
[[example]]
name = "window_positioning"
path = "examples/window_positioning.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[test]]
name = "action_macros"
path = "tests/action_macros.rs"
[dependencies.anyhow]
version = "1.0.86"
[dependencies.async-task]
version = "4.7"
[dependencies.backtrace]
version = "0.3"
optional = true
[dependencies.blade-graphics]
version = "0.7.0"
optional = true
[dependencies.blade-macros]
version = "0.3.0"
optional = true
[dependencies.blade-util]
version = "0.3.0"
optional = true
[dependencies.bytemuck]
version = "1"
optional = true
[dependencies.collections]
version = "0.2.2"
package = "gpui_collections"
[dependencies.ctor]
version = "0.4.0"
[dependencies.derive_more]
version = "0.99.17"
[dependencies.etagere]
version = "0.2"
[dependencies.futures]
version = "0.3"
[dependencies.gpui_macros]
version = "0.2.2"
package = "gpui-macros"
[dependencies.http_client]
version = "0.2.2"
package = "gpui_http_client"
[dependencies.image]
version = "0.25.1"
[dependencies.inventory]
version = "0.3.19"
[dependencies.itertools]
version = "0.14.0"
[dependencies.libc]
version = "0.2"
[dependencies.log]
version = "0.4.16"
features = [
"kv_unstable_serde",
"serde",
]
[dependencies.lyon]
version = "1.0"
[dependencies.num_cpus]
version = "1.13"
[dependencies.parking]
version = "2.0.0"
[dependencies.parking_lot]
version = "0.12.1"
[dependencies.pin-project]
version = "1.1.10"
[dependencies.postage]
version = "0.5"
features = ["futures-traits"]
[dependencies.profiling]
version = "1"
[dependencies.rand]
version = "0.9"
optional = true
[dependencies.raw-window-handle]
version = "0.6"
[dependencies.refineable]
version = "0.2.2"
package = "gpui_refineable"
[dependencies.resvg]
version = "0.45.0"
features = [
"text",
"system-fonts",
"memmap-fonts",
]
default-features = false
[dependencies.schemars]
version = "1.0"
features = ["indexmap2"]
[dependencies.seahash]
version = "4.1"
[dependencies.semantic_version]
version = "0.2.2"
package = "gpui_semantic_version"
[dependencies.serde]
version = "1.0.221"
features = [
"derive",
"rc",
]
[dependencies.serde_json]
version = "1.0.144"
features = [
"preserve_order",
"raw_value",
]
[dependencies.slotmap]
version = "1.0.6"
[dependencies.smallvec]
version = "1.6"
features = ["union"]
[dependencies.smol]
version = "2.0"
[dependencies.stacksafe]
version = "0.1"
[dependencies.strum]
version = "0.27.2"
features = ["derive"]
[dependencies.sum_tree]
version = "0.2.2"
package = "gpui_sum_tree"
[dependencies.taffy]
version = "=0.9.0"
[dependencies.thiserror]
version = "2.0.12"
[dependencies.usvg]
version = "0.45.0"
default-features = false
[dependencies.util]
version = "0.2.2"
package = "gpui_util"
[dependencies.util_macros]
version = "0.2.2"
package = "gpui_util_macros"
[dependencies.uuid]
version = "1.1.2"
features = [
"v4",
"v5",
"v7",
"serde",
]
[dependencies.waker-fn]
version = "1.2.0"
[dev-dependencies.backtrace]
version = "0.3"
[dev-dependencies.collections]
version = "0.2.2"
features = ["test-support"]
package = "gpui_collections"
[dev-dependencies.env_logger]
version = "0.11"
[dev-dependencies.http_client]
version = "0.2.2"
features = ["test-support"]
package = "gpui_http_client"
[dev-dependencies.lyon]
version = "1.0"
features = ["extra"]
[dev-dependencies.pretty_assertions]
version = "1.3.0"
features = ["unstable"]
[dev-dependencies.rand]
version = "0.9"
[dev-dependencies.unicode-segmentation]
version = "1.10"
[dev-dependencies.util]
version = "0.2.2"
features = ["test-support"]
package = "gpui_util"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.as-raw-xcb-connection]
version = "1"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.ashpd]
version = "0.11"
features = ["async-std"]
optional = true
default-features = false
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.blade-graphics]
version = "0.7.0"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.blade-macros]
version = "0.3.0"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.blade-util]
version = "0.3.0"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.bytemuck]
version = "1"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.calloop]
version = "0.13.0"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.calloop-wayland-source]
version = "0.3.0"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.cosmic-text]
version = "0.14.0"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.filedescriptor]
version = "0.8.2"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.flume]
version = "0.11"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.font-kit]
version = "0.14.1-zed"
features = ["source-fontconfig-dlopen"]
optional = true
package = "zed-font-kit"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.oo7]
version = "0.5.0"
features = [
"async-std",
"native_crypto",
]
default-features = false
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.open]
version = "5.2.0"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-backend]
version = "0.3.3"
features = [
"client_system",
"dlopen",
]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-client]
version = "0.31.2"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-cursor]
version = "0.31.1"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols]
version = "0.31.2"
features = [
"client",
"staging",
"unstable",
]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols-plasma]
version = "0.2.0"
features = ["client"]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.x11-clipboard]
version = "0.9.3"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.x11rb]
version = "0.13.1"
features = [
"allow-unsafe-code",
"xkb",
"randr",
"xinput",
"cursor",
"resource_manager",
"sync",
]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.xim]
version = "0.4.0-zed"
features = [
"x11rb-xcb",
"x11rb-client",
]
optional = true
package = "zed-xim"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.xkbcommon]
version = "0.8.0"
features = [
"wayland",
"x11",
]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies.naga]
version = "25.0"
features = ["wgsl-in"]
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies.pathfinder_geometry]
version = "0.5"
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies.scap]
version = "0.0.8-zed"
optional = true
default-features = false
package = "zed-scap"
[target.'cfg(target_os = "macos")'.dependencies.block]
version = "0.1"
[target.'cfg(target_os = "macos")'.dependencies.cocoa]
version = "=0.26.0"
[target.'cfg(target_os = "macos")'.dependencies.cocoa-foundation]
version = "=0.2.0"
[target.'cfg(target_os = "macos")'.dependencies.core-foundation]
version = "=0.10.0"
[target.'cfg(target_os = "macos")'.dependencies.core-foundation-sys]
version = "0.8.6"
[target.'cfg(target_os = "macos")'.dependencies.core-graphics]
version = "0.24"
[target.'cfg(target_os = "macos")'.dependencies.core-text]
version = "21"
[target.'cfg(target_os = "macos")'.dependencies.core-video]
version = "0.4.3"
features = ["metal"]
[target.'cfg(target_os = "macos")'.dependencies.font-kit]
version = "0.14.1-zed"
optional = true
package = "zed-font-kit"
[target.'cfg(target_os = "macos")'.dependencies.foreign-types]
version = "0.5"
[target.'cfg(target_os = "macos")'.dependencies.log]
version = "0.4.16"
features = [
"kv_unstable_serde",
"serde",
]
[target.'cfg(target_os = "macos")'.dependencies.media]
version = "0.2.2"
package = "gpui_media"
[target.'cfg(target_os = "macos")'.dependencies.metal]
version = "0.29"
[target.'cfg(target_os = "macos")'.dependencies.objc]
version = "0.2"
[target.'cfg(target_os = "macos")'.dependencies.objc2]
version = "0.6"
optional = true
[target.'cfg(target_os = "macos")'.dependencies.objc2-metal]
version = "0.3"
optional = true
[target.'cfg(target_os = "macos")'.build-dependencies.bindgen]
version = "0.71"
[target.'cfg(target_os = "macos")'.build-dependencies.cbindgen]
version = "0.28.0"
default-features = false
[target.'cfg(target_os = "macos")'.build-dependencies.naga]
version = "25.0"
features = ["wgsl-in"]
[target.'cfg(target_os = "windows")'.dependencies.flume]
version = "0.11"
[target.'cfg(target_os = "windows")'.dependencies.rand]
version = "0.9"
[target.'cfg(target_os = "windows")'.dependencies.windows]
version = "0.61"
features = [
"Foundation_Numerics",
"Storage_Search",
"Storage_Streams",
"System_Threading",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D_Fxc",
"Win32_Graphics_DirectComposition",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dwm",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
"Win32_Security_Cryptography",
"Win32_Storage_FileSystem",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_Console",
"Win32_System_DataExchange",
"Win32_System_IO",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_Performance",
"Win32_System_Pipes",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Variant",
"Win32_System_WinRT",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
"Win32_UI_Input_Ime",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem",
"Win32_UI_WindowsAndMessaging",
]
[target.'cfg(target_os = "windows")'.dependencies.windows-core]
version = "0.61"
[target.'cfg(target_os = "windows")'.dependencies.windows-numerics]
version = "0.2"
[target.'cfg(target_os = "windows")'.dependencies.windows-registry]
version = "0.5"
[target.'cfg(target_os = "windows")'.build-dependencies.embed-resource]
version = "3.0"
[lints.clippy]
dbg_macro = "deny"
declare_interior_mutable_const = "deny"
disallowed_methods = "deny"
large_enum_variant = "allow"
let_underscore_future = "allow"
nonminimal_bool = "allow"
redundant_clone = "deny"
single_range_in_vec_init = "allow"
todo = "deny"
too_many_arguments = "allow"
type_complexity = "allow"
[lints.clippy.style]
level = "allow"
priority = -1
[lints.rust.unexpected_cfgs]
level = "allow"
priority = 0
+222
View File
@@ -0,0 +1,222 @@
Copyright 2022 - 2025 Zed Industries, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+66
View File
@@ -0,0 +1,66 @@
# Welcome to GPUI!
GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
for Rust, designed to support a wide variety of applications.
## Getting Started
GPUI is still in active development as we work on the Zed code editor, and is still pre-1.0. There will often be breaking changes between versions. You'll also need to use the latest version of stable Rust and be on macOS or Linux. Add the following to your `Cargo.toml`:
```toml
gpui = { version = "*" }
```
- [Ownership and data flow](src/_ownership_and_data_flow.rs)
Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
### Dependencies
GPUI has various system dependencies that it needs in order to work.
#### macOS
On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following:
- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account.
> Ensure you launch Xcode after installing, and install the macOS components, which is the default option.
- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/)
```sh
xcode-select --install
```
- Ensure that the Xcode command line tools are using your newly installed copy of Xcode:
```sh
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
```
## The Big Picture
GPUI offers three different [registers](<https://en.wikipedia.org/wiki/Register_(sociolinguistics)>) depending on your needs:
- State management and communication with `Entity`'s. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's entities. Entities are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::context` module for more information.
- High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply an `Entity` that can be rendered, by implementing the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering.
- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information.
Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework.
## Other Resources
In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications:
- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information.
- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::App`.
- An async executor that is integrated with the platform's event loop. See the `executor` module for more information.,
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
+457
View File
@@ -0,0 +1,457 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))]
//TODO: consider generating shader code for WGSL
//TODO: deprecate "runtime-shaders" and "macos-blade"
use std::env;
fn main() {
let target = env::var("CARGO_CFG_TARGET_OS");
println!("cargo::rustc-check-cfg=cfg(gles)");
#[cfg(any(
not(any(target_os = "macos", target_os = "windows")),
all(target_os = "macos", feature = "macos-blade")
))]
check_wgsl_shaders();
match target.as_deref() {
Ok("macos") => {
#[cfg(target_os = "macos")]
macos::build();
}
Ok("windows") => {
#[cfg(target_os = "windows")]
windows::build();
}
_ => (),
};
}
#[cfg(any(
not(any(target_os = "macos", target_os = "windows")),
all(target_os = "macos", feature = "macos-blade")
))]
fn check_wgsl_shaders() {
use std::path::PathBuf;
use std::process;
use std::str::FromStr;
let shader_source_path = "./src/platform/blade/shaders.wgsl";
let shader_path = PathBuf::from_str(shader_source_path).unwrap();
println!("cargo:rerun-if-changed={}", &shader_path.display());
let shader_source = std::fs::read_to_string(&shader_path).unwrap();
match naga::front::wgsl::parse_str(&shader_source) {
Ok(_) => {
// All clear
}
Err(e) => {
println!("cargo::error=WGSL shader compilation failed:\n{}", e);
process::exit(1);
}
}
}
#[cfg(target_os = "macos")]
mod macos {
use std::{
env,
path::{Path, PathBuf},
};
use cbindgen::Config;
pub(super) fn build() {
generate_dispatch_bindings();
#[cfg(not(feature = "macos-blade"))]
{
let header_path = generate_shader_bindings();
#[cfg(feature = "runtime_shaders")]
emit_stitched_shaders(&header_path);
#[cfg(not(feature = "runtime_shaders"))]
compile_metal_shaders(&header_path);
}
}
fn generate_dispatch_bindings() {
println!("cargo:rustc-link-lib=framework=System");
let bindings = bindgen::Builder::default()
.header("src/platform/mac/dispatch.h")
.allowlist_var("_dispatch_main_q")
.allowlist_var("_dispatch_source_type_data_add")
.allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
.allowlist_var("DISPATCH_TIME_NOW")
.allowlist_function("dispatch_get_global_queue")
.allowlist_function("dispatch_async_f")
.allowlist_function("dispatch_after_f")
.allowlist_function("dispatch_time")
.allowlist_function("dispatch_source_merge_data")
.allowlist_function("dispatch_source_create")
.allowlist_function("dispatch_source_set_event_handler_f")
.allowlist_function("dispatch_resume")
.allowlist_function("dispatch_suspend")
.allowlist_function("dispatch_source_cancel")
.allowlist_function("dispatch_set_context")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.layout_tests(false)
.generate()
.expect("unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("dispatch_sys.rs"))
.expect("couldn't write dispatch bindings");
}
fn generate_shader_bindings() -> PathBuf {
let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let mut config = Config {
include_guard: Some("SCENE_H".into()),
language: cbindgen::Language::C,
no_includes: true,
..Default::default()
};
config.export.include.extend([
"Bounds".into(),
"Corners".into(),
"Edges".into(),
"Size".into(),
"Pixels".into(),
"PointF".into(),
"Hsla".into(),
"ContentMask".into(),
"Uniforms".into(),
"AtlasTile".into(),
"PathRasterizationInputIndex".into(),
"PathVertex_ScaledPixels".into(),
"PathRasterizationVertex".into(),
"ShadowInputIndex".into(),
"Shadow".into(),
"QuadInputIndex".into(),
"Underline".into(),
"UnderlineInputIndex".into(),
"Quad".into(),
"BorderStyle".into(),
"SpriteInputIndex".into(),
"MonochromeSprite".into(),
"PolychromeSprite".into(),
"PathSprite".into(),
"SurfaceInputIndex".into(),
"SurfaceBounds".into(),
"TransformationMatrix".into(),
]);
config.no_includes = true;
config.enumeration.prefix_with_name = true;
let mut builder = cbindgen::Builder::new();
let src_paths = [
crate_dir.join("src/scene.rs"),
crate_dir.join("src/geometry.rs"),
crate_dir.join("src/color.rs"),
crate_dir.join("src/window.rs"),
crate_dir.join("src/platform.rs"),
crate_dir.join("src/platform/mac/metal_renderer.rs"),
];
for src_path in src_paths {
println!("cargo:rerun-if-changed={}", src_path.display());
builder = builder.with_src(src_path);
}
builder
.with_config(config)
.generate()
.expect("Unable to generate bindings")
.write_to_file(&output_path);
output_path
}
/// To enable runtime compilation, we need to "stitch" the shaders file with the generated header
/// so that it is self-contained.
#[cfg(feature = "runtime_shaders")]
fn emit_stitched_shaders(header_path: &Path) {
use std::str::FromStr;
fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
let header_contents = std::fs::read_to_string(header)?;
let shader_contents = std::fs::read_to_string(shader_path)?;
let stitched_contents = format!("{header_contents}\n{shader_contents}");
let out_path =
PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal");
std::fs::write(&out_path, stitched_contents)?;
Ok(out_path)
}
let shader_source_path = "./src/platform/mac/shaders.metal";
let shader_path = PathBuf::from_str(shader_source_path).unwrap();
stitch_header(header_path, &shader_path).unwrap();
println!("cargo:rerun-if-changed={}", &shader_source_path);
}
#[cfg(not(feature = "runtime_shaders"))]
fn compile_metal_shaders(header_path: &Path) {
use std::process::{self, Command};
let shader_path = "./src/platform/mac/shaders.metal";
let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
let metallib_output_path =
PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
println!("cargo:rerun-if-changed={}", shader_path);
let output = Command::new("xcrun")
.args([
"-sdk",
"macosx",
"metal",
"-gline-tables-only",
"-mmacosx-version-min=10.15.7",
"-MO",
"-c",
shader_path,
"-include",
(header_path.to_str().unwrap()),
"-o",
])
.arg(&air_output_path)
.output()
.unwrap();
if !output.status.success() {
println!(
"cargo::error=metal shader compilation failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
process::exit(1);
}
let output = Command::new("xcrun")
.args(["-sdk", "macosx", "metallib"])
.arg(air_output_path)
.arg("-o")
.arg(metallib_output_path)
.output()
.unwrap();
if !output.status.success() {
println!(
"cargo::error=metallib compilation failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
process::exit(1);
}
}
}
#[cfg(target_os = "windows")]
mod windows {
use std::{
fs,
io::Write,
path::{Path, PathBuf},
process::{self, Command},
};
pub(super) fn build() {
// Compile HLSL shaders
#[cfg(not(debug_assertions))]
compile_shaders();
// Embed the Windows manifest and resource file
#[cfg(feature = "windows-manifest")]
embed_resource();
}
#[cfg(feature = "windows-manifest")]
fn embed_resource() {
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
println!("cargo:rerun-if-changed={}", manifest.display());
println!("cargo:rerun-if-changed={}", rc_file.display());
embed_resource::compile(rc_file, embed_resource::NONE)
.manifest_required()
.unwrap();
}
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
fn compile_shaders() {
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("src/platform/windows/shaders.hlsl");
let out_dir = std::env::var("OUT_DIR").unwrap();
println!("cargo:rerun-if-changed={}", shader_path.display());
// Check if fxc.exe is available
let fxc_path = find_fxc_compiler();
// Define all modules
let modules = [
"quad",
"shadow",
"path_rasterization",
"path_sprite",
"underline",
"monochrome_sprite",
"polychrome_sprite",
];
let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
if Path::new(&rust_binding_path).exists() {
fs::remove_file(&rust_binding_path)
.expect("Failed to remove existing Rust binding file");
}
for module in modules {
compile_shader_for_module(
module,
&out_dir,
&fxc_path,
shader_path.to_str().unwrap(),
&rust_binding_path,
);
}
{
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("src/platform/windows/color_text_raster.hlsl");
compile_shader_for_module(
"emoji_rasterization",
&out_dir,
&fxc_path,
shader_path.to_str().unwrap(),
&rust_binding_path,
);
}
}
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
fn find_fxc_compiler() -> String {
// Check environment variable
if let Ok(path) = std::env::var("GPUI_FXC_PATH")
&& Path::new(&path).exists()
{
return path;
}
// Try to find in PATH
// NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe`
if let Ok(output) = std::process::Command::new("where.exe")
.arg("fxc.exe")
.output()
&& output.status.success()
{
let path = String::from_utf8_lossy(&output.stdout);
return path.trim().to_string();
}
// Check the default path
if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
.exists()
{
return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
.to_string();
}
panic!("Failed to find fxc.exe");
}
fn compile_shader_for_module(
module: &str,
out_dir: &str,
fxc_path: &str,
shader_path: &str,
rust_binding_path: &str,
) {
// Compile vertex shader
let output_file = format!("{}/{}_vs.h", out_dir, module);
let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
compile_shader_impl(
fxc_path,
&format!("{module}_vertex"),
&output_file,
&const_name,
shader_path,
"vs_4_1",
);
generate_rust_binding(&const_name, &output_file, rust_binding_path);
// Compile fragment shader
let output_file = format!("{}/{}_ps.h", out_dir, module);
let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
compile_shader_impl(
fxc_path,
&format!("{module}_fragment"),
&output_file,
&const_name,
shader_path,
"ps_4_1",
);
generate_rust_binding(&const_name, &output_file, rust_binding_path);
}
fn compile_shader_impl(
fxc_path: &str,
entry_point: &str,
output_path: &str,
var_name: &str,
shader_path: &str,
target: &str,
) {
let output = Command::new(fxc_path)
.args([
"/T",
target,
"/E",
entry_point,
"/Fh",
output_path,
"/Vn",
var_name,
"/O3",
shader_path,
])
.output();
match output {
Ok(result) => {
if result.status.success() {
return;
}
println!(
"cargo::error=Shader compilation failed for {}:\n{}",
entry_point,
String::from_utf8_lossy(&result.stderr)
);
process::exit(1);
}
Err(e) => {
println!("cargo::error=Failed to run fxc for {}: {}", entry_point, e);
process::exit(1);
}
}
}
fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
let const_definition = {
let global_var_start = header_content.find("const BYTE").unwrap();
let global_var = &header_content[global_var_start..];
let equal = global_var.find('=').unwrap();
global_var[equal + 1..].trim()
};
let rust_binding = format!(
"const {}: &[u8] = &{}\n",
const_name,
const_definition.replace('{', "[").replace('}', "]")
);
let mut options = fs::OpenOptions::new()
.create(true)
.append(true)
.open(output_path)
.expect("Failed to open Rust binding file");
options
.write_all(rust_binding.as_bytes())
.expect("Failed to write Rust binding file");
}
}
+55
View File
@@ -0,0 +1,55 @@
--- a/src/app.rs
+++ b/src/app.rs
@@ -982,0 +983,5 @@
+ /// Keep the native loop alive when the last window closes (for tray apps).
+ pub fn set_quit_on_last_window_close(&self, quit: bool) {
+ self.platform.set_quit_on_last_window_close(quit);
+ }
+
--- a/src/platform/linux/platform.rs
+++ b/src/platform/linux/platform.rs
@@ -96,0 +97 @@
+ pub quit_on_last_window_close: bool,
@@ -122,0 +124 @@
+ quit_on_last_window_close: true,
@@ -232,0 +235,4 @@
+ }
+
+ fn set_quit_on_last_window_close(&self, quit: bool) {
+ self.with_common(|common| common.quit_on_last_window_close = quit);
--- a/src/platform/linux/wayland/client.rs
+++ b/src/platform/linux/wayland/client.rs
@@ -387 +387 @@
- if state.windows.is_empty() {
+ if state.windows.is_empty() && state.common.quit_on_last_window_close {
--- a/src/platform/linux/x11/client.rs
+++ b/src/platform/linux/x11/client.rs
@@ -250 +250 @@
- if state.windows.is_empty() {
+ if state.windows.is_empty() && state.common.quit_on_last_window_close {
--- a/src/platform/windows/platform.rs
+++ b/src/platform/windows/platform.rs
@@ -2 +2 @@
- cell::RefCell,
+ cell::{Cell, RefCell},
@@ -45,0 +46 @@
+ quit_on_last_window_close: Cell<bool>,
@@ -369,0 +371,4 @@
+ fn set_quit_on_last_window_close(&self, quit: bool) {
+ self.inner.quit_on_last_window_close.set(quit);
+ }
+
@@ -682,0 +688 @@
+ quit_on_last_window_close: Cell::new(true),
@@ -744 +750 @@
- lock.is_empty()
+ lock.is_empty() && self.quit_on_last_window_close.get()
--- a/src/platform.rs
+++ b/src/platform.rs
@@ -171,0 +172 @@
+ fn set_quit_on_last_window_close(&self, _quit: bool) {}
--- a/src/taffy.rs
+++ b/src/taffy.rs
@@ -312 +312 @@
- unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])])
+ unit.map(|count| vec![repeat(count, vec![minmax(length(0.0_f32), fr(1.0_f32))])])
+33
View File
@@ -0,0 +1,33 @@
# Contexts
GPUI makes extensive use of _context parameters_ (typically named `cx`) to provide access to application state and services. These contexts are references passed to functions, enabling interaction with global state, windows, entities, and system services.
---
## `App`
The root context granting access to the application's global state. This context owns all entities' data and can be used to read or update the data referenced by an `Entity<T>`.
## `Context<T>`
A context provided when interacting with an `Entity<T>`, with additional methods related to that specific entity such as notifying observers and emitting events. This context dereferences into `App`, meaning any function which can take an `App` reference can also take a `Context<T>` reference, allowing you to access the application's global state.
## `AsyncApp` and `AsyncWindowContext`
Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with entities with an async context, the calls become fallible, because the context may outlive the window or even the app itself.
## `TestAppContext`
These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests.
---
# Non-Context Core Types
## `Window`
Provides access to the state of an application window. This type has a root view (an `Entity` implementing `Render`) which it can read/update, but since it is not a context, you must pass a `&mut App` (or a context which dereferences to it) to do so, along with other functions interacting with global state. You can obtain a `Window` from an `WindowHandle` by calling `WindowHandle::update`.
## `Entity<T>`
A handle to a structure requiring state. This data is owned by the `App` and can be accessed and modified via references to contexts. If `T` implements `Render`, then the entity is sometimes referred to as a view. Entities can be observed by other entities and windows, allowing a closure to be called when `notify` is called on the entity's `Context`.
+100
View File
@@ -0,0 +1,100 @@
# Key Dispatch
GPUI is designed for keyboard-first interactivity.
To expose functionality to the mouse, you render a button with a click handler.
To expose functionality to the keyboard, you bind an _action_ in a _key context_.
Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself:
```rust
mod menu {
#[gpui::action]
struct MoveUp;
#[gpui::action]
struct MoveDown;
}
```
Actions are frequently unit structs, for which we have a macro. The above could also be written:
```rust
mod menu {
actions!(gpui, [MoveUp, MoveDown]);
}
```
Actions can also be more complex types:
```rust
mod menu {
#[gpui::action]
struct Move {
direction: Direction,
select: bool,
}
}
```
To bind actions, chain `on_action` on to your element:
```rust
impl Render for Menu {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.on_action(|this: &mut Menu, move: &MoveUp, window: &mut Window, cx: &mut Context<Menu>| {
// ...
})
.on_action(|this, move: &MoveDown, cx| {
// ...
})
.children(unimplemented!())
}
}
```
In order to bind keys to actions, you need to declare a _key context_ for part of the element tree by calling `key_context`.
```rust
impl Render for Menu {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context("menu")
.on_action(|this: &mut Menu, move: &MoveUp, window: &mut Window, cx: &mut Context<Menu>| {
// ...
})
.on_action(|this, move: &MoveDown, cx| {
// ...
})
.children(unimplemented!())
}
}
```
Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name.
```json
{
"context": "menu",
"bindings": {
"up": "menu::MoveUp",
"down": "menu::MoveDown"
}
}
```
If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name:
```json
{
"context": "menu",
"bindings": {
"up": ["menu::Move", {direction: "up", select: false}]
"down": ["menu::Move", {direction: "down", select: false}]
"shift-up": ["menu::Move", {direction: "up", select: true}]
"shift-down": ["menu::Move", {direction: "down", select: true}]
}
}
```
+121
View File
@@ -0,0 +1,121 @@
use std::time::Duration;
use anyhow::Result;
use gpui::{
Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, SharedString,
Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage,
prelude::*, px, 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/image/arrow_circle.svg"
);
struct AnimationExample {}
impl Render for AnimationExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.bg(gpui::white())
.text_color(gpui::black())
.justify_around()
.child(
div()
.flex()
.flex_col()
.size_full()
.justify_around()
.child(
div()
.id("content")
.flex()
.flex_col()
.h(px(150.))
.overflow_y_scroll()
.w_full()
.flex_1()
.justify_center()
.items_center()
.text_xl()
.gap_4()
.child("Hello Animation")
.child(
svg()
.size_20()
.overflow_hidden()
.path(ARROW_CIRCLE_SVG)
.text_color(gpui::black())
.with_animation(
"image_circle",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(bounce(ease_in_out)),
|svg, delta| {
svg.with_transformation(Transformation::rotate(
percentage(delta),
))
},
),
),
)
.child(
div()
.flex()
.h(px(64.))
.w_full()
.p_2()
.justify_center()
.items_center()
.border_t_1()
.border_color(gpui::black().opacity(0.1))
.bg(gpui::black().opacity(0.05))
.child("Other Panel"),
),
)
}
}
fn main() {
Application::new()
.with_assets(Assets {})
.run(|cx: &mut App| {
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(300.), px(300.)),
cx,
))),
..Default::default()
};
cx.open_window(options, |_, cx| {
cx.activate(false);
cx.new(|_| AnimationExample {})
})
.unwrap();
});
}
+473
View File
@@ -0,0 +1,473 @@
use std::{ops::Range, rc::Rc, time::Duration};
use gpui::{
App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
Render, SharedString, UniformListScrollHandle, Window, WindowBounds, WindowOptions, canvas,
div, point, prelude::*, px, rgb, size, uniform_list,
};
const TOTAL_ITEMS: usize = 10000;
const SCROLLBAR_THUMB_WIDTH: Pixels = px(8.);
const SCROLLBAR_THUMB_HEIGHT: Pixels = px(100.);
pub struct Quote {
name: SharedString,
symbol: SharedString,
last_done: f64,
prev_close: f64,
open: f64,
high: f64,
low: f64,
timestamp: Duration,
volume: i64,
turnover: f64,
ttm: f64,
market_cap: f64,
float_cap: f64,
shares: f64,
pb: f64,
pe: f64,
eps: f64,
dividend: f64,
dividend_yield: f64,
dividend_per_share: f64,
dividend_date: SharedString,
dividend_payment: f64,
}
impl Quote {
pub fn random() -> Self {
use rand::Rng;
let mut rng = rand::rng();
// simulate a base price in a realistic range
let prev_close = rng.random_range(100.0..200.0);
let change = rng.random_range(-5.0..5.0);
let last_done = prev_close + change;
let open = prev_close + rng.random_range(-3.0..3.0);
let high = (prev_close + rng.random_range::<f64, _>(0.0..10.0)).max(open);
let low = (prev_close - rng.random_range::<f64, _>(0.0..10.0)).min(open);
let timestamp = Duration::from_secs(rng.random_range(0..86400));
let volume = rng.random_range(1_000_000..100_000_000);
let turnover = last_done * volume as f64;
let symbol = {
let mut ticker = String::new();
if rng.random_bool(0.5) {
ticker.push_str(&format!(
"{:03}.{}",
rng.random_range(100..1000),
rng.random_range(0..10)
));
} else {
ticker.push_str(&format!(
"{}{}",
rng.random_range('A'..='Z'),
rng.random_range('A'..='Z')
));
}
ticker.push_str(&format!(".{}", rng.random_range('A'..='Z')));
ticker
};
let name = format!(
"{} {} - #{}",
symbol,
rng.random_range(1..100),
rng.random_range(10000..100000)
);
let ttm = rng.random_range(0.0..10.0);
let market_cap = rng.random_range(1_000_000.0..10_000_000.0);
let float_cap = market_cap + rng.random_range(1_000.0..10_000.0);
let shares = rng.random_range(100.0..1000.0);
let pb = market_cap / shares;
let pe = market_cap / shares;
let eps = market_cap / shares;
let dividend = rng.random_range(0.0..10.0);
let dividend_yield = rng.random_range(0.0..10.0);
let dividend_per_share = rng.random_range(0.0..10.0);
let dividend_date = SharedString::new(format!(
"{}-{}-{}",
rng.random_range(2000..2023),
rng.random_range(1..12),
rng.random_range(1..28)
));
let dividend_payment = rng.random_range(0.0..10.0);
Self {
name: name.into(),
symbol: symbol.into(),
last_done,
prev_close,
open,
high,
low,
timestamp,
volume,
turnover,
pb,
pe,
eps,
ttm,
market_cap,
float_cap,
shares,
dividend,
dividend_yield,
dividend_per_share,
dividend_date,
dividend_payment,
}
}
fn change(&self) -> f64 {
(self.last_done - self.prev_close) / self.prev_close * 100.0
}
fn change_color(&self) -> gpui::Hsla {
if self.change() > 0.0 {
gpui::green()
} else {
gpui::red()
}
}
fn turnover_ratio(&self) -> f64 {
self.volume as f64 / self.turnover * 100.0
}
}
#[derive(IntoElement)]
struct TableRow {
ix: usize,
quote: Rc<Quote>,
}
impl TableRow {
fn new(ix: usize, quote: Rc<Quote>) -> Self {
Self { ix, quote }
}
fn render_cell(&self, key: &str, width: Pixels, color: gpui::Hsla) -> impl IntoElement {
div()
.whitespace_nowrap()
.truncate()
.w(width)
.px_1()
.child(match key {
"id" => div().child(format!("{}", self.ix)),
"symbol" => div().child(self.quote.symbol.clone()),
"name" => div().child(self.quote.name.clone()),
"last_done" => div()
.text_color(color)
.child(format!("{:.3}", self.quote.last_done)),
"prev_close" => div()
.text_color(color)
.child(format!("{:.3}", self.quote.prev_close)),
"change" => div()
.text_color(color)
.child(format!("{:.2}%", self.quote.change())),
"timestamp" => div()
.text_color(color)
.child(format!("{:?}", self.quote.timestamp.as_secs())),
"open" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.open)),
"low" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.low)),
"high" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.high)),
"ttm" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.ttm)),
"eps" => div()
.text_color(color)
.child(format!("{:.2}", self.quote.eps)),
"market_cap" => {
div().child(format!("{:.2} M", self.quote.market_cap / 1_000_000.0))
}
"float_cap" => div().child(format!("{:.2} M", self.quote.float_cap / 1_000_000.0)),
"turnover" => div().child(format!("{:.2} M", self.quote.turnover / 1_000_000.0)),
"volume" => div().child(format!("{:.2} M", self.quote.volume as f64 / 1_000_000.0)),
"turnover_ratio" => div().child(format!("{:.2}%", self.quote.turnover_ratio())),
"pe" => div().child(format!("{:.2}", self.quote.pe)),
"pb" => div().child(format!("{:.2}", self.quote.pb)),
"shares" => div().child(format!("{:.2}", self.quote.shares)),
"dividend" => div().child(format!("{:.2}", self.quote.dividend)),
"yield" => div().child(format!("{:.2}%", self.quote.dividend_yield)),
"dividend_per_share" => {
div().child(format!("{:.2}", self.quote.dividend_per_share))
}
"dividend_date" => div().child(format!("{}", self.quote.dividend_date)),
"dividend_payment" => div().child(format!("{:.2}", self.quote.dividend_payment)),
_ => div().child("--"),
})
}
}
const FIELDS: [(&str, f32); 24] = [
("id", 64.),
("symbol", 64.),
("name", 180.),
("last_done", 80.),
("prev_close", 80.),
("open", 80.),
("low", 80.),
("high", 80.),
("ttm", 50.),
("market_cap", 96.),
("float_cap", 96.),
("turnover", 120.),
("volume", 100.),
("turnover_ratio", 96.),
("pe", 64.),
("pb", 64.),
("eps", 64.),
("shares", 96.),
("dividend", 64.),
("yield", 64.),
("dividend_per_share", 64.),
("dividend_date", 96.),
("dividend_payment", 64.),
("timestamp", 120.),
];
impl RenderOnce for TableRow {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let color = self.quote.change_color();
div()
.flex()
.flex_row()
.border_b_1()
.border_color(rgb(0xE0E0E0))
.bg(if self.ix.is_multiple_of(2) {
rgb(0xFFFFFF)
} else {
rgb(0xFAFAFA)
})
.py_0p5()
.px_2()
.children(FIELDS.map(|(key, width)| self.render_cell(key, px(width), color)))
}
}
struct DataTable {
/// Use `Rc` to share the same quote data across multiple items, avoid cloning.
quotes: Vec<Rc<Quote>>,
visible_range: Range<usize>,
scroll_handle: UniformListScrollHandle,
/// The position in thumb bounds when dragging start mouse down.
drag_position: Option<Point<Pixels>>,
}
impl DataTable {
fn new() -> Self {
Self {
quotes: Vec::new(),
visible_range: 0..0,
scroll_handle: UniformListScrollHandle::new(),
drag_position: None,
}
}
fn generate(&mut self) {
self.quotes = (0..TOTAL_ITEMS).map(|_| Rc::new(Quote::random())).collect();
}
fn table_bounds(&self) -> Bounds<Pixels> {
self.scroll_handle.0.borrow().base_handle.bounds()
}
fn scroll_top(&self) -> Pixels {
self.scroll_handle.0.borrow().base_handle.offset().y
}
fn scroll_height(&self) -> Pixels {
self.scroll_handle
.0
.borrow()
.last_item_size
.unwrap_or_default()
.contents
.height
}
fn render_scrollbar(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let scroll_height = self.scroll_height();
let table_bounds = self.table_bounds();
let table_height = table_bounds.size.height;
if table_height == px(0.) {
return div().id("scrollbar");
}
let percentage = -self.scroll_top() / scroll_height;
let offset_top = (table_height * percentage).clamp(
px(4.),
(table_height - SCROLLBAR_THUMB_HEIGHT - px(4.)).max(px(4.)),
);
let entity = cx.entity();
let scroll_handle = self.scroll_handle.0.borrow().base_handle.clone();
div()
.id("scrollbar")
.absolute()
.top(offset_top)
.right_1()
.h(SCROLLBAR_THUMB_HEIGHT)
.w(SCROLLBAR_THUMB_WIDTH)
.bg(rgb(0xC0C0C0))
.hover(|this| this.bg(rgb(0xA0A0A0)))
.rounded_lg()
.child(
canvas(
|_, _, _| (),
move |thumb_bounds, _, window, _| {
window.on_mouse_event({
let entity = entity.clone();
move |ev: &MouseDownEvent, _, _, cx| {
if !thumb_bounds.contains(&ev.position) {
return;
}
entity.update(cx, |this, _| {
this.drag_position = Some(
ev.position - thumb_bounds.origin - table_bounds.origin,
);
})
}
});
window.on_mouse_event({
let entity = entity.clone();
move |_: &MouseUpEvent, _, _, cx| {
entity.update(cx, |this, _| {
this.drag_position = None;
})
}
});
window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| {
if !ev.dragging() {
return;
}
let Some(drag_pos) = entity.read(cx).drag_position else {
return;
};
let inside_offset = drag_pos.y;
let percentage = ((ev.position.y - table_bounds.origin.y
+ inside_offset)
/ (table_bounds.size.height))
.clamp(0., 1.);
let offset_y = ((scroll_height - table_bounds.size.height)
* percentage)
.clamp(px(0.), scroll_height - SCROLLBAR_THUMB_HEIGHT);
scroll_handle.set_offset(point(px(0.), -offset_y));
cx.notify(entity.entity_id());
})
},
)
.size_full(),
)
}
}
impl Render for DataTable {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.bg(gpui::white())
.text_sm()
.size_full()
.p_4()
.gap_2()
.flex()
.flex_col()
.child(format!(
"Total {} items, visible range: {:?}",
self.quotes.len(),
self.visible_range
))
.child(
div()
.flex()
.flex_col()
.flex_1()
.overflow_hidden()
.border_1()
.border_color(rgb(0xE0E0E0))
.rounded_sm()
.child(
div()
.flex()
.flex_row()
.w_full()
.overflow_hidden()
.border_b_1()
.border_color(rgb(0xE0E0E0))
.text_color(rgb(0x555555))
.bg(rgb(0xF0F0F0))
.py_1()
.px_2()
.text_xs()
.children(FIELDS.map(|(key, width)| {
div()
.whitespace_nowrap()
.flex_shrink_0()
.truncate()
.px_1()
.w(px(width))
.child(key.replace("_", " ").to_uppercase())
})),
)
.child(
div()
.relative()
.size_full()
.child(
uniform_list(
"items",
self.quotes.len(),
cx.processor(move |this, range: Range<usize>, _, _| {
this.visible_range = range.clone();
let mut items = Vec::with_capacity(range.end - range.start);
for i in range {
if let Some(quote) = this.quotes.get(i) {
items.push(TableRow::new(i, quote.clone()));
}
}
items
}),
)
.size_full()
.track_scroll(self.scroll_handle.clone()),
)
.child(self.render_scrollbar(window, cx)),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.open_window(
WindowOptions {
focus: true,
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(1280.0), px(1000.0)),
cx,
))),
..Default::default()
},
|_, cx| {
cx.new(|_| {
let mut table = DataTable::new();
table.generate();
table
})
},
)
.unwrap();
cx.activate(true);
});
}
+137
View File
@@ -0,0 +1,137 @@
use gpui::{
App, Application, Bounds, Context, Half, Hsla, Pixels, Point, Window, WindowBounds,
WindowOptions, div, prelude::*, px, rgb, size,
};
#[derive(Clone, Copy)]
struct DragInfo {
ix: usize,
color: Hsla,
position: Point<Pixels>,
}
impl DragInfo {
fn new(ix: usize, color: Hsla) -> Self {
Self {
ix,
color,
position: Point::default(),
}
}
fn position(mut self, pos: Point<Pixels>) -> Self {
self.position = pos;
self
}
}
impl Render for DragInfo {
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
let size = gpui::size(px(120.), px(50.));
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.5))
.text_color(gpui::white())
.text_xs()
.shadow_md()
.child(format!("Item {}", self.ix)),
)
}
}
struct DragDrop {
drop_on: Option<DragInfo>,
}
impl DragDrop {
fn new() -> Self {
Self { drop_on: None }
}
}
impl Render for DragDrop {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let items = [gpui::blue(), gpui::red(), gpui::green()];
div()
.size_full()
.flex()
.flex_col()
.gap_5()
.bg(gpui::white())
.justify_center()
.items_center()
.text_color(rgb(0x333333))
.child(div().text_xl().text_center().child("Drop & Drop"))
.child(
div()
.w_full()
.mb_10()
.justify_center()
.flex()
.flex_row()
.gap_4()
.items_center()
.children(items.into_iter().enumerate().map(|(ix, color)| {
let drag_info = DragInfo::new(ix, color);
div()
.id(("item", ix))
.size_32()
.flex()
.justify_center()
.items_center()
.border_2()
.border_color(color)
.text_color(color)
.cursor_move()
.hover(|this| this.bg(color.opacity(0.2)))
.child(format!("Item ({})", ix))
.on_drag(drag_info, |info: &DragInfo, position, _, cx| {
cx.new(|_| info.position(position))
})
})),
)
.child(
div()
.id("drop-target")
.w_128()
.h_32()
.flex()
.justify_center()
.items_center()
.border_3()
.border_color(self.drop_on.map(|info| info.color).unwrap_or(gpui::black()))
.when_some(self.drop_on, |this, info| this.bg(info.color.opacity(0.5)))
.on_drop(cx.listener(|this, info: &DragInfo, _, _| {
this.drop_on = Some(*info);
}))
.child("Drop items here"),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| DragDrop::new()),
)
.unwrap();
cx.activate(true);
});
}
+41
View File
@@ -0,0 +1,41 @@
use gpui::{App, Application, Context, Render, Window, WindowOptions, div, img, prelude::*};
use std::path::PathBuf;
struct GifViewer {
gif_path: PathBuf,
}
impl GifViewer {
fn new(gif_path: PathBuf) -> Self {
Self { gif_path }
}
}
impl Render for GifViewer {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(
img(self.gif_path.clone())
.size_full()
.object_fit(gpui::ObjectFit::Contain)
.id("gif"),
)
}
}
fn main() {
env_logger::init();
Application::new().run(|cx: &mut App| {
let gif_path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif");
cx.open_window(
WindowOptions {
focus: true,
..Default::default()
},
|_, cx| cx.new(|_| GifViewer::new(gif_path)),
)
.unwrap();
cx.activate(true);
});
}
+257
View File
@@ -0,0 +1,257 @@
use gpui::{
App, Application, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas,
div, linear_color_stop, linear_gradient, point, prelude::*, px, size,
};
struct GradientViewer {
color_space: ColorSpace,
}
impl GradientViewer {
fn new() -> Self {
Self {
color_space: ColorSpace::default(),
}
}
}
impl Render for GradientViewer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let color_space = self.color_space;
div()
.bg(gpui::white())
.size_full()
.p_4()
.flex()
.flex_col()
.gap_3()
.child(
div()
.flex()
.gap_2()
.justify_between()
.items_center()
.child("Gradient Examples")
.child(
div().flex().gap_2().items_center().child(
div()
.id("method")
.flex()
.px_3()
.py_1()
.text_sm()
.bg(gpui::black())
.text_color(gpui::white())
.child(format!("{}", color_space))
.active(|this| this.opacity(0.8))
.on_click(cx.listener(move |this, _, _, cx| {
this.color_space = match this.color_space {
ColorSpace::Oklab => ColorSpace::Srgb,
ColorSpace::Srgb => ColorSpace::Oklab,
};
cx.notify();
})),
),
),
)
.child(
div()
.flex()
.flex_1()
.gap_3()
.child(
div()
.size_full()
.rounded_xl()
.flex()
.items_center()
.justify_center()
.bg(gpui::red())
.text_color(gpui::white())
.child("Solid Color"),
)
.child(
div()
.size_full()
.rounded_xl()
.flex()
.items_center()
.justify_center()
.bg(gpui::blue())
.text_color(gpui::white())
.child("Solid Color"),
),
)
.child(
div()
.flex()
.flex_1()
.gap_3()
.h_24()
.text_color(gpui::white())
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
45.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::blue(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
135.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::green(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
225.,
linear_color_stop(gpui::green(), 0.),
linear_color_stop(gpui::blue(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
315.,
linear_color_stop(gpui::green(), 0.),
linear_color_stop(gpui::yellow(), 1.),
)
.color_space(color_space)),
),
)
.child(
div()
.flex()
.flex_1()
.gap_3()
.h_24()
.text_color(gpui::white())
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
0.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::white(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
90.,
linear_color_stop(gpui::blue(), 0.),
linear_color_stop(gpui::white(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
180.,
linear_color_stop(gpui::green(), 0.),
linear_color_stop(gpui::white(), 1.),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
360.,
linear_color_stop(gpui::yellow(), 0.),
linear_color_stop(gpui::white(), 1.),
)
.color_space(color_space)),
),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
0.,
linear_color_stop(gpui::green(), 0.05),
linear_color_stop(gpui::yellow(), 0.95),
)
.color_space(color_space)),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
90.,
linear_color_stop(gpui::blue(), 0.05),
linear_color_stop(gpui::red(), 0.95),
)
.color_space(color_space)),
)
.child(
div()
.flex()
.flex_1()
.gap_3()
.child(
div().flex().flex_1().gap_3().child(
div().flex_1().rounded_xl().bg(linear_gradient(
90.,
linear_color_stop(gpui::blue(), 0.5),
linear_color_stop(gpui::red(), 0.5),
)
.color_space(color_space)),
),
)
.child(
div().flex_1().rounded_xl().bg(linear_gradient(
180.,
linear_color_stop(gpui::green(), 0.),
linear_color_stop(gpui::blue(), 0.5),
)
.color_space(color_space)),
),
)
.child(div().h_24().child(canvas(
move |_, _, _| {},
move |bounds, _, window, _| {
let size = size(bounds.size.width * 0.8, px(80.));
let square_bounds = Bounds {
origin: point(
bounds.size.width.half() - size.width.half(),
bounds.origin.y,
),
size,
};
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut builder = gpui::PathBuilder::fill();
builder.move_to(square_bounds.bottom_left());
builder
.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
builder.line_to(
square_bounds.top_right() + point(-horizontal_offset, vertical_offset),
);
builder.line_to(square_bounds.bottom_right());
builder.line_to(square_bounds.bottom_left());
let path = builder.build().unwrap();
window.paint_path(
path,
linear_gradient(
180.,
linear_color_stop(gpui::red(), 0.),
linear_color_stop(gpui::blue(), 1.),
)
.color_space(color_space),
);
},
)))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.open_window(
WindowOptions {
focus: true,
..Default::default()
},
|_, cx| cx.new(|_| GradientViewer::new()),
)
.unwrap();
cx.activate(true);
});
}
+80
View File
@@ -0,0 +1,80 @@
use gpui::{
App, Application, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*,
px, rgb, size,
};
// https://en.wikipedia.org/wiki/Holy_grail_(web_design)
struct HolyGrailExample {}
impl Render for HolyGrailExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let block = |color: Hsla| {
div()
.size_full()
.bg(color)
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white())
.items_center()
};
div()
.gap_1()
.grid()
.bg(rgb(0x505050))
.size(px(500.0))
.shadow_lg()
.border_1()
.size_full()
.grid_cols(5)
.grid_rows(5)
.child(
block(gpui::white())
.row_span(1)
.col_span_full()
.child("Header"),
)
.child(
block(gpui::red())
.col_span(1)
.h_56()
.child("Table of contents"),
)
.child(
block(gpui::green())
.col_span(3)
.row_span(3)
.child("Content"),
)
.child(
block(gpui::blue())
.col_span(1)
.row_span(3)
.child("AD :(")
.text_color(gpui::white()),
)
.child(
block(gpui::black())
.row_span(1)
.col_span_full()
.text_color(gpui::white())
.child("Footer"),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| HolyGrailExample {}),
)
.unwrap();
cx.activate(true);
});
}
+106
View File
@@ -0,0 +1,106 @@
use gpui::{
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
prelude::*, px, rgb, size,
};
struct HelloWorld {
text: SharedString,
}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.bg(rgb(0x505050))
.size(px(500.0))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(format!("Hello, {}!", &self.text))
.child(
div()
.flex()
.gap_2()
.child(
div()
.size_8()
.bg(gpui::red())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::green())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::blue())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::yellow())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::black())
.border_1()
.border_dashed()
.rounded_md()
.rounded_md()
.border_color(gpui::white()),
)
.child(
div()
.size_8()
.bg(gpui::white())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::black()),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| {
cx.new(|_| HelloWorld {
text: "World".into(),
})
},
)
.unwrap();
cx.activate(true);
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

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

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

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

After

Width:  |  Height:  |  Size: 746 B

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

After

Width:  |  Height:  |  Size: 20 KiB

+87
View File
@@ -0,0 +1,87 @@
use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use gpui::{
App, Application, AssetSource, Bounds, Context, SharedString, Window, WindowBounds,
WindowOptions, div, prelude::*, px, rgb, size, svg,
};
struct Assets {
base: PathBuf,
}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
fs::read(self.base.join(path))
.map(|data| Some(std::borrow::Cow::Owned(data)))
.map_err(|err| err.into())
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
fs::read_dir(self.base.join(path))
.map(|entries| {
entries
.filter_map(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().into_string().ok())
.map(SharedString::from)
})
.collect()
})
.map_err(|err| err.into())
}
}
struct SvgExample;
impl Render for SvgExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_row()
.size_full()
.justify_center()
.items_center()
.gap_8()
.bg(rgb(0xffffff))
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0xff0000)),
)
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0x00ff00)),
)
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0x0000ff)),
)
}
}
fn main() {
Application::new()
.with_assets(Assets {
base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"),
})
.run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| SvgExample),
)
.unwrap();
cx.activate(true);
});
}
+155
View File
@@ -0,0 +1,155 @@
use gpui::{
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
};
actions!(example, [Tab, TabPrev]);
struct Example {
focus_handle: FocusHandle,
items: Vec<FocusHandle>,
message: SharedString,
}
impl Example {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let items = vec![
cx.focus_handle().tab_index(1).tab_stop(true),
cx.focus_handle().tab_index(2).tab_stop(true),
cx.focus_handle().tab_index(3).tab_stop(true),
cx.focus_handle(),
cx.focus_handle().tab_index(2).tab_stop(true),
];
let focus_handle = cx.focus_handle();
window.focus(&focus_handle);
Self {
focus_handle,
items,
message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."),
}
}
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
self.message = SharedString::from("You have pressed `Tab`.");
}
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
window.focus_prev();
self.message = SharedString::from("You have pressed `Shift-Tab`.");
}
}
impl Render for Example {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn tab_stop_style<T: Styled>(this: T) -> T {
this.border_3().border_color(gpui::blue())
}
fn button(id: impl Into<ElementId>) -> Stateful<Div> {
div()
.id(id)
.h_10()
.flex_1()
.flex()
.justify_center()
.items_center()
.border_1()
.border_color(gpui::black())
.bg(gpui::black())
.text_color(gpui::white())
.focus(tab_stop_style)
.shadow_sm()
}
div()
.id("app")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.size_full()
.flex()
.flex_col()
.p_4()
.gap_3()
.bg(gpui::white())
.text_color(gpui::black())
.child(self.message.clone())
.children(
self.items
.clone()
.into_iter()
.enumerate()
.map(|(ix, item_handle)| {
div()
.id(("item", ix))
.track_focus(&item_handle)
.h_10()
.w_full()
.flex()
.justify_center()
.items_center()
.border_1()
.border_color(gpui::black())
.when(
item_handle.tab_stop && item_handle.is_focused(window),
tab_stop_style,
)
.map(|this| match item_handle.tab_stop {
true => this
.hover(|this| this.bg(gpui::black().opacity(0.1)))
.child(format!("tab_index: {}", item_handle.tab_index)),
false => this.opacity(0.4).child("tab_stop: false"),
})
}),
)
.child(
div()
.flex()
.flex_row()
.gap_3()
.items_center()
.child(
button("el1")
.tab_index(4)
.child("Button 1")
.on_click(cx.listener(|this, _, _, cx| {
this.message = "You have clicked Button 1.".into();
cx.notify();
})),
)
.child(
button("el2")
.tab_index(5)
.child("Button 2")
.on_click(cx.listener(|this, _, _, cx| {
this.message = "You have clicked Button 2.".into();
cx.notify();
})),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.bind_keys([
KeyBinding::new("tab", Tab, None),
KeyBinding::new("shift-tab", TabPrev, None),
]);
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| Example::new(window, cx)),
)
.unwrap();
cx.activate(true);
});
}
+333
View File
@@ -0,0 +1,333 @@
use std::{
ops::{Deref, DerefMut},
sync::Arc,
};
use gpui::{
AbsoluteLength, App, Application, Context, DefiniteLength, ElementId, Global, Hsla, Menu,
SharedString, TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds,
colors::DefaultColors, div, point, prelude::*, px, relative, rgb, size,
};
use std::iter;
#[derive(Clone, Debug)]
pub struct TextContext {
font_size: f32,
line_height: f32,
type_scale: f32,
}
impl Default for TextContext {
fn default() -> Self {
TextContext {
font_size: 16.0,
line_height: 1.3,
type_scale: 1.33,
}
}
}
impl TextContext {
pub fn get_global(cx: &App) -> &Arc<TextContext> {
&cx.global::<GlobalTextContext>().0
}
}
#[derive(Clone, Debug)]
pub struct GlobalTextContext(pub Arc<TextContext>);
impl Deref for GlobalTextContext {
type Target = Arc<TextContext>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for GlobalTextContext {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Global for GlobalTextContext {}
pub trait ActiveTextContext {
fn text_context(&self) -> &Arc<TextContext>;
}
impl ActiveTextContext for App {
fn text_context(&self) -> &Arc<TextContext> {
&self.global::<GlobalTextContext>().0
}
}
#[derive(Clone, PartialEq)]
pub struct SpecimenTheme {
pub bg: Hsla,
pub fg: Hsla,
}
impl Default for SpecimenTheme {
fn default() -> Self {
Self {
bg: gpui::white(),
fg: gpui::black(),
}
}
}
impl SpecimenTheme {
pub fn invert(&self) -> Self {
Self {
bg: self.fg,
fg: self.bg,
}
}
}
#[derive(Debug, Clone, PartialEq, IntoElement)]
struct Specimen {
id: ElementId,
scale: f32,
text_style: Option<TextStyle>,
string: SharedString,
invert: bool,
}
impl Specimen {
pub fn new(id: usize) -> Self {
let string = SharedString::new_static("The quick brown fox jumps over the lazy dog");
let id_string = format!("specimen-{}", id);
let id = ElementId::Name(id_string.into());
Self {
id,
scale: 1.0,
text_style: None,
string,
invert: false,
}
}
pub fn invert(mut self) -> Self {
self.invert = !self.invert;
self
}
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
}
impl RenderOnce for Specimen {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let rem_size = window.rem_size();
let scale = self.scale;
let global_style = cx.text_context();
let style_override = self.text_style;
let mut font_size = global_style.font_size;
let mut line_height = global_style.line_height;
if let Some(style_override) = style_override {
font_size = style_override.font_size.to_pixels(rem_size).into();
line_height = match style_override.line_height {
DefiniteLength::Absolute(absolute_len) => match absolute_len {
AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).into(),
AbsoluteLength::Pixels(absolute_len) => absolute_len.into(),
},
DefiniteLength::Fraction(value) => value,
};
}
let mut theme = SpecimenTheme::default();
if self.invert {
theme = theme.invert();
}
div()
.id(self.id)
.bg(theme.bg)
.text_color(theme.fg)
.text_size(px(font_size * scale))
.line_height(relative(line_height))
.p(px(10.0))
.child(self.string)
}
}
#[derive(Debug, Clone, PartialEq, IntoElement)]
struct CharacterGrid {
scale: f32,
invert: bool,
text_style: Option<TextStyle>,
}
impl CharacterGrid {
pub fn new() -> Self {
Self {
scale: 1.0,
invert: false,
text_style: None,
}
}
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
}
impl RenderOnce for CharacterGrid {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let mut theme = SpecimenTheme::default();
if self.invert {
theme = theme.invert();
}
let characters = vec![
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
"Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q",
"r", "s", "t", "u", "v", "w", "x", "y", "z", "ẞ", "ſ", "ß", "ð", "Þ", "þ", "α", "β",
"Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ",
"χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
"У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
"_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "", "µ",
"❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎",
];
let columns = 11;
let rows = characters.len().div_ceil(columns);
let grid_rows = (0..rows).map(|row_idx| {
let start_idx = row_idx * columns;
let end_idx = (start_idx + columns).min(characters.len());
div()
.w_full()
.flex()
.flex_row()
.children((start_idx..end_idx).map(|i| {
div()
.text_center()
.size(px(62.))
.bg(theme.bg)
.text_color(theme.fg)
.text_size(px(24.0))
.line_height(relative(1.0))
.child(characters[i])
}))
.when(end_idx - start_idx < columns, |d| {
d.children(
iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)),
)
})
});
div().p_4().gap_2().flex().flex_col().children(grid_rows)
}
}
struct TextExample {
next_id: usize,
}
impl TextExample {
fn next_id(&mut self) -> usize {
self.next_id += 1;
self.next_id
}
}
impl Render for TextExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let tcx = cx.text_context();
let colors = cx.default_colors().clone();
let type_scale = tcx.type_scale;
let step_down_2 = 1.0 / (type_scale * type_scale);
let step_down_1 = 1.0 / type_scale;
let base = 1.0;
let step_up_1 = base * type_scale;
let step_up_2 = step_up_1 * type_scale;
let step_up_3 = step_up_2 * type_scale;
let step_up_4 = step_up_3 * type_scale;
let step_up_5 = step_up_4 * type_scale;
let step_up_6 = step_up_5 * type_scale;
div()
.size_full()
.child(
div()
.id("text-example")
.overflow_y_scroll()
.overflow_x_hidden()
.bg(rgb(0xffffff))
.size_full()
.child(div().child(CharacterGrid::new().scale(base)))
.child(
div()
.child(Specimen::new(self.next_id()).scale(step_down_2))
.child(Specimen::new(self.next_id()).scale(step_down_2).invert())
.child(Specimen::new(self.next_id()).scale(step_down_1))
.child(Specimen::new(self.next_id()).scale(step_down_1).invert())
.child(Specimen::new(self.next_id()).scale(base))
.child(Specimen::new(self.next_id()).scale(base).invert())
.child(Specimen::new(self.next_id()).scale(step_up_1))
.child(Specimen::new(self.next_id()).scale(step_up_1).invert())
.child(Specimen::new(self.next_id()).scale(step_up_2))
.child(Specimen::new(self.next_id()).scale(step_up_2).invert())
.child(Specimen::new(self.next_id()).scale(step_up_3))
.child(Specimen::new(self.next_id()).scale(step_up_3).invert())
.child(Specimen::new(self.next_id()).scale(step_up_4))
.child(Specimen::new(self.next_id()).scale(step_up_4).invert())
.child(Specimen::new(self.next_id()).scale(step_up_5))
.child(Specimen::new(self.next_id()).scale(step_up_5).invert())
.child(Specimen::new(self.next_id()).scale(step_up_6))
.child(Specimen::new(self.next_id()).scale(step_up_6).invert()),
),
)
.child(div().w(px(240.)).h_full().bg(colors.container))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.set_menus(vec![Menu {
name: "GPUI Typography".into(),
items: vec![],
}]);
cx.init_colors();
cx.set_global(GlobalTextContext(Arc::new(TextContext::default())));
let window = cx
.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some("GPUI Typography".into()),
..Default::default()
}),
window_bounds: Some(WindowBounds::Windowed(bounds(
point(px(0.0), px(0.0)),
size(px(920.), px(720.)),
))),
..Default::default()
},
|_window, cx| cx.new(|_cx| TextExample { next_id: 0 }),
)
.unwrap();
window
.update(cx, |_view, _window, cx| {
cx.activate(true);
})
.unwrap();
});
}
+96
View File
@@ -0,0 +1,96 @@
use gpui::{
App, Application, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
};
struct HelloWorld {}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.bg(gpui::white())
.flex()
.flex_col()
.gap_2()
.p_4()
.size_full()
.child(div().child("Text left"))
.child(div().text_center().child("Text center"))
.child(div().text_right().child("Text right"))
.child(div().text_decoration_1().child("Text left (underline)"))
.child(
div()
.text_center()
.text_decoration_1()
.child("Text center (underline)"),
)
.child(
div()
.text_right()
.text_decoration_1()
.child("Text right (underline)"),
)
.child(div().line_through().child("Text left (line_through)"))
.child(
div()
.text_center()
.line_through()
.child("Text center (line_through)"),
)
.child(
div()
.text_right()
.line_through()
.child("Text right (line_through)"),
)
.child(
div()
.flex()
.gap_2()
.justify_between()
.child(
div()
.w(px(400.))
.border_1()
.border_color(gpui::blue())
.p_1()
.whitespace_nowrap()
.overflow_hidden()
.text_center()
.child("A long non-wrapping text align center"),
)
.child(
div()
.w_32()
.border_1()
.border_color(gpui::blue())
.p_1()
.whitespace_nowrap()
.overflow_hidden()
.text_right()
.child("100%"),
),
)
.child(div().flex().gap_2().justify_between().child(
StyledText::new("ABCD").with_highlights([
(0..1, FontWeight::EXTRA_BOLD.into()),
(2..3, FontStyle::Italic.into()),
]),
))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| HelloWorld {}),
)
.unwrap();
cx.activate(true);
});
}
+123
View File
@@ -0,0 +1,123 @@
use gpui::{
App, Application, Bounds, Context, TextOverflow, Window, WindowBounds, WindowOptions, div,
prelude::*, px, size,
};
struct HelloWorld {}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let text = "The longest word 你好世界这段是中文,こんにちはこの段落は日本語です in any of the major \
English language dictionaries is pneumonoultramicroscopicsilicovolcanoconiosis, a word that \
refers to a lung disease contracted from the inhalation of very fine silica particles, \
a url https://github.com/zed-industries/zed/pull/35724?query=foo&bar=2, \
specifically from a volcano; medically, it is the same as silicosis.";
div()
.id("page")
.size_full()
.flex()
.flex_col()
.p_2()
.gap_2()
.bg(gpui::white())
.child(
div()
.flex()
.flex_row()
.flex_shrink_0()
.gap_2()
.child(
div()
.flex()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.child("longer text in flex 1"),
)
.child(
div()
.flex()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.child("short flex"),
)
.child(
div()
.overflow_hidden()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.w_full()
.child("A short text in normal div"),
),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.truncate()
.border_1()
.border_color(gpui::blue())
.child("ELLIPSIS: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.text_ellipsis()
.line_clamp(2)
.border_1()
.border_color(gpui::blue())
.child("ELLIPSIS 2 lines: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.text_overflow(TextOverflow::Truncate("".into()))
.border_1()
.border_color(gpui::green())
.child("TRUNCATE: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.text_overflow(TextOverflow::Truncate("".into()))
.line_clamp(3)
.border_1()
.border_color(gpui::green())
.child("TRUNCATE 3 lines: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.whitespace_nowrap()
.overflow_hidden()
.border_1()
.border_color(gpui::black())
.child("NOWRAP: ".to_owned() + text),
)
.child(div().text_xl().w_full().child(text))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| HelloWorld {}),
)
.unwrap();
cx.activate(true);
});
}
+46
View File
@@ -0,0 +1,46 @@
//! Renders a div with deep children hierarchy. This example is useful to exemplify that Zed can
//! handle deep hierarchies (even though it cannot just yet!).
use std::sync::LazyLock;
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
};
struct Tree {}
static DEPTH: LazyLock<u64> = LazyLock::new(|| {
std::env::var("GPUI_TREE_DEPTH")
.ok()
.and_then(|depth| depth.parse().ok())
.unwrap_or_else(|| 50)
});
impl Render for Tree {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let mut depth = *DEPTH;
static COLORS: [gpui::Hsla; 4] = [gpui::red(), gpui::blue(), gpui::green(), gpui::yellow()];
let mut colors = COLORS.iter().cycle().copied();
let mut next_div = || div().p_0p5().bg(colors.next().unwrap());
let mut innermost_node = next_div();
while depth > 0 {
innermost_node = next_div().child(innermost_node);
depth -= 1;
}
innermost_node
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Tree {}),
)
.unwrap();
});
}
+50
View File
@@ -0,0 +1,50 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size, uniform_list,
};
struct UniformListExample {}
impl Render for UniformListExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().bg(rgb(0xffffff)).child(
uniform_list(
"entries",
50,
cx.processor(|_this, range, _window, _cx| {
let mut items = Vec::new();
for ix in range {
let item = ix + 1;
items.push(
div()
.id(ix)
.px_2()
.cursor_pointer()
.on_click(move |_event, _window, _cx| {
println!("clicked Item {item:?}");
})
.child(format!("Item {item}")),
);
}
items
}),
)
.h_full(),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| UniformListExample {}),
)
.unwrap();
});
}
+269
View File
@@ -0,0 +1,269 @@
use gpui::{
App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, SharedString, Timer,
Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
};
struct SubWindow {
custom_titlebar: bool,
}
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
div()
.id(SharedString::from(text.to_string()))
.flex_none()
.px_2()
.bg(rgb(0xf7f7f7))
.active(|this| this.opacity(0.85))
.border_1()
.border_color(rgb(0xe0e0e0))
.rounded_sm()
.cursor_pointer()
.child(text.to_string())
.on_click(move |_, window, cx| on_click(window, cx))
}
impl Render for SubWindow {
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.bg(rgb(0xffffff))
.size_full()
.gap_2()
.when(self.custom_titlebar, |cx| {
cx.child(
div()
.flex()
.h(px(32.))
.px_4()
.bg(gpui::blue())
.text_color(gpui::white())
.w_full()
.child(
div()
.flex()
.items_center()
.justify_center()
.size_full()
.child("Custom Titlebar"),
),
)
})
.child(
div()
.p_8()
.gap_2()
.child("SubWindow")
.child(button("Close", |window, _| {
window.remove_window();
})),
)
}
}
struct WindowDemo {}
impl Render for WindowDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_bounds =
WindowBounds::Windowed(Bounds::centered(None, size(px(300.0), px(300.0)), cx));
div()
.p_4()
.flex()
.flex_wrap()
.bg(rgb(0xffffff))
.size_full()
.justify_center()
.content_center()
.gap_2()
.child(button("Normal", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Popup", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
kind: WindowKind::PopUp,
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Custom Titlebar", move |_, cx| {
cx.open_window(
WindowOptions {
titlebar: None,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: true,
})
},
)
.unwrap();
}))
.child(button("Invisible", move |_, cx| {
cx.open_window(
WindowOptions {
show: false,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Unmovable", move |_, cx| {
cx.open_window(
WindowOptions {
is_movable: false,
titlebar: None,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Unresizable", move |_, cx| {
cx.open_window(
WindowOptions {
is_resizable: false,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Unminimizable", move |_, cx| {
cx.open_window(
WindowOptions {
is_minimizable: false,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Hide Application", |window, cx| {
cx.hide();
// Restore the application after 3 seconds
window
.spawn(cx, async move |cx| {
Timer::after(std::time::Duration::from_secs(3)).await;
cx.update(|_, cx| {
cx.activate(false);
})
})
.detach();
}))
.child(button("Resize", |window, _| {
let content_size = window.bounds().size;
window.resize(size(content_size.height, content_size.width));
}))
.child(button("Prompt", |window, cx| {
let answer = window.prompt(
PromptLevel::Info,
"Are you sure?",
None,
&["Ok", "Cancel"],
cx,
);
cx.spawn(async move |_| {
if answer.await.unwrap() == 0 {
println!("You have clicked Ok");
} else {
println!("You have clicked Cancel");
}
})
.detach();
}))
.child(button("Prompt (non-English)", |window, cx| {
let answer = window.prompt(
PromptLevel::Info,
"Are you sure?",
None,
&[PromptButton::ok("确定"), PromptButton::cancel("取消")],
cx,
);
cx.spawn(async move |_| {
if answer.await.unwrap() == 0 {
println!("You have clicked Ok");
} else {
println!("You have clicked Cancel");
}
})
.detach();
}))
}
}
actions!(window, [Quit]);
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| {
cx.new(|cx| {
cx.observe_window_bounds(window, move |_, window, _| {
println!("Window bounds changed: {:?}", window.bounds());
})
.detach();
WindowDemo {}
})
},
)
.unwrap();
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
});
}
+219
View File
@@ -0,0 +1,219 @@
use gpui::{
App, Application, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window,
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*,
px, rgb,
};
struct WindowContent {
text: SharedString,
bounds: Bounds<Pixels>,
bg: Hsla,
}
impl Render for WindowContent {
fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let window_bounds = window.bounds();
div()
.flex()
.flex_col()
.bg(self.bg)
.size_full()
.items_center()
.text_color(rgb(0xffffff))
.child(self.text.clone())
.child(
div()
.flex()
.flex_col()
.text_sm()
.items_center()
.size_full()
.child(format!(
"origin: {}, {} size: {}, {}",
self.bounds.origin.x,
self.bounds.origin.y,
self.bounds.size.width,
self.bounds.size.height
))
.child(format!(
"cx.bounds() origin: {}, {} size {}, {}",
window_bounds.origin.x,
window_bounds.origin.y,
window_bounds.size.width,
window_bounds.size.height
)),
)
}
}
fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> WindowOptions {
WindowOptions {
// Set the bounds of the window in screen coordinates
window_bounds: Some(WindowBounds::Windowed(bounds)),
// Specify the display_id to ensure the window is created on the correct screen
display_id: Some(display_id),
titlebar: None,
window_background: WindowBackgroundAppearance::Transparent,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
app_id: None,
window_min_size: None,
window_decorations: None,
tabbing_identifier: None,
..Default::default()
}
}
fn main() {
Application::new().run(|cx: &mut App| {
// Create several new windows, positioned in the top right corner of each screen
let size = Size {
width: px(350.),
height: px(75.),
};
let margin_offset = px(150.);
for screen in cx.displays() {
let bounds = Bounds {
origin: point(margin_offset, margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Top Left {:?}", screen.id()).into(),
bg: gpui::red(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().top_right()
- point(size.width + margin_offset, -margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Top Right {:?}", screen.id()).into(),
bg: gpui::red(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().bottom_left()
- point(-margin_offset, size.height + margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Bottom Left {:?}", screen.id()).into(),
bg: gpui::blue(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().bottom_right()
- point(size.width + margin_offset, size.height + margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Bottom Right {:?}", screen.id()).into(),
bg: gpui::blue(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(screen.bounds().center().x - size.center().x, margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Top Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(margin_offset, screen.bounds().center().y - size.center().y),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Left Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().center().x - size.center().x,
screen.bounds().center().y - size.center().y,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().size.width - size.width - margin_offset,
screen.bounds().center().y - size.center().y,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Right Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().center().x - size.center().x,
screen.bounds().size.height - size.height - margin_offset,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
cx.new(|_| WindowContent {
text: format!("Bottom Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
}
});
}
+227
View File
@@ -0,0 +1,227 @@
use gpui::{
App, Application, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton,
Pixels, Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds,
WindowDecorations, WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size,
transparent_black, white,
};
struct WindowShadow {}
// Things to do:
// 1. We need a way of calculating which edge or corner the mouse is on,
// and then dispatch on that
// 2. We need to improve the shadow rendering significantly
// 3. We need to implement the techniques in here in Zed
impl Render for WindowShadow {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let decorations = window.window_decorations();
let rounding = px(10.0);
let shadow_size = px(10.0);
let border_size = px(1.0);
let grey = rgb(0x808080);
window.set_client_inset(shadow_size);
div()
.id("window-backdrop")
.bg(transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div
.bg(gpui::transparent_black())
.child(
canvas(
|_bounds, window, _cx| {
window.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
window.window_bounds().get_bounds().size,
),
HitboxBehavior::Normal,
)
},
move |_bounds, hitbox, window, _cx| {
let mouse = window.mouse_position();
let size = window.window_bounds().get_bounds().size;
let Some(edge) = resize_edge(mouse, shadow_size, size) else {
return;
};
window.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => {
CursorStyle::ResizeUpDown
}
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(rounding)
})
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
.when(!tiling.top, |div| div.pt(shadow_size))
.when(!tiling.bottom, |div| div.pb(shadow_size))
.when(!tiling.left, |div| div.pl(shadow_size))
.when(!tiling.right, |div| div.pr(shadow_size))
.on_mouse_move(|_e, window, _cx| window.refresh())
.on_mouse_down(MouseButton::Left, move |e, window, _cx| {
let size = window.window_bounds().get_bounds().size;
let pos = e.position;
match resize_edge(pos, shadow_size, size) {
Some(edge) => window.start_window_resize(edge),
None => window.start_window_move(),
};
}),
})
.size_full()
.child(
div()
.cursor(CursorStyle::Arrow)
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.border_color(grey)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(rounding)
})
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
.when(!tiling.top, |div| div.border_t(border_size))
.when(!tiling.bottom, |div| div.border_b(border_size))
.when(!tiling.left, |div| div.border_l(border_size))
.when(!tiling.right, |div| div.border_r(border_size))
.when(!tiling.is_tiled(), |div| {
div.shadow(vec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.4,
},
blur_radius: shadow_size / 2.,
spread_radius: px(0.),
offset: point(px(0.0), px(0.0)),
}])
}),
})
.on_mouse_move(|_e, _, cx| {
cx.stop_propagation();
})
.bg(gpui::rgb(0xCCCCFF))
.size_full()
.flex()
.flex_col()
.justify_around()
.child(
div().w_full().flex().flex_row().justify_around().child(
div()
.flex()
.bg(white())
.size(px(300.0))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(
div()
.id("hello")
.w(px(200.0))
.h(px(100.0))
.bg(green())
.shadow(vec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 1.0,
},
blur_radius: px(20.0),
spread_radius: px(0.0),
offset: point(px(0.0), px(0.0)),
}])
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { .. } => div
.on_mouse_down(
MouseButton::Left,
|_e, window, _| {
window.start_window_move();
},
)
.on_click(|e, window, _| {
if e.is_right_click() {
window.show_window_menu(e.position());
}
})
.text_color(black())
.child("this is the custom titlebar"),
}),
),
),
),
)
}
}
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
let edge = if pos.y < shadow_size && pos.x < shadow_size {
ResizeEdge::TopLeft
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::TopRight
} else if pos.y < shadow_size {
ResizeEdge::Top
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
ResizeEdge::BottomLeft
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::BottomRight
} else if pos.y > size.height - shadow_size {
ResizeEdge::Bottom
} else if pos.x < shadow_size {
ResizeEdge::Left
} else if pos.x > size.width - shadow_size {
ResizeEdge::Right
} else {
return None;
};
Some(edge)
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
window_background: WindowBackgroundAppearance::Opaque,
window_decorations: Some(WindowDecorations::Client),
..Default::default()
},
|window, cx| {
cx.new(|cx| {
cx.observe_window_appearance(window, |_, window, _| {
window.refresh();
})
.detach();
WindowShadow {}
})
},
)
.unwrap();
});
}
+16
View File
@@ -0,0 +1,16 @@
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<dependency>
<dependentAssembly>
<assemblyIdentity type='win32'
name='Microsoft.Windows.Common-Controls'
version='6.0.0.0' processorArchitecture='*'
publicKeyToken='6595b64144ccf1df' />
</dependentAssembly>
</dependency>
</assembly>
+2
View File
@@ -0,0 +1,2 @@
#define RT_MANIFEST 24
1 RT_MANIFEST "resources/windows/gpui.manifest.xml"
+140
View File
@@ -0,0 +1,140 @@
//! In GPUI, every model or view in the application is actually owned by a single top-level object called the `App`. When a new entity or view is created (referred to collectively as _entities_), the application is given ownership of their state to enable their participation in a variety of app services and interaction with other entities.
//!
//! To illustrate, consider the trivial app below. We start the app by calling `run` with a callback, which is passed a reference to the `App` that owns all the state for the application. This `App` is our gateway to all application-level services, such as opening windows, presenting dialogs, etc. It also has an `insert_entity` method, which is called below to create an entity and give ownership of it to the application.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let _counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! // ...
//! });
//! ```
//!
//! The call to `new_entity` returns an _entity handle_, which carries a type parameter based on the type of object it references. By itself, this `Entity<Counter>` handle doesn't provide access to the entity's state. It's merely an inert identifier plus a compile-time type tag, and it maintains a reference counted pointer to the underlying `Counter` object that is owned by the app.
//!
//! Much like an `Rc` from the Rust standard library, this reference count is incremented when the handle is cloned and decremented when it is dropped to enable shared ownership over the underlying model, but unlike an `Rc` it only provides access to the model's state when a reference to an `App` is available. The handle doesn't truly _own_ the state, but it can be used to access the state from its true owner, the `App`. Stripping away some of the setup code for brevity:
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Context, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! // Call `update` to access the model's state.
//! counter.update(cx, |counter: &mut Counter, _cx: &mut Context<Counter>| {
//! counter.count += 1;
//! });
//! });
//! ```
//!
//! To update the counter, we call `update` on the handle, passing the context reference and a callback. The callback is yielded a mutable reference to the counter, which can be used to manipulate state.
//!
//! The callback is also provided a second `Context<Counter>` reference. This reference is similar to the `App` reference provided to the `run` callback. A `Context` is actually a wrapper around the `App`, including some additional data to indicate which particular entity it is tied to; in this case the counter.
//!
//! In addition to the application-level services provided by `App`, a `Context` provides access to entity-level services. For example, it can be used it to inform observers of this entity that its state has changed. Let's add that to our example, by calling `cx.notify()`.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! counter.update(cx, |counter, cx| {
//! counter.count += 1;
//! cx.notify(); // Notify observers
//! });
//! });
//! ```
//!
//! Next, these notifications need to be observed and reacted to. Before updating the counter, we'll construct a second counter that observes it. Whenever the first counter changes, twice its count is assigned to the second counter. Note how `observe` is called on the `Context` belonging to our second counter to arrange for it to be notified whenever the first counter notifies. The call to `observe` returns a `Subscription`, which is `detach`ed to preserve this behavior for as long as both counters exist. We could also store this subscription and drop it at a time of our choosing to cancel this behavior.
//!
//! The `observe` callback is passed a mutable reference to the observer and a _handle_ to the observed counter, whose state we access with the `read` method.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity, prelude::*};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//!
//! let second_counter = cx.new(|cx: &mut Context<Counter>| {
//! // Note we can set up the callback before the Counter is even created!
//! cx.observe(
//! &first_counter,
//! |second: &mut Counter, first: Entity<Counter>, cx| {
//! second.count = first.read(cx).count * 2;
//! },
//! )
//! .detach();
//!
//! Counter { count: 0 }
//! });
//!
//! first_counter.update(cx, |counter, cx| {
//! counter.count += 1;
//! cx.notify();
//! });
//!
//! assert_eq!(second_counter.read(cx).count, 2);
//! });
//! ```
//!
//! After updating the first counter, it can be noted that the observing counter's state is maintained according to our subscription.
//!
//! In addition to `observe` and `notify`, which indicate that an entity's state has changed, GPUI also offers `subscribe` and `emit`, which enables entities to emit typed events. To opt into this system, the emitting object must implement the `EventEmitter` trait.
//!
//! Let's introduce a new event type called `CounterChangeEvent`, then indicate that `Counter` can emit this type of event:
//!
//! ```no_run
//! use gpui::EventEmitter;
//! # struct Counter {
//! # count: usize,
//! # }
//! struct CounterChangeEvent {
//! increment: usize,
//! }
//!
//! impl EventEmitter<CounterChangeEvent> for Counter {}
//! ```
//!
//! Next, the example should be updated, replacing the observation with a subscription. Whenever the counter is incremented, a `Change` event is emitted to indicate the magnitude of the increase.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Context, Entity, EventEmitter};
//! # struct Counter {
//! # count: usize,
//! # }
//! # struct CounterChangeEvent {
//! # increment: usize,
//! # }
//! # impl EventEmitter<CounterChangeEvent> for Counter {}
//! Application::new().run(|cx: &mut App| {
//! let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//!
//! let second_counter = cx.new(|cx: &mut Context<Counter>| {
//! // Note we can set up the callback before the Counter is even created!
//! cx.subscribe(&first_counter, |second: &mut Counter, _first: Entity<Counter>, event, _cx| {
//! second.count += event.increment * 2;
//! })
//! .detach();
//!
//! Counter {
//! count: first_counter.read(cx).count * 2,
//! }
//! });
//!
//! first_counter.update(cx, |first, cx| {
//! first.count += 2;
//! cx.emit(CounterChangeEvent { increment: 2 });
//! cx.notify();
//! });
//!
//! assert_eq!(second_counter.read(cx).count, 4);
//! });
//! ```
+440
View File
@@ -0,0 +1,440 @@
use anyhow::{Context as _, Result};
use collections::HashMap;
pub use gpui_macros::Action;
pub use no_action::{NoAction, is_no_action};
use serde_json::json;
use std::{
any::{Any, TypeId},
fmt::Display,
};
/// Defines and registers unit structs that can be used as actions. For more complex data types, derive `Action`.
///
/// For example:
///
/// ```
/// use gpui::actions;
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
/// ```
///
/// This will create actions with names like `editor::MoveUp`, `editor::MoveDown`, etc.
///
/// The namespace argument `editor` can also be omitted, though it is required for Zed actions.
#[macro_export]
macro_rules! actions {
($namespace:path, [ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => {
$(
#[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, gpui::Action)]
#[action(namespace = $namespace)]
$(#[$attr])*
pub struct $name;
)*
};
([ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => {
$(
#[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, gpui::Action)]
$(#[$attr])*
pub struct $name;
)*
};
}
/// Actions are used to implement keyboard-driven UI. When you declare an action, you can bind keys
/// to the action in the keymap and listeners for that action in the element tree.
///
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit
/// struct action for each listed action name in the given namespace.
///
/// ```
/// use gpui::actions;
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
/// ```
///
/// Registering the actions with the same name will result in a panic during `App` creation.
///
/// # Derive Macro
///
/// More complex data types can also be actions, by using the derive macro for `Action`:
///
/// ```
/// use gpui::Action;
/// #[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, Action)]
/// #[action(namespace = editor)]
/// pub struct SelectNext {
/// pub replace_newest: bool,
/// }
/// ```
///
/// The derive macro for `Action` requires that the type implement `Clone` and `PartialEq`. It also
/// requires `serde::Deserialize` and `schemars::JsonSchema` unless `#[action(no_json)]` is
/// specified. In Zed these trait impls are used to load keymaps from JSON.
///
/// Multiple arguments separated by commas may be specified in `#[action(...)]`:
///
/// - `namespace = some_namespace` sets the namespace. In Zed this is required.
///
/// - `name = "ActionName"` overrides the action's name. This must not contain `::`.
///
/// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`,
/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`.
///
/// - `no_register` skips registering the action. This is useful for implementing the `Action` trait
/// while not supporting invocation by name or JSON deserialization.
///
/// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action.
/// These action names should *not* correspond to any actions that are registered. These old names
/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will
/// accept these old names and provide warnings.
///
/// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message.
/// In Zed, the keymap JSON schema will cause this to be displayed as a warning.
///
/// # Manual Implementation
///
/// If you want to control the behavior of the action trait manually, you can use the lower-level
/// `#[register_action]` macro, which only generates the code needed to register your action before
/// `main`.
///
/// ```
/// use gpui::{SharedString, register_action};
/// #[derive(Clone, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)]
/// pub struct Paste {
/// pub content: SharedString,
/// }
///
/// impl gpui::Action for Paste {
/// # fn boxed_clone(&self) -> Box<dyn gpui::Action> { unimplemented!()}
/// # fn partial_eq(&self, other: &dyn gpui::Action) -> bool { unimplemented!() }
/// # fn name(&self) -> &'static str { "Paste" }
/// # fn name_for_type() -> &'static str { "Paste" }
/// # fn build(value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>> {
/// # unimplemented!()
/// # }
/// }
///
/// register_action!(Paste);
/// ```
pub trait Action: Any + Send {
/// Clone the action into a new box
fn boxed_clone(&self) -> Box<dyn Action>;
/// Do a partial equality check on this action and the other
fn partial_eq(&self, action: &dyn Action) -> bool;
/// Get the name of this action, for displaying in UI
fn name(&self) -> &'static str;
/// Get the name of this action type (static)
fn name_for_type() -> &'static str
where
Self: Sized;
/// Build this action from a JSON value. This is used to construct actions from the keymap.
/// A value of `{}` will be passed for actions that don't have any parameters.
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
where
Self: Sized;
/// Optional JSON schema for the action's input data.
fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option<schemars::Schema>
where
Self: Sized,
{
None
}
/// A list of alternate, deprecated names for this action. These names can still be used to
/// invoke the action. In Zed, the keymap JSON schema will accept these old names and provide
/// warnings.
fn deprecated_aliases() -> &'static [&'static str]
where
Self: Sized,
{
&[]
}
/// Returns the deprecation message for this action, if any. In Zed, the keymap JSON schema will
/// cause this to be displayed as a warning.
fn deprecation_message() -> Option<&'static str>
where
Self: Sized,
{
None
}
/// The documentation for this action, if any. When using the derive macro for actions
/// this will be automatically generated from the doc comments on the action struct.
fn documentation() -> Option<&'static str>
where
Self: Sized,
{
None
}
}
impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action")
.field("name", &self.name())
.finish()
}
}
impl dyn Action {
/// Type-erase Action type.
pub fn as_any(&self) -> &dyn Any {
self as &dyn Any
}
}
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
/// markdown to display it.
#[derive(Debug)]
pub enum ActionBuildError {
/// Indicates that an action with this name has not been registered.
NotFound {
/// Name of the action that was not found.
name: String,
},
/// Indicates that an error occurred while building the action, typically a JSON deserialization
/// error.
BuildError {
/// Name of the action that was attempting to be built.
name: String,
/// Error that occurred while building the action.
error: anyhow::Error,
},
}
impl std::error::Error for ActionBuildError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ActionBuildError::NotFound { .. } => None,
ActionBuildError::BuildError { error, .. } => error.source(),
}
}
}
impl Display for ActionBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ActionBuildError::NotFound { name } => {
write!(f, "Didn't find an action named \"{name}\"")
}
ActionBuildError::BuildError { name, error } => {
write!(f, "Error while building action \"{name}\": {error}")
}
}
}
}
type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
pub(crate) struct ActionRegistry {
by_name: HashMap<&'static str, ActionData>,
names_by_type_id: HashMap<TypeId, &'static str>,
all_names: Vec<&'static str>, // So we can return a static slice.
deprecated_aliases: HashMap<&'static str, &'static str>, // deprecated name -> preferred name
deprecation_messages: HashMap<&'static str, &'static str>, // action name -> deprecation message
documentation: HashMap<&'static str, &'static str>, // action name -> documentation
}
impl Default for ActionRegistry {
fn default() -> Self {
let mut this = ActionRegistry {
by_name: Default::default(),
names_by_type_id: Default::default(),
documentation: Default::default(),
all_names: Default::default(),
deprecated_aliases: Default::default(),
deprecation_messages: Default::default(),
};
this.load_actions();
this
}
}
struct ActionData {
pub build: ActionBuilder,
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
}
/// This type must be public so that our macros can build it in other crates.
/// But this is an implementation detail and should not be used directly.
#[doc(hidden)]
pub struct MacroActionBuilder(pub fn() -> MacroActionData);
/// This type must be public so that our macros can build it in other crates.
/// But this is an implementation detail and should not be used directly.
#[doc(hidden)]
pub struct MacroActionData {
pub name: &'static str,
pub type_id: TypeId,
pub build: ActionBuilder,
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
pub deprecated_aliases: &'static [&'static str],
pub deprecation_message: Option<&'static str>,
pub documentation: Option<&'static str>,
}
inventory::collect!(MacroActionBuilder);
impl ActionRegistry {
/// Load all registered actions into the registry.
pub(crate) fn load_actions(&mut self) {
for builder in inventory::iter::<MacroActionBuilder> {
let action = builder.0();
self.insert_action(action);
}
}
#[cfg(test)]
pub(crate) fn load_action<A: Action>(&mut self) {
self.insert_action(MacroActionData {
name: A::name_for_type(),
type_id: TypeId::of::<A>(),
build: A::build,
json_schema: A::action_json_schema,
deprecated_aliases: A::deprecated_aliases(),
deprecation_message: A::deprecation_message(),
documentation: A::documentation(),
});
}
fn insert_action(&mut self, action: MacroActionData) {
let name = action.name;
if self.by_name.contains_key(name) {
panic!(
"Action with name `{name}` already registered \
(might be registered in `#[action(deprecated_aliases = [...])]`."
);
}
self.by_name.insert(
name,
ActionData {
build: action.build,
json_schema: action.json_schema,
},
);
for &alias in action.deprecated_aliases {
if self.by_name.contains_key(alias) {
panic!(
"Action with name `{alias}` already registered. \
`{alias}` is specified in `#[action(deprecated_aliases = [...])]` for action `{name}`."
);
}
self.by_name.insert(
alias,
ActionData {
build: action.build,
json_schema: action.json_schema,
},
);
self.deprecated_aliases.insert(alias, name);
self.all_names.push(alias);
}
self.names_by_type_id.insert(action.type_id, name);
self.all_names.push(name);
if let Some(deprecation_msg) = action.deprecation_message {
self.deprecation_messages.insert(name, deprecation_msg);
}
if let Some(documentation) = action.documentation {
self.documentation.insert(name, documentation);
}
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
let name = self
.names_by_type_id
.get(type_id)
.with_context(|| format!("no action type registered for {type_id:?}"))?;
Ok(self.build_action(name, None)?)
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action(
&self,
name: &str,
params: Option<serde_json::Value>,
) -> std::result::Result<Box<dyn Action>, ActionBuildError> {
let build_action = self
.by_name
.get(name)
.ok_or_else(|| ActionBuildError::NotFound {
name: name.to_owned(),
})?
.build;
(build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| {
ActionBuildError::BuildError {
name: name.to_owned(),
error: e,
}
})
}
pub fn all_action_names(&self) -> &[&'static str] {
self.all_names.as_slice()
}
pub fn action_schemas(
&self,
generator: &mut schemars::SchemaGenerator,
) -> Vec<(&'static str, Option<schemars::Schema>)> {
// Use the order from all_names so that the resulting schema has sensible order.
self.all_names
.iter()
.map(|name| {
let action_data = self
.by_name
.get(name)
.expect("All actions in all_names should be registered");
(*name, (action_data.json_schema)(generator))
})
.collect::<Vec<_>>()
}
pub fn deprecated_aliases(&self) -> &HashMap<&'static str, &'static str> {
&self.deprecated_aliases
}
pub fn deprecation_messages(&self) -> &HashMap<&'static str, &'static str> {
&self.deprecation_messages
}
pub fn documentation(&self) -> &HashMap<&'static str, &'static str> {
&self.documentation
}
}
/// Generate a list of all the registered actions.
/// Useful for transforming the list of available actions into a
/// format suited for static analysis such as in validating keymaps, or
/// generating documentation.
pub fn generate_list_of_all_registered_actions() -> impl Iterator<Item = MacroActionData> {
inventory::iter::<MacroActionBuilder>
.into_iter()
.map(|builder| builder.0())
}
mod no_action {
use crate as gpui;
use std::any::Any as _;
actions!(
zed,
[
/// Action with special handling which unbinds the keybinding this is associated with,
/// if it is the highest precedence match.
NoAction
]
);
/// Returns whether or not this action represents a removed key binding.
pub fn is_no_action(action: &dyn gpui::Action) -> bool {
action.as_any().type_id() == (NoAction {}).type_id()
}
}
+2465
View File
File diff suppressed because it is too large Load Diff
+488
View File
@@ -0,0 +1,488 @@
use crate::{
AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
};
use anyhow::{Context as _, anyhow};
use derive_more::{Deref, DerefMut};
use futures::channel::oneshot;
use std::{future::Future, rc::Weak};
use super::{Context, WeakEntity};
/// An async-friendly version of [App] with a static lifetime so it can be held across `await` points in async code.
/// You're provided with an instance when calling [App::spawn], and you can also create one with [App::to_async].
/// Internally, this holds a weak reference to an `App`, so its methods are fallible to protect against cases where the [App] is dropped.
#[derive(Clone)]
pub struct AsyncApp {
pub(crate) app: Weak<AppCell>,
pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
}
impl AppContext for AsyncApp {
type Result<T> = Result<T>;
fn new<T: 'static>(
&mut self,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Self::Result<Entity<T>> {
let app = self.app.upgrade().context("app was released")?;
let mut app = app.borrow_mut();
Ok(app.new(build_entity))
}
fn reserve_entity<T: 'static>(&mut self) -> Result<Reservation<T>> {
let app = self.app.upgrade().context("app was released")?;
let mut app = app.borrow_mut();
Ok(app.reserve_entity())
}
fn insert_entity<T: 'static>(
&mut self,
reservation: Reservation<T>,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Result<Entity<T>> {
let app = self.app.upgrade().context("app was released")?;
let mut app = app.borrow_mut();
Ok(app.insert_entity(reservation, build_entity))
}
fn update_entity<T: 'static, R>(
&mut self,
handle: &Entity<T>,
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> Self::Result<R> {
let app = self.app.upgrade().context("app was released")?;
let mut app = app.borrow_mut();
Ok(app.update_entity(handle, update))
}
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
where
T: 'static,
{
Err(anyhow!(
"Cannot as_mut with an async context. Try calling update() first"
))
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,
callback: impl FnOnce(&T, &App) -> R,
) -> Self::Result<R>
where
T: 'static,
{
let app = self.app.upgrade().context("app was released")?;
let lock = app.borrow();
Ok(lock.read_entity(handle, callback))
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
{
let app = self.app.upgrade().context("app was released")?;
let mut lock = app.try_borrow_mut()?;
lock.update_window(window, f)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(Entity<T>, &App) -> R,
) -> Result<R>
where
T: 'static,
{
let app = self.app.upgrade().context("app was released")?;
let lock = app.borrow();
lock.read_window(window, read)
}
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.background_executor.spawn(future)
}
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
where
G: Global,
{
let app = self.app.upgrade().context("app was released")?;
let mut lock = app.borrow_mut();
Ok(lock.update(|this| this.read_global(callback)))
}
}
impl AsyncApp {
/// Schedules all windows in the application to be redrawn.
pub fn refresh(&self) -> Result<()> {
let app = self.app.upgrade().context("app was released")?;
let mut lock = app.borrow_mut();
lock.refresh_windows();
Ok(())
}
/// Get an executor which can be used to spawn futures in the background.
pub fn background_executor(&self) -> &BackgroundExecutor {
&self.background_executor
}
/// Get an executor which can be used to spawn futures in the foreground.
pub fn foreground_executor(&self) -> &ForegroundExecutor {
&self.foreground_executor
}
/// Invoke the given function in the context of the app, then flush any effects produced during its invocation.
pub fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> Result<R> {
let app = self.app.upgrade().context("app was released")?;
let mut lock = app.borrow_mut();
Ok(lock.update(f))
}
/// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type.
/// The callback is provided a handle to the emitting entity and a reference to the emitted event.
pub fn subscribe<T, Event>(
&mut self,
entity: &Entity<T>,
mut on_event: impl FnMut(Entity<T>, &Event, &mut App) + 'static,
) -> Result<Subscription>
where
T: 'static + EventEmitter<Event>,
Event: 'static,
{
let app = self.app.upgrade().context("app was released")?;
let mut lock = app.borrow_mut();
let subscription = lock.subscribe(entity, on_event);
Ok(subscription)
}
/// Open a window with the given options based on the root view returned by the given function.
pub fn open_window<V>(
&self,
options: crate::WindowOptions,
build_root_view: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
) -> Result<WindowHandle<V>>
where
V: 'static + Render,
{
let app = self.app.upgrade().context("app was released")?;
let mut lock = app.borrow_mut();
lock.open_window(options, build_root_view)
}
/// Schedule a future to be polled in the background.
#[track_caller]
pub fn spawn<AsyncFn, R>(&self, f: AsyncFn) -> Task<R>
where
AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static,
R: 'static,
{
let mut cx = self.clone();
self.foreground_executor
.spawn(async move { f(&mut cx).await })
}
/// Determine whether global state of the specified type has been assigned.
/// Returns an error if the `App` has been dropped.
pub fn has_global<G: Global>(&self) -> Result<bool> {
let app = self.app.upgrade().context("app was released")?;
let app = app.borrow_mut();
Ok(app.has_global::<G>())
}
/// Reads the global state of the specified type, passing it to the given callback.
///
/// Panics if no global state of the specified type has been assigned.
/// Returns an error if the `App` has been dropped.
pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> Result<R> {
let app = self.app.upgrade().context("app was released")?;
let app = app.borrow_mut();
Ok(read(app.global(), &app))
}
/// Reads the global state of the specified type, passing it to the given callback.
///
/// Similar to [`AsyncApp::read_global`], but returns an error instead of panicking
/// if no state of the specified type has been assigned.
///
/// Returns an error if no state of the specified type has been assigned the `App` has been dropped.
pub fn try_read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> Option<R> {
let app = self.app.upgrade()?;
let app = app.borrow_mut();
Some(read(app.try_global()?, &app))
}
/// Reads the global state of the specified type, passing it to the given callback.
/// A default value is assigned if a global of this type has not yet been assigned.
///
/// # Errors
/// If the app has ben dropped this returns an error.
pub fn try_read_default_global<G: Global + Default, R>(
&self,
read: impl FnOnce(&G, &App) -> R,
) -> Result<R> {
let app = self.app.upgrade().context("app was released")?;
let mut app = app.borrow_mut();
app.update(|cx| {
cx.default_global::<G>();
});
Ok(read(app.try_global().context("app was released")?, &app))
}
/// A convenience method for [`App::update_global`](BorrowAppContext::update_global)
/// for updating the global state of the specified type.
pub fn update_global<G: Global, R>(
&self,
update: impl FnOnce(&mut G, &mut App) -> R,
) -> Result<R> {
let app = self.app.upgrade().context("app was released")?;
let mut app = app.borrow_mut();
Ok(app.update(|cx| cx.update_global(update)))
}
/// Run something using this entity and cx, when the returned struct is dropped
pub fn on_drop<T: 'static, Callback: FnOnce(&mut T, &mut Context<T>) + 'static>(
&self,
entity: &WeakEntity<T>,
f: Callback,
) -> util::Deferred<impl FnOnce() + use<T, Callback>> {
let entity = entity.clone();
let mut cx = self.clone();
util::defer(move || {
entity.update(&mut cx, f).ok();
})
}
}
/// A cloneable, owned handle to the application context,
/// composed with the window associated with the current task.
#[derive(Clone, Deref, DerefMut)]
pub struct AsyncWindowContext {
#[deref]
#[deref_mut]
app: AsyncApp,
window: AnyWindowHandle,
}
impl AsyncWindowContext {
pub(crate) fn new_context(app: AsyncApp, window: AnyWindowHandle) -> Self {
Self { app, window }
}
/// Get the handle of the window this context is associated with.
pub fn window_handle(&self) -> AnyWindowHandle {
self.window
}
/// A convenience method for [`App::update_window`].
pub fn update<R>(&mut self, update: impl FnOnce(&mut Window, &mut App) -> R) -> Result<R> {
self.app
.update_window(self.window, |_, window, cx| update(window, cx))
}
/// A convenience method for [`App::update_window`].
pub fn update_root<R>(
&mut self,
update: impl FnOnce(AnyView, &mut Window, &mut App) -> R,
) -> Result<R> {
self.app.update_window(self.window, update)
}
/// A convenience method for [`Window::on_next_frame`].
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut Window, &mut App) + 'static) {
self.window
.update(self, |_, window, _| window.on_next_frame(f))
.ok();
}
/// A convenience method for [`App::global`].
pub fn read_global<G: Global, R>(
&mut self,
read: impl FnOnce(&G, &Window, &App) -> R,
) -> Result<R> {
self.window
.update(self, |_, window, cx| read(cx.global(), window, cx))
}
/// A convenience method for [`App::update_global`](BorrowAppContext::update_global).
/// for updating the global state of the specified type.
pub fn update_global<G, R>(
&mut self,
update: impl FnOnce(&mut G, &mut Window, &mut App) -> R,
) -> Result<R>
where
G: Global,
{
self.window.update(self, |_, window, cx| {
cx.update_global(|global, cx| update(global, window, cx))
})
}
/// Schedule a future to be executed on the main thread. This is used for collecting
/// the results of background tasks and updating the UI.
#[track_caller]
pub fn spawn<AsyncFn, R>(&self, f: AsyncFn) -> Task<R>
where
AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static,
R: 'static,
{
let mut cx = self.clone();
self.foreground_executor
.spawn(async move { f(&mut cx).await })
}
/// Present a platform dialog.
/// The provided message will be presented, along with buttons for each answer.
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
pub fn prompt<T>(
&mut self,
level: PromptLevel,
message: &str,
detail: Option<&str>,
answers: &[T],
) -> oneshot::Receiver<usize>
where
T: Clone + Into<PromptButton>,
{
self.window
.update(self, |_, window, cx| {
window.prompt(level, message, detail, answers, cx)
})
.unwrap_or_else(|_| oneshot::channel().1)
}
}
impl AppContext for AsyncWindowContext {
type Result<T> = Result<T>;
fn new<T>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Result<Entity<T>>
where
T: 'static,
{
self.window.update(self, |_, _, cx| cx.new(build_entity))
}
fn reserve_entity<T: 'static>(&mut self) -> Result<Reservation<T>> {
self.window.update(self, |_, _, cx| cx.reserve_entity())
}
fn insert_entity<T: 'static>(
&mut self,
reservation: Reservation<T>,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Self::Result<Entity<T>> {
self.window
.update(self, |_, _, cx| cx.insert_entity(reservation, build_entity))
}
fn update_entity<T: 'static, R>(
&mut self,
handle: &Entity<T>,
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> Result<R> {
self.window
.update(self, |_, _, cx| cx.update_entity(handle, update))
}
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
where
T: 'static,
{
Err(anyhow!(
"Cannot use as_mut() from an async context, call `update`"
))
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,
read: impl FnOnce(&T, &App) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.app.read_entity(handle, read)
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<T>
where
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
{
self.app.update_window(window, update)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(Entity<T>, &App) -> R,
) -> Result<R>
where
T: 'static,
{
self.app.read_window(window, read)
}
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.app.background_executor.spawn(future)
}
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Result<R>
where
G: Global,
{
self.app.read_global(callback)
}
}
impl VisualContext for AsyncWindowContext {
fn window_handle(&self) -> AnyWindowHandle {
self.window
}
fn new_window_entity<T: 'static>(
&mut self,
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
) -> Self::Result<Entity<T>> {
self.window
.update(self, |_, window, cx| cx.new(|cx| build_entity(window, cx)))
}
fn update_window_entity<T: 'static, R>(
&mut self,
view: &Entity<T>,
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
) -> Self::Result<R> {
self.window.update(self, |_, window, cx| {
view.update(cx, |entity, cx| update(entity, window, cx))
})
}
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
) -> Self::Result<Entity<V>>
where
V: 'static + Render,
{
self.window
.update(self, |_, window, cx| window.replace_root(cx, build_view))
}
fn focus<V>(&mut self, view: &Entity<V>) -> Self::Result<()>
where
V: Focusable,
{
self.window.update(self, |_, window, cx| {
view.read(cx).focus_handle(cx).focus(window);
})
}
}
+824
View File
@@ -0,0 +1,824 @@
use crate::{
AnyView, AnyWindowHandle, AppContext, AsyncApp, DispatchPhase, Effect, EntityId, EventEmitter,
FocusHandle, FocusOutEvent, Focusable, Global, KeystrokeObserver, Reservation, SubscriberSet,
Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle,
};
use anyhow::Result;
use futures::FutureExt;
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
future::Future,
ops,
sync::Arc,
};
use util::Deferred;
use super::{App, AsyncWindowContext, Entity, KeystrokeEvent};
/// The app context, with specialized behavior for the given entity.
pub struct Context<'a, T> {
app: &'a mut App,
entity_state: WeakEntity<T>,
}
impl<'a, T> ops::Deref for Context<'a, T> {
type Target = App;
fn deref(&self) -> &Self::Target {
self.app
}
}
impl<'a, T> ops::DerefMut for Context<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.app
}
}
impl<'a, T: 'static> Context<'a, T> {
pub(crate) fn new_context(app: &'a mut App, entity_state: WeakEntity<T>) -> Self {
Self { app, entity_state }
}
/// The entity id of the entity backing this context.
pub fn entity_id(&self) -> EntityId {
self.entity_state.entity_id
}
/// Returns a handle to the entity belonging to this context.
pub fn entity(&self) -> Entity<T> {
self.weak_entity()
.upgrade()
.expect("The entity must be alive if we have a entity context")
}
/// Returns a weak handle to the entity belonging to this context.
pub fn weak_entity(&self) -> WeakEntity<T> {
self.entity_state.clone()
}
/// Arranges for the given function to be called whenever [`Context::notify`] is
/// called with the given entity.
pub fn observe<W>(
&mut self,
entity: &Entity<W>,
mut on_notify: impl FnMut(&mut T, Entity<W>, &mut Context<T>) + 'static,
) -> Subscription
where
T: 'static,
W: 'static,
{
let this = self.weak_entity();
self.app.observe_internal(entity, move |e, cx| {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| on_notify(this, e, cx));
true
} else {
false
}
})
}
/// Observe changes to ourselves
pub fn observe_self(
&mut self,
mut on_event: impl FnMut(&mut T, &mut Context<T>) + 'static,
) -> Subscription
where
T: 'static,
{
let this = self.entity();
self.app.observe(&this, move |this, cx| {
this.update(cx, |this, cx| on_event(this, cx))
})
}
/// Subscribe to an event type from another entity
pub fn subscribe<T2, Evt>(
&mut self,
entity: &Entity<T2>,
mut on_event: impl FnMut(&mut T, Entity<T2>, &Evt, &mut Context<T>) + 'static,
) -> Subscription
where
T: 'static,
T2: 'static + EventEmitter<Evt>,
Evt: 'static,
{
let this = self.weak_entity();
self.app.subscribe_internal(entity, move |e, event, cx| {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| on_event(this, e, event, cx));
true
} else {
false
}
})
}
/// Subscribe to an event type from ourself
pub fn subscribe_self<Evt>(
&mut self,
mut on_event: impl FnMut(&mut T, &Evt, &mut Context<T>) + 'static,
) -> Subscription
where
T: 'static + EventEmitter<Evt>,
Evt: 'static,
{
let this = self.entity();
self.app.subscribe(&this, move |this, evt, cx| {
this.update(cx, |this, cx| on_event(this, evt, cx))
})
}
/// Register a callback to be invoked when GPUI releases this entity.
pub fn on_release(&self, on_release: impl FnOnce(&mut T, &mut App) + 'static) -> Subscription
where
T: 'static,
{
let (subscription, activate) = self.app.release_listeners.insert(
self.entity_state.entity_id,
Box::new(move |this, cx| {
let this = this.downcast_mut().expect("invalid entity type");
on_release(this, cx);
}),
);
activate();
subscription
}
/// Register a callback to be run on the release of another entity
pub fn observe_release<T2>(
&self,
entity: &Entity<T2>,
on_release: impl FnOnce(&mut T, &mut T2, &mut Context<T>) + 'static,
) -> Subscription
where
T: Any,
T2: 'static,
{
let entity_id = entity.entity_id();
let this = self.weak_entity();
let (subscription, activate) = self.app.release_listeners.insert(
entity_id,
Box::new(move |entity, cx| {
let entity = entity.downcast_mut().expect("invalid entity type");
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| on_release(this, entity, cx));
}
}),
);
activate();
subscription
}
/// Register a callback to for updates to the given global
pub fn observe_global<G: 'static>(
&mut self,
mut f: impl FnMut(&mut T, &mut Context<T>) + 'static,
) -> Subscription
where
T: 'static,
{
let handle = self.weak_entity();
let (subscription, activate) = self.global_observers.insert(
TypeId::of::<G>(),
Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()),
);
self.defer(move |_| activate());
subscription
}
/// Register a callback to be invoked when the application is about to restart.
pub fn on_app_restart(
&self,
mut on_restart: impl FnMut(&mut T, &mut App) + 'static,
) -> Subscription
where
T: 'static,
{
let handle = self.weak_entity();
self.app.on_app_restart(move |cx| {
handle.update(cx, |entity, cx| on_restart(entity, cx)).ok();
})
}
/// Arrange for the given function to be invoked whenever the application is quit.
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
pub fn on_app_quit<Fut>(
&self,
mut on_quit: impl FnMut(&mut T, &mut Context<T>) -> Fut + 'static,
) -> Subscription
where
Fut: 'static + Future<Output = ()>,
T: 'static,
{
let handle = self.weak_entity();
self.app.on_app_quit(move |cx| {
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
async move {
if let Some(future) = future {
future.await;
}
}
.boxed_local()
})
}
/// Tell GPUI that this entity has changed and observers of it should be notified.
pub fn notify(&mut self) {
self.app.notify(self.entity_state.entity_id);
}
/// Spawn the future returned by the given function.
/// The function is provided a weak handle to the entity owned by this context and a context that can be held across await points.
/// The returned task must be held or detached.
#[track_caller]
pub fn spawn<AsyncFn, R>(&self, f: AsyncFn) -> Task<R>
where
T: 'static,
AsyncFn: AsyncFnOnce(WeakEntity<T>, &mut AsyncApp) -> R + 'static,
R: 'static,
{
let this = self.weak_entity();
self.app.spawn(async move |cx| f(this, cx).await)
}
/// Convenience method for accessing view state in an event callback.
///
/// Many GPUI callbacks take the form of `Fn(&E, &mut Window, &mut App)`,
/// but it's often useful to be able to access view state in these
/// callbacks. This method provides a convenient way to do so.
pub fn listener<E: ?Sized>(
&self,
f: impl Fn(&mut T, &E, &mut Window, &mut Context<T>) + 'static,
) -> impl Fn(&E, &mut Window, &mut App) + 'static {
let view = self.entity().downgrade();
move |e: &E, window: &mut Window, cx: &mut App| {
view.update(cx, |view, cx| f(view, e, window, cx)).ok();
}
}
/// Convenience method for producing view state in a closure.
/// See `listener` for more details.
pub fn processor<E, R>(
&self,
f: impl Fn(&mut T, E, &mut Window, &mut Context<T>) -> R + 'static,
) -> impl Fn(E, &mut Window, &mut App) -> R + 'static {
let view = self.entity();
move |e: E, window: &mut Window, cx: &mut App| {
view.update(cx, |view, cx| f(view, e, window, cx))
}
}
/// Run something using this entity and cx, when the returned struct is dropped
pub fn on_drop(
&self,
f: impl FnOnce(&mut T, &mut Context<T>) + 'static,
) -> Deferred<impl FnOnce()> {
let this = self.weak_entity();
let mut cx = self.to_async();
util::defer(move || {
this.update(&mut cx, f).ok();
})
}
/// Focus the given view in the given window. View type is required to implement Focusable.
pub fn focus_view<W: Focusable>(&mut self, view: &Entity<W>, window: &mut Window) {
window.focus(&view.focus_handle(self));
}
/// Sets a given callback to be run on the next frame.
pub fn on_next_frame(
&self,
window: &mut Window,
f: impl FnOnce(&mut T, &mut Window, &mut Context<T>) + 'static,
) where
T: 'static,
{
let view = self.entity();
window.on_next_frame(move |window, cx| view.update(cx, |view, cx| f(view, window, cx)));
}
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
/// that are currently on the stack to be returned to the app.
pub fn defer_in(
&mut self,
window: &Window,
f: impl FnOnce(&mut T, &mut Window, &mut Context<T>) + 'static,
) {
let view = self.entity();
window.defer(self, move |window, cx| {
view.update(cx, |view, cx| f(view, window, cx))
});
}
/// Observe another entity for changes to its state, as tracked by [`Context::notify`].
pub fn observe_in<V2>(
&mut self,
observed: &Entity<V2>,
window: &mut Window,
mut on_notify: impl FnMut(&mut T, Entity<V2>, &mut Window, &mut Context<T>) + 'static,
) -> Subscription
where
V2: 'static,
T: 'static,
{
let observed_id = observed.entity_id();
let observed = observed.downgrade();
let window_handle = window.handle;
let observer = self.weak_entity();
self.new_observer(
observed_id,
Box::new(move |cx| {
window_handle
.update(cx, |_, window, cx| {
if let Some((observer, observed)) =
observer.upgrade().zip(observed.upgrade())
{
observer.update(cx, |observer, cx| {
on_notify(observer, observed, window, cx);
});
true
} else {
false
}
})
.unwrap_or(false)
}),
)
}
/// Subscribe to events emitted by another entity.
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
/// The callback will be invoked with a reference to the current view, a handle to the emitting `Entity`, the event, a mutable reference to the `Window`, and the context for the entity.
pub fn subscribe_in<Emitter, Evt>(
&mut self,
emitter: &Entity<Emitter>,
window: &Window,
mut on_event: impl FnMut(&mut T, &Entity<Emitter>, &Evt, &mut Window, &mut Context<T>) + 'static,
) -> Subscription
where
Emitter: EventEmitter<Evt>,
Evt: 'static,
{
let emitter = emitter.downgrade();
let window_handle = window.handle;
let subscriber = self.weak_entity();
self.new_subscription(
emitter.entity_id(),
(
TypeId::of::<Evt>(),
Box::new(move |event, cx| {
window_handle
.update(cx, |_, window, cx| {
if let Some((subscriber, emitter)) =
subscriber.upgrade().zip(emitter.upgrade())
{
let event = event.downcast_ref().expect("invalid event type");
subscriber.update(cx, |subscriber, cx| {
on_event(subscriber, &emitter, event, window, cx);
});
true
} else {
false
}
})
.unwrap_or(false)
}),
),
)
}
/// Register a callback to be invoked when the view is released.
///
/// The callback receives a handle to the view's window. This handle may be
/// invalid, if the window was closed before the view was released.
pub fn on_release_in(
&mut self,
window: &Window,
on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static,
) -> Subscription {
let entity = self.entity();
self.app.observe_release_in(&entity, window, on_release)
}
/// Register a callback to be invoked when the given Entity is released.
pub fn observe_release_in<T2>(
&self,
observed: &Entity<T2>,
window: &Window,
mut on_release: impl FnMut(&mut T, &mut T2, &mut Window, &mut Context<T>) + 'static,
) -> Subscription
where
T: 'static,
T2: 'static,
{
let observer = self.weak_entity();
self.app
.observe_release_in(observed, window, move |observed, window, cx| {
observer
.update(cx, |observer, cx| {
on_release(observer, observed, window, cx)
})
.ok();
})
}
/// Register a callback to be invoked when the window is resized.
pub fn observe_window_bounds(
&self,
window: &mut Window,
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let (subscription, activate) = window.bounds_observers.insert(
(),
Box::new(move |window, cx| {
view.update(cx, |view, cx| callback(view, window, cx))
.is_ok()
}),
);
activate();
subscription
}
/// Register a callback to be invoked when the window is activated or deactivated.
pub fn observe_window_activation(
&self,
window: &mut Window,
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let (subscription, activate) = window.activation_observers.insert(
(),
Box::new(move |window, cx| {
view.update(cx, |view, cx| callback(view, window, cx))
.is_ok()
}),
);
activate();
subscription
}
/// Registers a callback to be invoked when the window appearance changes.
pub fn observe_window_appearance(
&self,
window: &mut Window,
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let (subscription, activate) = window.appearance_observers.insert(
(),
Box::new(move |window, cx| {
view.update(cx, |view, cx| callback(view, window, cx))
.is_ok()
}),
);
activate();
subscription
}
/// Register a callback to be invoked when a keystroke is received by the application
/// in any window. Note that this fires after all other action and event mechanisms have resolved
/// and that this API will not be invoked if the event's propagation is stopped.
pub fn observe_keystrokes(
&mut self,
mut f: impl FnMut(&mut T, &KeystrokeEvent, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
fn inner(
keystroke_observers: &SubscriberSet<(), KeystrokeObserver>,
handler: KeystrokeObserver,
) -> Subscription {
let (subscription, activate) = keystroke_observers.insert((), handler);
activate();
subscription
}
let view = self.weak_entity();
inner(
&self.keystroke_observers,
Box::new(move |event, window, cx| {
if let Some(view) = view.upgrade() {
view.update(cx, |view, cx| f(view, event, window, cx));
true
} else {
false
}
}),
)
}
/// Register a callback to be invoked when the window's pending input changes.
pub fn observe_pending_input(
&self,
window: &mut Window,
mut callback: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let (subscription, activate) = window.pending_input_observers.insert(
(),
Box::new(move |window, cx| {
view.update(cx, |view, cx| callback(view, window, cx))
.is_ok()
}),
);
activate();
subscription
}
/// Register a listener to be called when the given focus handle receives focus.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus(
&mut self,
handle: &FocusHandle,
window: &mut Window,
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let focus_id = handle.id;
let (subscription, activate) =
window.new_focus_listener(Box::new(move |event, window, cx| {
view.update(cx, |view, cx| {
if event.previous_focus_path.last() != Some(&focus_id)
&& event.current_focus_path.last() == Some(&focus_id)
{
listener(view, window, cx)
}
})
.is_ok()
}));
self.defer(|_| activate());
subscription
}
/// Register a listener to be called when the given focus handle or one of its descendants receives focus.
/// This does not fire if the given focus handle - or one of its descendants - was previously focused.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_in(
&mut self,
handle: &FocusHandle,
window: &mut Window,
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let focus_id = handle.id;
let (subscription, activate) =
window.new_focus_listener(Box::new(move |event, window, cx| {
view.update(cx, |view, cx| {
if event.is_focus_in(focus_id) {
listener(view, window, cx)
}
})
.is_ok()
}));
self.defer(|_| activate());
subscription
}
/// Register a listener to be called when the given focus handle loses focus.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_blur(
&mut self,
handle: &FocusHandle,
window: &mut Window,
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let focus_id = handle.id;
let (subscription, activate) =
window.new_focus_listener(Box::new(move |event, window, cx| {
view.update(cx, |view, cx| {
if event.previous_focus_path.last() == Some(&focus_id)
&& event.current_focus_path.last() != Some(&focus_id)
{
listener(view, window, cx)
}
})
.is_ok()
}));
self.defer(|_| activate());
subscription
}
/// Register a listener to be called when nothing in the window has focus.
/// This typically happens when the node that was focused is removed from the tree,
/// and this callback lets you chose a default place to restore the users focus.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_lost(
&mut self,
window: &mut Window,
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let (subscription, activate) = window.focus_lost_listeners.insert(
(),
Box::new(move |window, cx| {
view.update(cx, |view, cx| listener(view, window, cx))
.is_ok()
}),
);
self.defer(|_| activate());
subscription
}
/// Register a listener to be called when the given focus handle or one of its descendants loses focus.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_out(
&mut self,
handle: &FocusHandle,
window: &mut Window,
mut listener: impl FnMut(&mut T, FocusOutEvent, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let view = self.weak_entity();
let focus_id = handle.id;
let (subscription, activate) =
window.new_focus_listener(Box::new(move |event, window, cx| {
view.update(cx, |view, cx| {
if let Some(blurred_id) = event.previous_focus_path.last().copied()
&& event.is_focus_out(focus_id)
{
let event = FocusOutEvent {
blurred: WeakFocusHandle {
id: blurred_id,
handles: Arc::downgrade(&cx.focus_handles),
},
};
listener(view, event, window, cx)
}
})
.is_ok()
}));
self.defer(|_| activate());
subscription
}
/// Schedule a future to be run asynchronously.
/// The given callback is invoked with a [`WeakEntity<V>`] to avoid leaking the entity for a long-running process.
/// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points.
/// The returned future will be polled on the main thread.
#[track_caller]
pub fn spawn_in<AsyncFn, R>(&self, window: &Window, f: AsyncFn) -> Task<R>
where
R: 'static,
AsyncFn: AsyncFnOnce(WeakEntity<T>, &mut AsyncWindowContext) -> R + 'static,
{
let view = self.weak_entity();
window.spawn(self, async move |cx| f(view, cx).await)
}
/// Register a callback to be invoked when the given global state changes.
pub fn observe_global_in<G: Global>(
&mut self,
window: &Window,
mut f: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
let window_handle = window.handle;
let view = self.weak_entity();
let (subscription, activate) = self.global_observers.insert(
TypeId::of::<G>(),
Box::new(move |cx| {
window_handle
.update(cx, |_, window, cx| {
view.update(cx, |view, cx| f(view, window, cx)).is_ok()
})
.unwrap_or(false)
}),
);
self.defer(move |_| activate());
subscription
}
/// Register a callback to be invoked when the given Action type is dispatched to the window.
pub fn on_action(
&mut self,
action_type: TypeId,
window: &mut Window,
listener: impl Fn(&mut T, &dyn Any, DispatchPhase, &mut Window, &mut Context<T>) + 'static,
) {
let handle = self.weak_entity();
window.on_action(action_type, move |action, phase, window, cx| {
handle
.update(cx, |view, cx| {
listener(view, action, phase, window, cx);
})
.ok();
});
}
/// Move focus to the current view, assuming it implements [`Focusable`].
pub fn focus_self(&mut self, window: &mut Window)
where
T: Focusable,
{
let view = self.entity();
window.defer(self, move |window, cx| {
view.read(cx).focus_handle(cx).focus(window)
})
}
}
impl<T> Context<'_, T> {
/// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts.
pub fn emit<Evt>(&mut self, event: Evt)
where
T: EventEmitter<Evt>,
Evt: 'static,
{
self.app.pending_effects.push_back(Effect::Emit {
emitter: self.entity_state.entity_id,
event_type: TypeId::of::<Evt>(),
event: Box::new(event),
});
}
}
impl<T> AppContext for Context<'_, T> {
type Result<U> = U;
fn new<U: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<U>) -> U) -> Entity<U> {
self.app.new(build_entity)
}
fn reserve_entity<U: 'static>(&mut self) -> Reservation<U> {
self.app.reserve_entity()
}
fn insert_entity<U: 'static>(
&mut self,
reservation: Reservation<U>,
build_entity: impl FnOnce(&mut Context<U>) -> U,
) -> Self::Result<Entity<U>> {
self.app.insert_entity(reservation, build_entity)
}
fn update_entity<U: 'static, R>(
&mut self,
handle: &Entity<U>,
update: impl FnOnce(&mut U, &mut Context<U>) -> R,
) -> R {
self.app.update_entity(handle, update)
}
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> Self::Result<super::GpuiBorrow<'a, E>>
where
E: 'static,
{
self.app.as_mut(handle)
}
fn read_entity<U, R>(
&self,
handle: &Entity<U>,
read: impl FnOnce(&U, &App) -> R,
) -> Self::Result<R>
where
U: 'static,
{
self.app.read_entity(handle, read)
}
fn update_window<R, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<R>
where
F: FnOnce(AnyView, &mut Window, &mut App) -> R,
{
self.app.update_window(window, update)
}
fn read_window<U, R>(
&self,
window: &WindowHandle<U>,
read: impl FnOnce(Entity<U>, &App) -> R,
) -> Result<R>
where
U: 'static,
{
self.app.read_window(window, read)
}
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.app.background_executor.spawn(future)
}
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
where
G: Global,
{
self.app.read_global(callback)
}
}
impl<T> Borrow<App> for Context<'_, T> {
fn borrow(&self) -> &App {
self.app
}
}
impl<T> BorrowMut<App> for Context<'_, T> {
fn borrow_mut(&mut self) -> &mut App {
self.app
}
}
+889
View File
@@ -0,0 +1,889 @@
use crate::{App, AppContext, GpuiBorrow, VisualContext, Window, seal::Sealed};
use anyhow::{Context as _, Result};
use collections::FxHashSet;
use derive_more::{Deref, DerefMut};
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use slotmap::{KeyData, SecondaryMap, SlotMap};
use std::{
any::{Any, TypeId, type_name},
cell::RefCell,
cmp::Ordering,
fmt::{self, Display},
hash::{Hash, Hasher},
marker::PhantomData,
mem,
num::NonZeroU64,
sync::{
Arc, Weak,
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
},
thread::panicking,
};
use super::Context;
use crate::util::atomic_incr_if_not_zero;
#[cfg(any(test, feature = "leak-detection"))]
use collections::HashMap;
slotmap::new_key_type! {
/// A unique identifier for a entity across the application.
pub struct EntityId;
}
impl From<u64> for EntityId {
fn from(value: u64) -> Self {
Self(KeyData::from_ffi(value))
}
}
impl EntityId {
/// Converts this entity id to a [NonZeroU64]
pub fn as_non_zero_u64(self) -> NonZeroU64 {
NonZeroU64::new(self.0.as_ffi()).unwrap()
}
/// Converts this entity id to a [u64]
pub fn as_u64(self) -> u64 {
self.0.as_ffi()
}
}
impl Display for EntityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_u64())
}
}
pub(crate) struct EntityMap {
entities: SecondaryMap<EntityId, Box<dyn Any>>,
pub accessed_entities: RefCell<FxHashSet<EntityId>>,
ref_counts: Arc<RwLock<EntityRefCounts>>,
}
struct EntityRefCounts {
counts: SlotMap<EntityId, AtomicUsize>,
dropped_entity_ids: Vec<EntityId>,
#[cfg(any(test, feature = "leak-detection"))]
leak_detector: LeakDetector,
}
impl EntityMap {
pub fn new() -> Self {
Self {
entities: SecondaryMap::new(),
accessed_entities: RefCell::new(FxHashSet::default()),
ref_counts: Arc::new(RwLock::new(EntityRefCounts {
counts: SlotMap::with_key(),
dropped_entity_ids: Vec::new(),
#[cfg(any(test, feature = "leak-detection"))]
leak_detector: LeakDetector {
next_handle_id: 0,
entity_handles: HashMap::default(),
},
})),
}
}
/// Reserve a slot for an entity, which you can subsequently use with `insert`.
pub fn reserve<T: 'static>(&self) -> Slot<T> {
let id = self.ref_counts.write().counts.insert(1.into());
Slot(Entity::new(id, Arc::downgrade(&self.ref_counts)))
}
/// Insert an entity into a slot obtained by calling `reserve`.
pub fn insert<T>(&mut self, slot: Slot<T>, entity: T) -> Entity<T>
where
T: 'static,
{
let mut accessed_entities = self.accessed_entities.borrow_mut();
accessed_entities.insert(slot.entity_id);
let handle = slot.0;
self.entities.insert(handle.entity_id, Box::new(entity));
handle
}
/// Move an entity to the stack.
#[track_caller]
pub fn lease<T>(&mut self, pointer: &Entity<T>) -> Lease<T> {
self.assert_valid_context(pointer);
let mut accessed_entities = self.accessed_entities.borrow_mut();
accessed_entities.insert(pointer.entity_id);
let entity = Some(
self.entities
.remove(pointer.entity_id)
.unwrap_or_else(|| double_lease_panic::<T>("update")),
);
Lease {
entity,
id: pointer.entity_id,
entity_type: PhantomData,
}
}
/// Returns an entity after moving it to the stack.
pub fn end_lease<T>(&mut self, mut lease: Lease<T>) {
self.entities.insert(lease.id, lease.entity.take().unwrap());
}
pub fn read<T: 'static>(&self, entity: &Entity<T>) -> &T {
self.assert_valid_context(entity);
let mut accessed_entities = self.accessed_entities.borrow_mut();
accessed_entities.insert(entity.entity_id);
self.entities
.get(entity.entity_id)
.and_then(|entity| entity.downcast_ref())
.unwrap_or_else(|| double_lease_panic::<T>("read"))
}
fn assert_valid_context(&self, entity: &AnyEntity) {
debug_assert!(
Weak::ptr_eq(&entity.entity_map, &Arc::downgrade(&self.ref_counts)),
"used a entity with the wrong context"
);
}
pub fn extend_accessed(&mut self, entities: &FxHashSet<EntityId>) {
self.accessed_entities
.borrow_mut()
.extend(entities.iter().copied());
}
pub fn clear_accessed(&mut self) {
self.accessed_entities.borrow_mut().clear();
}
pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
let mut ref_counts = self.ref_counts.write();
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
let mut accessed_entities = self.accessed_entities.borrow_mut();
dropped_entity_ids
.into_iter()
.filter_map(|entity_id| {
let count = ref_counts.counts.remove(entity_id).unwrap();
debug_assert_eq!(
count.load(SeqCst),
0,
"dropped an entity that was referenced"
);
accessed_entities.remove(&entity_id);
// If the EntityId was allocated with `Context::reserve`,
// the entity may not have been inserted.
Some((entity_id, self.entities.remove(entity_id)?))
})
.collect()
}
}
#[track_caller]
fn double_lease_panic<T>(operation: &str) -> ! {
panic!(
"cannot {operation} {} while it is already being updated",
std::any::type_name::<T>()
)
}
pub(crate) struct Lease<T> {
entity: Option<Box<dyn Any>>,
pub id: EntityId,
entity_type: PhantomData<T>,
}
impl<T: 'static> core::ops::Deref for Lease<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.entity.as_ref().unwrap().downcast_ref().unwrap()
}
}
impl<T: 'static> core::ops::DerefMut for Lease<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.entity.as_mut().unwrap().downcast_mut().unwrap()
}
}
impl<T> Drop for Lease<T> {
fn drop(&mut self) {
if self.entity.is_some() && !panicking() {
panic!("Leases must be ended with EntityMap::end_lease")
}
}
}
#[derive(Deref, DerefMut)]
pub(crate) struct Slot<T>(Entity<T>);
/// A dynamically typed reference to a entity, which can be downcast into a `Entity<T>`.
pub struct AnyEntity {
pub(crate) entity_id: EntityId,
pub(crate) entity_type: TypeId,
entity_map: Weak<RwLock<EntityRefCounts>>,
#[cfg(any(test, feature = "leak-detection"))]
handle_id: HandleId,
}
impl AnyEntity {
fn new(id: EntityId, entity_type: TypeId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self {
Self {
entity_id: id,
entity_type,
#[cfg(any(test, feature = "leak-detection"))]
handle_id: entity_map
.clone()
.upgrade()
.unwrap()
.write()
.leak_detector
.handle_created(id),
entity_map,
}
}
/// Returns the id associated with this entity.
pub fn entity_id(&self) -> EntityId {
self.entity_id
}
/// Returns the [TypeId] associated with this entity.
pub fn entity_type(&self) -> TypeId {
self.entity_type
}
/// Converts this entity handle into a weak variant, which does not prevent it from being released.
pub fn downgrade(&self) -> AnyWeakEntity {
AnyWeakEntity {
entity_id: self.entity_id,
entity_type: self.entity_type,
entity_ref_counts: self.entity_map.clone(),
}
}
/// Converts this entity handle into a strongly-typed entity handle of the given type.
/// If this entity handle is not of the specified type, returns itself as an error variant.
pub fn downcast<T: 'static>(self) -> Result<Entity<T>, AnyEntity> {
if TypeId::of::<T>() == self.entity_type {
Ok(Entity {
any_entity: self,
entity_type: PhantomData,
})
} else {
Err(self)
}
}
}
impl Clone for AnyEntity {
fn clone(&self) -> Self {
if let Some(entity_map) = self.entity_map.upgrade() {
let entity_map = entity_map.read();
let count = entity_map
.counts
.get(self.entity_id)
.expect("detected over-release of a entity");
let prev_count = count.fetch_add(1, SeqCst);
assert_ne!(prev_count, 0, "Detected over-release of a entity.");
}
Self {
entity_id: self.entity_id,
entity_type: self.entity_type,
entity_map: self.entity_map.clone(),
#[cfg(any(test, feature = "leak-detection"))]
handle_id: self
.entity_map
.upgrade()
.unwrap()
.write()
.leak_detector
.handle_created(self.entity_id),
}
}
}
impl Drop for AnyEntity {
fn drop(&mut self) {
if let Some(entity_map) = self.entity_map.upgrade() {
let entity_map = entity_map.upgradable_read();
let count = entity_map
.counts
.get(self.entity_id)
.expect("detected over-release of a handle.");
let prev_count = count.fetch_sub(1, SeqCst);
assert_ne!(prev_count, 0, "Detected over-release of a entity.");
if prev_count == 1 {
// We were the last reference to this entity, so we can remove it.
let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map);
entity_map.dropped_entity_ids.push(self.entity_id);
}
}
#[cfg(any(test, feature = "leak-detection"))]
if let Some(entity_map) = self.entity_map.upgrade() {
entity_map
.write()
.leak_detector
.handle_released(self.entity_id, self.handle_id)
}
}
}
impl<T> From<Entity<T>> for AnyEntity {
fn from(entity: Entity<T>) -> Self {
entity.any_entity
}
}
impl Hash for AnyEntity {
fn hash<H: Hasher>(&self, state: &mut H) {
self.entity_id.hash(state);
}
}
impl PartialEq for AnyEntity {
fn eq(&self, other: &Self) -> bool {
self.entity_id == other.entity_id
}
}
impl Eq for AnyEntity {}
impl Ord for AnyEntity {
fn cmp(&self, other: &Self) -> Ordering {
self.entity_id.cmp(&other.entity_id)
}
}
impl PartialOrd for AnyEntity {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl std::fmt::Debug for AnyEntity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AnyEntity")
.field("entity_id", &self.entity_id.as_u64())
.finish()
}
}
/// A strong, well-typed reference to a struct which is managed
/// by GPUI
#[derive(Deref, DerefMut)]
pub struct Entity<T> {
#[deref]
#[deref_mut]
pub(crate) any_entity: AnyEntity,
pub(crate) entity_type: PhantomData<fn(T) -> T>,
}
impl<T> Sealed for Entity<T> {}
impl<T: 'static> Entity<T> {
fn new(id: EntityId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self
where
T: 'static,
{
Self {
any_entity: AnyEntity::new(id, TypeId::of::<T>(), entity_map),
entity_type: PhantomData,
}
}
/// Get the entity ID associated with this entity
pub fn entity_id(&self) -> EntityId {
self.any_entity.entity_id
}
/// Downgrade this entity pointer to a non-retaining weak pointer
pub fn downgrade(&self) -> WeakEntity<T> {
WeakEntity {
any_entity: self.any_entity.downgrade(),
entity_type: self.entity_type,
}
}
/// Convert this into a dynamically typed entity.
pub fn into_any(self) -> AnyEntity {
self.any_entity
}
/// Grab a reference to this entity from the context.
pub fn read<'a>(&self, cx: &'a App) -> &'a T {
cx.entities.read(self)
}
/// Read the entity referenced by this handle with the given function.
pub fn read_with<R, C: AppContext>(
&self,
cx: &C,
f: impl FnOnce(&T, &App) -> R,
) -> C::Result<R> {
cx.read_entity(self, f)
}
/// Updates the entity referenced by this handle with the given function.
pub fn update<R, C: AppContext>(
&self,
cx: &mut C,
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> C::Result<R> {
cx.update_entity(self, update)
}
/// Updates the entity referenced by this handle with the given function.
pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> C::Result<GpuiBorrow<'a, T>> {
cx.as_mut(self)
}
/// Updates the entity referenced by this handle with the given function.
pub fn write<C: AppContext>(&self, cx: &mut C, value: T) -> C::Result<()> {
self.update(cx, |entity, cx| {
*entity = value;
cx.notify();
})
}
/// Updates the entity referenced by this handle with the given function if
/// the referenced entity still exists, within a visual context that has a window.
/// Returns an error if the entity has been released.
pub fn update_in<R, C: VisualContext>(
&self,
cx: &mut C,
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
) -> C::Result<R> {
cx.update_window_entity(self, update)
}
}
impl<T> Clone for Entity<T> {
fn clone(&self) -> Self {
Self {
any_entity: self.any_entity.clone(),
entity_type: self.entity_type,
}
}
}
impl<T> std::fmt::Debug for Entity<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Entity")
.field("entity_id", &self.any_entity.entity_id)
.field("entity_type", &type_name::<T>())
.finish()
}
}
impl<T> Hash for Entity<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.any_entity.hash(state);
}
}
impl<T> PartialEq for Entity<T> {
fn eq(&self, other: &Self) -> bool {
self.any_entity == other.any_entity
}
}
impl<T> Eq for Entity<T> {}
impl<T> PartialEq<WeakEntity<T>> for Entity<T> {
fn eq(&self, other: &WeakEntity<T>) -> bool {
self.any_entity.entity_id() == other.entity_id()
}
}
impl<T: 'static> Ord for Entity<T> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.entity_id().cmp(&other.entity_id())
}
}
impl<T: 'static> PartialOrd for Entity<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
/// A type erased, weak reference to a entity.
#[derive(Clone)]
pub struct AnyWeakEntity {
pub(crate) entity_id: EntityId,
entity_type: TypeId,
entity_ref_counts: Weak<RwLock<EntityRefCounts>>,
}
impl AnyWeakEntity {
/// Get the entity ID associated with this weak reference.
pub fn entity_id(&self) -> EntityId {
self.entity_id
}
/// Check if this weak handle can be upgraded, or if the entity has already been dropped
pub fn is_upgradable(&self) -> bool {
let ref_count = self
.entity_ref_counts
.upgrade()
.and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst)))
.unwrap_or(0);
ref_count > 0
}
/// Upgrade this weak entity reference to a strong reference.
pub fn upgrade(&self) -> Option<AnyEntity> {
let ref_counts = &self.entity_ref_counts.upgrade()?;
let ref_counts = ref_counts.read();
let ref_count = ref_counts.counts.get(self.entity_id)?;
if atomic_incr_if_not_zero(ref_count) == 0 {
// entity_id is in dropped_entity_ids
return None;
}
drop(ref_counts);
Some(AnyEntity {
entity_id: self.entity_id,
entity_type: self.entity_type,
entity_map: self.entity_ref_counts.clone(),
#[cfg(any(test, feature = "leak-detection"))]
handle_id: self
.entity_ref_counts
.upgrade()
.unwrap()
.write()
.leak_detector
.handle_created(self.entity_id),
})
}
/// Assert that entity referenced by this weak handle has been released.
#[cfg(any(test, feature = "leak-detection"))]
pub fn assert_released(&self) {
self.entity_ref_counts
.upgrade()
.unwrap()
.write()
.leak_detector
.assert_released(self.entity_id);
if self
.entity_ref_counts
.upgrade()
.and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst)))
.is_some()
{
panic!(
"entity was recently dropped but resources are retained until the end of the effect cycle."
)
}
}
/// Creates a weak entity that can never be upgraded.
pub fn new_invalid() -> Self {
/// To hold the invariant that all ids are unique, and considering that slotmap
/// increases their IDs from `0`, we can decrease ours from `u64::MAX` so these
/// two will never conflict (u64 is way too large).
static UNIQUE_NON_CONFLICTING_ID_GENERATOR: AtomicU64 = AtomicU64::new(u64::MAX);
let entity_id = UNIQUE_NON_CONFLICTING_ID_GENERATOR.fetch_sub(1, SeqCst);
Self {
// Safety:
// Docs say this is safe but can be unspecified if slotmap changes the representation
// after `1.0.7`, that said, providing a valid entity_id here is not necessary as long
// as we guarantee that `entity_id` is never used if `entity_ref_counts` equals
// to `Weak::new()` (that is, it's unable to upgrade), that is the invariant that
// actually needs to be hold true.
//
// And there is no sane reason to read an entity slot if `entity_ref_counts` can't be
// read in the first place, so we're good!
entity_id: entity_id.into(),
entity_type: TypeId::of::<()>(),
entity_ref_counts: Weak::new(),
}
}
}
impl std::fmt::Debug for AnyWeakEntity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(type_name::<Self>())
.field("entity_id", &self.entity_id)
.field("entity_type", &self.entity_type)
.finish()
}
}
impl<T> From<WeakEntity<T>> for AnyWeakEntity {
fn from(entity: WeakEntity<T>) -> Self {
entity.any_entity
}
}
impl Hash for AnyWeakEntity {
fn hash<H: Hasher>(&self, state: &mut H) {
self.entity_id.hash(state);
}
}
impl PartialEq for AnyWeakEntity {
fn eq(&self, other: &Self) -> bool {
self.entity_id == other.entity_id
}
}
impl Eq for AnyWeakEntity {}
impl Ord for AnyWeakEntity {
fn cmp(&self, other: &Self) -> Ordering {
self.entity_id.cmp(&other.entity_id)
}
}
impl PartialOrd for AnyWeakEntity {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// A weak reference to a entity of the given type.
#[derive(Deref, DerefMut)]
pub struct WeakEntity<T> {
#[deref]
#[deref_mut]
any_entity: AnyWeakEntity,
entity_type: PhantomData<fn(T) -> T>,
}
impl<T> std::fmt::Debug for WeakEntity<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(type_name::<Self>())
.field("entity_id", &self.any_entity.entity_id)
.field("entity_type", &type_name::<T>())
.finish()
}
}
impl<T> Clone for WeakEntity<T> {
fn clone(&self) -> Self {
Self {
any_entity: self.any_entity.clone(),
entity_type: self.entity_type,
}
}
}
impl<T: 'static> WeakEntity<T> {
/// Upgrade this weak entity reference into a strong entity reference
pub fn upgrade(&self) -> Option<Entity<T>> {
Some(Entity {
any_entity: self.any_entity.upgrade()?,
entity_type: self.entity_type,
})
}
/// Updates the entity referenced by this handle with the given function if
/// the referenced entity still exists. Returns an error if the entity has
/// been released.
pub fn update<C, R>(
&self,
cx: &mut C,
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> Result<R>
where
C: AppContext,
Result<C::Result<R>>: crate::Flatten<R>,
{
crate::Flatten::flatten(
self.upgrade()
.context("entity released")
.map(|this| cx.update_entity(&this, update)),
)
}
/// Updates the entity referenced by this handle with the given function if
/// the referenced entity still exists, within a visual context that has a window.
/// Returns an error if the entity has been released.
pub fn update_in<C, R>(
&self,
cx: &mut C,
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
) -> Result<R>
where
C: VisualContext,
Result<C::Result<R>>: crate::Flatten<R>,
{
let window = cx.window_handle();
let this = self.upgrade().context("entity released")?;
crate::Flatten::flatten(window.update(cx, |_, window, cx| {
this.update(cx, |entity, cx| update(entity, window, cx))
}))
}
/// Reads the entity referenced by this handle with the given function if
/// the referenced entity still exists. Returns an error if the entity has
/// been released.
pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &App) -> R) -> Result<R>
where
C: AppContext,
Result<C::Result<R>>: crate::Flatten<R>,
{
crate::Flatten::flatten(
self.upgrade()
.context("entity released")
.map(|this| cx.read_entity(&this, read)),
)
}
/// Create a new weak entity that can never be upgraded.
pub fn new_invalid() -> Self {
Self {
any_entity: AnyWeakEntity::new_invalid(),
entity_type: PhantomData,
}
}
}
impl<T> Hash for WeakEntity<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.any_entity.hash(state);
}
}
impl<T> PartialEq for WeakEntity<T> {
fn eq(&self, other: &Self) -> bool {
self.any_entity == other.any_entity
}
}
impl<T> Eq for WeakEntity<T> {}
impl<T> PartialEq<Entity<T>> for WeakEntity<T> {
fn eq(&self, other: &Entity<T>) -> bool {
self.entity_id() == other.any_entity.entity_id()
}
}
impl<T: 'static> Ord for WeakEntity<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.entity_id().cmp(&other.entity_id())
}
}
impl<T: 'static> PartialOrd for WeakEntity<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(any(test, feature = "leak-detection"))]
static LEAK_BACKTRACE: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty()));
#[cfg(any(test, feature = "leak-detection"))]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
pub(crate) struct HandleId {
id: u64, // id of the handle itself, not the pointed at object
}
#[cfg(any(test, feature = "leak-detection"))]
pub(crate) struct LeakDetector {
next_handle_id: u64,
entity_handles: HashMap<EntityId, HashMap<HandleId, Option<backtrace::Backtrace>>>,
}
#[cfg(any(test, feature = "leak-detection"))]
impl LeakDetector {
#[track_caller]
pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId {
let id = util::post_inc(&mut self.next_handle_id);
let handle_id = HandleId { id };
let handles = self.entity_handles.entry(entity_id).or_default();
handles.insert(
handle_id,
LEAK_BACKTRACE.then(backtrace::Backtrace::new_unresolved),
);
handle_id
}
pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) {
let handles = self.entity_handles.entry(entity_id).or_default();
handles.remove(&handle_id);
}
pub fn assert_released(&mut self, entity_id: EntityId) {
let handles = self.entity_handles.entry(entity_id).or_default();
if !handles.is_empty() {
for backtrace in handles.values_mut() {
if let Some(mut backtrace) = backtrace.take() {
backtrace.resolve();
eprintln!("Leaked handle: {:#?}", backtrace);
} else {
eprintln!("Leaked handle: export LEAK_BACKTRACE to find allocation site");
}
}
panic!();
}
}
}
#[cfg(test)]
mod test {
use crate::EntityMap;
struct TestEntity {
pub i: i32,
}
#[test]
fn test_entity_map_slot_assignment_before_cleanup() {
// Tests that slots are not re-used before take_dropped.
let mut entity_map = EntityMap::new();
let slot = entity_map.reserve::<TestEntity>();
entity_map.insert(slot, TestEntity { i: 1 });
let slot = entity_map.reserve::<TestEntity>();
entity_map.insert(slot, TestEntity { i: 2 });
let dropped = entity_map.take_dropped();
assert_eq!(dropped.len(), 2);
assert_eq!(
dropped
.into_iter()
.map(|(_, entity)| entity.downcast::<TestEntity>().unwrap().i)
.collect::<Vec<i32>>(),
vec![1, 2],
);
}
#[test]
fn test_entity_map_weak_upgrade_before_cleanup() {
// Tests that weak handles are not upgraded before take_dropped
let mut entity_map = EntityMap::new();
let slot = entity_map.reserve::<TestEntity>();
let handle = entity_map.insert(slot, TestEntity { i: 1 });
let weak = handle.downgrade();
drop(handle);
let strong = weak.upgrade();
assert_eq!(strong, None);
let dropped = entity_map.take_dropped();
assert_eq!(dropped.len(), 1);
assert_eq!(
dropped
.into_iter()
.map(|(_, entity)| entity.downcast::<TestEntity>().unwrap().i)
.collect::<Vec<i32>>(),
vec![1],
);
}
}
File diff suppressed because it is too large Load Diff
+289
View File
@@ -0,0 +1,289 @@
use std::{
alloc::{self, handle_alloc_error},
cell::Cell,
num::NonZeroUsize,
ops::{Deref, DerefMut},
ptr::{self, NonNull},
rc::Rc,
};
struct ArenaElement {
value: *mut u8,
drop: unsafe fn(*mut u8),
}
impl Drop for ArenaElement {
#[inline(always)]
fn drop(&mut self) {
unsafe { (self.drop)(self.value) };
}
}
struct Chunk {
start: *mut u8,
end: *mut u8,
offset: *mut u8,
}
impl Drop for Chunk {
fn drop(&mut self) {
unsafe {
let chunk_size = self.end.offset_from_unsigned(self.start);
// SAFETY: This succeeded during allocation.
let layout = alloc::Layout::from_size_align_unchecked(chunk_size, 1);
alloc::dealloc(self.start, layout);
}
}
}
impl Chunk {
fn new(chunk_size: NonZeroUsize) -> Self {
// this only fails if chunk_size is unreasonably huge
let layout = alloc::Layout::from_size_align(chunk_size.get(), 1).unwrap();
let start = unsafe { alloc::alloc(layout) };
if start.is_null() {
handle_alloc_error(layout);
}
let end = unsafe { start.add(chunk_size.get()) };
Self {
start,
end,
offset: start,
}
}
fn allocate(&mut self, layout: alloc::Layout) -> Option<NonNull<u8>> {
let aligned = unsafe { self.offset.add(self.offset.align_offset(layout.align())) };
let next = unsafe { aligned.add(layout.size()) };
if next <= self.end {
self.offset = next;
NonNull::new(aligned)
} else {
None
}
}
fn reset(&mut self) {
self.offset = self.start;
}
}
pub struct Arena {
chunks: Vec<Chunk>,
elements: Vec<ArenaElement>,
valid: Rc<Cell<bool>>,
current_chunk_index: usize,
chunk_size: NonZeroUsize,
}
impl Drop for Arena {
fn drop(&mut self) {
self.clear();
}
}
impl Arena {
pub fn new(chunk_size: usize) -> Self {
let chunk_size = NonZeroUsize::try_from(chunk_size).unwrap();
Self {
chunks: vec![Chunk::new(chunk_size)],
elements: Vec::new(),
valid: Rc::new(Cell::new(true)),
current_chunk_index: 0,
chunk_size,
}
}
pub fn capacity(&self) -> usize {
self.chunks.len() * self.chunk_size.get()
}
pub fn clear(&mut self) {
self.valid.set(false);
self.valid = Rc::new(Cell::new(true));
self.elements.clear();
for chunk_index in 0..=self.current_chunk_index {
self.chunks[chunk_index].reset();
}
self.current_chunk_index = 0;
}
#[inline(always)]
pub fn alloc<T>(&mut self, f: impl FnOnce() -> T) -> ArenaBox<T> {
#[inline(always)]
unsafe fn inner_writer<T, F>(ptr: *mut T, f: F)
where
F: FnOnce() -> T,
{
unsafe { ptr::write(ptr, f()) };
}
unsafe fn drop<T>(ptr: *mut u8) {
unsafe { std::ptr::drop_in_place(ptr.cast::<T>()) };
}
let layout = alloc::Layout::new::<T>();
let mut current_chunk = &mut self.chunks[self.current_chunk_index];
let ptr = if let Some(ptr) = current_chunk.allocate(layout) {
ptr.as_ptr()
} else {
self.current_chunk_index += 1;
if self.current_chunk_index >= self.chunks.len() {
self.chunks.push(Chunk::new(self.chunk_size));
assert_eq!(self.current_chunk_index, self.chunks.len() - 1);
log::trace!(
"increased element arena capacity to {}kb",
self.capacity() / 1024,
);
}
current_chunk = &mut self.chunks[self.current_chunk_index];
if let Some(ptr) = current_chunk.allocate(layout) {
ptr.as_ptr()
} else {
panic!(
"Arena chunk_size of {} is too small to allocate {} bytes",
self.chunk_size,
layout.size()
);
}
};
unsafe { inner_writer(ptr.cast(), f) };
self.elements.push(ArenaElement {
value: ptr,
drop: drop::<T>,
});
ArenaBox {
ptr: ptr.cast(),
valid: self.valid.clone(),
}
}
}
pub struct ArenaBox<T: ?Sized> {
ptr: *mut T,
valid: Rc<Cell<bool>>,
}
impl<T: ?Sized> ArenaBox<T> {
#[inline(always)]
pub fn map<U: ?Sized>(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaBox<U> {
ArenaBox {
ptr: f(&mut self),
valid: self.valid,
}
}
#[track_caller]
fn validate(&self) {
assert!(
self.valid.get(),
"attempted to dereference an ArenaRef after its Arena was cleared"
);
}
}
impl<T: ?Sized> Deref for ArenaBox<T> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &Self::Target {
self.validate();
unsafe { &*self.ptr }
}
}
impl<T: ?Sized> DerefMut for ArenaBox<T> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut Self::Target {
self.validate();
unsafe { &mut *self.ptr }
}
}
#[cfg(test)]
mod tests {
use std::{cell::Cell, rc::Rc};
use super::*;
#[test]
fn test_arena() {
let mut arena = Arena::new(1024);
let a = arena.alloc(|| 1u64);
let b = arena.alloc(|| 2u32);
let c = arena.alloc(|| 3u16);
let d = arena.alloc(|| 4u8);
assert_eq!(*a, 1);
assert_eq!(*b, 2);
assert_eq!(*c, 3);
assert_eq!(*d, 4);
arena.clear();
let a = arena.alloc(|| 5u64);
let b = arena.alloc(|| 6u32);
let c = arena.alloc(|| 7u16);
let d = arena.alloc(|| 8u8);
assert_eq!(*a, 5);
assert_eq!(*b, 6);
assert_eq!(*c, 7);
assert_eq!(*d, 8);
// Ensure drop gets called.
let dropped = Rc::new(Cell::new(false));
struct DropGuard(Rc<Cell<bool>>);
impl Drop for DropGuard {
fn drop(&mut self) {
self.0.set(true);
}
}
arena.alloc(|| DropGuard(dropped.clone()));
arena.clear();
assert!(dropped.get());
}
#[test]
fn test_arena_grow() {
let mut arena = Arena::new(8);
arena.alloc(|| 1u64);
arena.alloc(|| 2u64);
assert_eq!(arena.capacity(), 16);
arena.alloc(|| 3u32);
arena.alloc(|| 4u32);
assert_eq!(arena.capacity(), 24);
}
#[test]
fn test_arena_alignment() {
let mut arena = Arena::new(256);
let x1 = arena.alloc(|| 1u8);
let x2 = arena.alloc(|| 2u16);
let x3 = arena.alloc(|| 3u32);
let x4 = arena.alloc(|| 4u64);
let x5 = arena.alloc(|| 5u64);
assert_eq!(*x1, 1);
assert_eq!(*x2, 2);
assert_eq!(*x3, 3);
assert_eq!(*x4, 4);
assert_eq!(*x5, 5);
assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0);
assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0);
}
#[test]
#[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")]
fn test_arena_use_after_clear() {
let mut arena = Arena::new(16);
let value = arena.alloc(|| 1u64);
arena.clear();
let _read_value = *value;
}
}
+84
View File
@@ -0,0 +1,84 @@
use crate::{App, SharedString, SharedUri};
use futures::{Future, TryFutureExt};
use std::fmt::Debug;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::Arc;
/// An enum representing
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum Resource {
/// This resource is at a given URI
Uri(SharedUri),
/// This resource is at a given path in the file system
Path(Arc<Path>),
/// This resource is embedded in the application binary
Embedded(SharedString),
}
impl From<SharedUri> for Resource {
fn from(value: SharedUri) -> Self {
Self::Uri(value)
}
}
impl From<PathBuf> for Resource {
fn from(value: PathBuf) -> Self {
Self::Path(value.into())
}
}
impl From<Arc<Path>> for Resource {
fn from(value: Arc<Path>) -> Self {
Self::Path(value)
}
}
/// A trait for asynchronous asset loading.
pub trait Asset: 'static {
/// The source of the asset.
type Source: Clone + Hash + Send;
/// The loaded asset
type Output: Clone + Send;
/// Load the asset asynchronously
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static;
}
/// An asset Loader which logs the [`Err`] variant of a [`Result`] during loading
pub enum AssetLogger<T> {
#[doc(hidden)]
_Phantom(PhantomData<T>, &'static dyn crate::seal::Sealed),
}
impl<T, R, E> Asset for AssetLogger<T>
where
T: Asset<Output = Result<R, E>>,
R: Clone + Send,
E: Clone + Send + std::fmt::Display,
{
type Source = T::Source;
type Output = T::Output;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
let load = T::load(source, cx);
load.inspect_err(|e| log::error!("Failed to load asset: {}", e))
}
}
/// Use a quick, non-cryptographically secure hash function to get an identifier from data
pub fn hash<T: Hash>(data: &T) -> u64 {
let mut hasher = collections::FxHasher::default();
data.hash(&mut hasher);
hasher.finish()
}
+107
View File
@@ -0,0 +1,107 @@
use crate::{DevicePixels, Pixels, Result, SharedString, Size, size};
use smallvec::SmallVec;
use image::{Delay, Frame};
use std::{
borrow::Cow,
fmt,
hash::Hash,
sync::atomic::{AtomicUsize, Ordering::SeqCst},
};
/// A source of assets for this app to use.
pub trait AssetSource: 'static + Send + Sync {
/// Load the given asset from the source path.
fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>>;
/// List the assets at the given path.
fn list(&self, path: &str) -> Result<Vec<SharedString>>;
}
impl AssetSource for () {
fn load(&self, _path: &str) -> Result<Option<Cow<'static, [u8]>>> {
Ok(None)
}
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
Ok(vec![])
}
}
/// A unique identifier for the image cache
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ImageId(pub usize);
#[derive(PartialEq, Eq, Hash, Clone)]
pub(crate) struct RenderImageParams {
pub(crate) image_id: ImageId,
pub(crate) frame_index: usize,
}
/// A cached and processed image, in BGRA format
pub struct RenderImage {
/// The ID associated with this image
pub id: ImageId,
/// The scale factor of this image on render.
pub(crate) scale_factor: f32,
data: SmallVec<[Frame; 1]>,
}
impl PartialEq for RenderImage {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for RenderImage {}
impl RenderImage {
/// Create a new image from the given data.
pub fn new(data: impl Into<SmallVec<[Frame; 1]>>) -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Self {
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
scale_factor: 1.0,
data: data.into(),
}
}
/// Convert this image into a byte slice.
pub fn as_bytes(&self, frame_index: usize) -> Option<&[u8]> {
self.data
.get(frame_index)
.map(|frame| frame.buffer().as_raw().as_slice())
}
/// Get the size of this image, in pixels.
pub fn size(&self, frame_index: usize) -> Size<DevicePixels> {
let (width, height) = self.data[frame_index].buffer().dimensions();
size(width.into(), height.into())
}
/// Get the size of this image, in pixels for display, adjusted for the scale factor.
pub(crate) fn render_size(&self, frame_index: usize) -> Size<Pixels> {
self.size(frame_index)
.map(|v| (v.0 as f32 / self.scale_factor).into())
}
/// Get the delay of this frame from the previous
pub fn delay(&self, frame_index: usize) -> Delay {
self.data[frame_index].delay()
}
/// Get the number of frames for this image.
pub fn frame_count(&self) -> usize {
self.data.len()
}
}
impl fmt::Debug for RenderImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageData")
.field("id", &self.id)
.field("size", &self.size(0))
.finish()
}
}
+337
View File
@@ -0,0 +1,337 @@
use crate::{Bounds, Half};
use std::{
cmp,
fmt::Debug,
ops::{Add, Sub},
};
#[derive(Debug)]
pub(crate) struct BoundsTree<U>
where
U: Clone + Debug + Default + PartialEq,
{
root: Option<usize>,
nodes: Vec<Node<U>>,
stack: Vec<usize>,
}
impl<U> BoundsTree<U>
where
U: Clone
+ Debug
+ PartialEq
+ PartialOrd
+ Add<U, Output = U>
+ Sub<Output = U>
+ Half
+ Default,
{
pub fn clear(&mut self) {
self.root = None;
self.nodes.clear();
self.stack.clear();
}
pub fn insert(&mut self, new_bounds: Bounds<U>) -> u32 {
// If the tree is empty, make the root the new leaf.
if self.root.is_none() {
let new_node = self.push_leaf(new_bounds, 1);
self.root = Some(new_node);
return 1;
}
// Search for the best place to add the new leaf based on heuristics.
let mut max_intersecting_ordering = 0;
let mut index = self.root.unwrap();
while let Node::Internal {
left,
right,
bounds: node_bounds,
..
} = &mut self.nodes[index]
{
let left = *left;
let right = *right;
*node_bounds = node_bounds.union(&new_bounds);
self.stack.push(index);
// Descend to the best-fit child, based on which one would increase
// the surface area the least. This attempts to keep the tree balanced
// in terms of surface area. If there is an intersection with the other child,
// add its keys to the intersections vector.
let left_cost = new_bounds.union(self.nodes[left].bounds()).half_perimeter();
let right_cost = new_bounds
.union(self.nodes[right].bounds())
.half_perimeter();
if left_cost < right_cost {
max_intersecting_ordering =
self.find_max_ordering(right, &new_bounds, max_intersecting_ordering);
index = left;
} else {
max_intersecting_ordering =
self.find_max_ordering(left, &new_bounds, max_intersecting_ordering);
index = right;
}
}
// We've found a leaf ('index' now refers to a leaf node).
// We'll insert a new parent node above the leaf and attach our new leaf to it.
let sibling = index;
// Check for collision with the located leaf node
let Node::Leaf {
bounds: sibling_bounds,
order: sibling_ordering,
..
} = &self.nodes[index]
else {
unreachable!();
};
if sibling_bounds.intersects(&new_bounds) {
max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering);
}
let ordering = max_intersecting_ordering + 1;
let new_node = self.push_leaf(new_bounds, ordering);
let new_parent = self.push_internal(sibling, new_node);
// If there was an old parent, we need to update its children indices.
if let Some(old_parent) = self.stack.last().copied() {
let Node::Internal { left, right, .. } = &mut self.nodes[old_parent] else {
unreachable!();
};
if *left == sibling {
*left = new_parent;
} else {
*right = new_parent;
}
} else {
// If the old parent was the root, the new parent is the new root.
self.root = Some(new_parent);
}
for node_index in self.stack.drain(..).rev() {
let Node::Internal {
max_order: max_ordering,
..
} = &mut self.nodes[node_index]
else {
unreachable!()
};
if *max_ordering >= ordering {
break;
}
*max_ordering = ordering;
}
ordering
}
fn find_max_ordering(&self, index: usize, bounds: &Bounds<U>, mut max_ordering: u32) -> u32 {
match &self.nodes[index] {
Node::Leaf {
bounds: node_bounds,
order: ordering,
..
} => {
if bounds.intersects(node_bounds) {
max_ordering = cmp::max(*ordering, max_ordering);
}
}
Node::Internal {
left,
right,
bounds: node_bounds,
max_order: node_max_ordering,
..
} => {
if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering {
let left_max_ordering = self.nodes[*left].max_ordering();
let right_max_ordering = self.nodes[*right].max_ordering();
if left_max_ordering > right_max_ordering {
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
} else {
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
}
}
}
}
max_ordering
}
fn push_leaf(&mut self, bounds: Bounds<U>, order: u32) -> usize {
self.nodes.push(Node::Leaf { bounds, order });
self.nodes.len() - 1
}
fn push_internal(&mut self, left: usize, right: usize) -> usize {
let left_node = &self.nodes[left];
let right_node = &self.nodes[right];
let new_bounds = left_node.bounds().union(right_node.bounds());
let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering());
self.nodes.push(Node::Internal {
bounds: new_bounds,
left,
right,
max_order: max_ordering,
});
self.nodes.len() - 1
}
}
impl<U> Default for BoundsTree<U>
where
U: Clone + Debug + Default + PartialEq,
{
fn default() -> Self {
BoundsTree {
root: None,
nodes: Vec::new(),
stack: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
enum Node<U>
where
U: Clone + Debug + Default + PartialEq,
{
Leaf {
bounds: Bounds<U>,
order: u32,
},
Internal {
left: usize,
right: usize,
bounds: Bounds<U>,
max_order: u32,
},
}
impl<U> Node<U>
where
U: Clone + Debug + Default + PartialEq,
{
fn bounds(&self) -> &Bounds<U> {
match self {
Node::Leaf { bounds, .. } => bounds,
Node::Internal { bounds, .. } => bounds,
}
}
fn max_ordering(&self) -> u32 {
match self {
Node::Leaf {
order: ordering, ..
} => *ordering,
Node::Internal {
max_order: max_ordering,
..
} => *max_ordering,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Bounds, Point, Size};
use rand::{Rng, SeedableRng};
#[test]
fn test_insert() {
let mut tree = BoundsTree::<f32>::default();
let bounds1 = Bounds {
origin: Point { x: 0.0, y: 0.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
let bounds2 = Bounds {
origin: Point { x: 5.0, y: 5.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
let bounds3 = Bounds {
origin: Point { x: 10.0, y: 10.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
// Insert the bounds into the tree and verify the order is correct
assert_eq!(tree.insert(bounds1), 1);
assert_eq!(tree.insert(bounds2), 2);
assert_eq!(tree.insert(bounds3), 3);
// Insert non-overlapping bounds and verify they can reuse orders
let bounds4 = Bounds {
origin: Point { x: 20.0, y: 20.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
let bounds5 = Bounds {
origin: Point { x: 40.0, y: 40.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
let bounds6 = Bounds {
origin: Point { x: 25.0, y: 25.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
assert_eq!(tree.insert(bounds4), 1); // bounds4 does not overlap with bounds1, bounds2, or bounds3
assert_eq!(tree.insert(bounds5), 1); // bounds5 does not overlap with any other bounds
assert_eq!(tree.insert(bounds6), 2); // bounds6 overlaps with bounds4, so it should have a different order
}
#[test]
fn test_random_iterations() {
let max_bounds = 100;
for seed in 1..=1000 {
// let seed = 44;
let mut tree = BoundsTree::default();
let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64);
let mut expected_quads: Vec<(Bounds<f32>, u32)> = Vec::new();
// Insert a random number of random AABBs into the tree.
let num_bounds = rng.random_range(1..=max_bounds);
for _ in 0..num_bounds {
let min_x: f32 = rng.random_range(-100.0..100.0);
let min_y: f32 = rng.random_range(-100.0..100.0);
let width: f32 = rng.random_range(0.0..50.0);
let height: f32 = rng.random_range(0.0..50.0);
let bounds = Bounds {
origin: Point { x: min_x, y: min_y },
size: Size { width, height },
};
let expected_ordering = expected_quads
.iter()
.filter_map(|quad| quad.0.intersects(&bounds).then_some(quad.1))
.max()
.unwrap_or(0)
+ 1;
expected_quads.push((bounds, expected_ordering));
// Insert the AABB into the tree and collect intersections.
let actual_ordering = tree.insert(bounds);
assert_eq!(actual_ordering, expected_ordering);
}
}
}
}
+934
View File
@@ -0,0 +1,934 @@
use anyhow::{Context as _, bail};
use schemars::{JsonSchema, json_schema};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{self, Visitor},
};
use std::borrow::Cow;
use std::{
fmt::{self, Display, Formatter},
hash::{Hash, Hasher},
};
/// Convert an RGB hex color code number to a color type
pub fn rgb(hex: u32) -> Rgba {
let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
Rgba { r, g, b, a: 1.0 }
}
/// Convert an RGBA hex color code number to [`Rgba`]
pub fn rgba(hex: u32) -> Rgba {
let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
Rgba { r, g, b, a }
}
/// Swap from RGBA with premultiplied alpha to BGRA
pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
color.swap(0, 2);
if color[3] > 0 {
let a = color[3] as f32 / 255.;
color[0] = (color[0] as f32 / a) as u8;
color[1] = (color[1] as f32 / a) as u8;
color[2] = (color[2] as f32 / a) as u8;
}
}
/// An RGBA color
#[derive(PartialEq, Clone, Copy, Default)]
#[repr(C)]
pub struct Rgba {
/// The red component of the color, in the range 0.0 to 1.0
pub r: f32,
/// The green component of the color, in the range 0.0 to 1.0
pub g: f32,
/// The blue component of the color, in the range 0.0 to 1.0
pub b: f32,
/// The alpha component of the color, in the range 0.0 to 1.0
pub a: f32,
}
impl fmt::Debug for Rgba {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "rgba({:#010x})", u32::from(*self))
}
}
impl Rgba {
/// Create a new [`Rgba`] color by blending this and another color together
pub fn blend(&self, other: Rgba) -> Self {
if other.a >= 1.0 {
other
} else if other.a <= 0.0 {
*self
} else {
Rgba {
r: (self.r * (1.0 - other.a)) + (other.r * other.a),
g: (self.g * (1.0 - other.a)) + (other.g * other.a),
b: (self.b * (1.0 - other.a)) + (other.b * other.a),
a: self.a,
}
}
}
}
impl From<Rgba> for u32 {
fn from(rgba: Rgba) -> Self {
let r = (rgba.r * 255.0) as u32;
let g = (rgba.g * 255.0) as u32;
let b = (rgba.b * 255.0) as u32;
let a = (rgba.a * 255.0) as u32;
(r << 24) | (g << 16) | (b << 8) | a
}
}
struct RgbaVisitor;
impl Visitor<'_> for RgbaVisitor {
type Value = Rgba;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
Rgba::try_from(value).map_err(E::custom)
}
}
impl JsonSchema for Rgba {
fn schema_name() -> Cow<'static, str> {
"Rgba".into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string",
"pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
})
}
}
impl<'de> Deserialize<'de> for Rgba {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(RgbaVisitor)
}
}
impl Serialize for Rgba {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let r = (self.r * 255.0).round() as u8;
let g = (self.g * 255.0).round() as u8;
let b = (self.b * 255.0).round() as u8;
let a = (self.a * 255.0).round() as u8;
let s = format!("#{r:02x}{g:02x}{b:02x}{a:02x}");
serializer.serialize_str(&s)
}
}
impl From<Hsla> for Rgba {
fn from(color: Hsla) -> Self {
let h = color.h;
let s = color.s;
let l = color.l;
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
let m = l - c / 2.0;
let cm = c + m;
let xm = x + m;
let (r, g, b) = match (h * 6.0).floor() as i32 {
0 | 6 => (cm, xm, m),
1 => (xm, cm, m),
2 => (m, cm, xm),
3 => (m, xm, cm),
4 => (xm, m, cm),
_ => (cm, m, xm),
};
Rgba {
r: r.clamp(0., 1.),
g: g.clamp(0., 1.),
b: b.clamp(0., 1.),
a: color.a,
}
}
}
impl TryFrom<&'_ str> for Rgba {
type Error = anyhow::Error;
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
const RGB: usize = "rgb".len();
const RGBA: usize = "rgba".len();
const RRGGBB: usize = "rrggbb".len();
const RRGGBBAA: usize = "rrggbbaa".len();
const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
const INVALID_UNICODE: &str = "invalid unicode characters in color";
let Some(("", hex)) = value.trim().split_once('#') else {
bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
};
let (r, g, b, a) = match hex.len() {
RGB | RGBA => {
let r = u8::from_str_radix(
hex.get(0..1).with_context(|| {
format!("{INVALID_UNICODE}: r component of #rgb/#rgba for value: '{value}'")
})?,
16,
)?;
let g = u8::from_str_radix(
hex.get(1..2).with_context(|| {
format!("{INVALID_UNICODE}: g component of #rgb/#rgba for value: '{value}'")
})?,
16,
)?;
let b = u8::from_str_radix(
hex.get(2..3).with_context(|| {
format!("{INVALID_UNICODE}: b component of #rgb/#rgba for value: '{value}'")
})?,
16,
)?;
let a = if hex.len() == RGBA {
u8::from_str_radix(
hex.get(3..4).with_context(|| {
format!("{INVALID_UNICODE}: a component of #rgba for value: '{value}'")
})?,
16,
)?
} else {
0xf
};
/// Duplicates a given hex digit.
/// E.g., `0xf` -> `0xff`.
const fn duplicate(value: u8) -> u8 {
(value << 4) | value
}
(duplicate(r), duplicate(g), duplicate(b), duplicate(a))
}
RRGGBB | RRGGBBAA => {
let r = u8::from_str_radix(
hex.get(0..2).with_context(|| {
format!(
"{}: r component of #rrggbb/#rrggbbaa for value: '{}'",
INVALID_UNICODE, value
)
})?,
16,
)?;
let g = u8::from_str_radix(
hex.get(2..4).with_context(|| {
format!(
"{INVALID_UNICODE}: g component of #rrggbb/#rrggbbaa for value: '{value}'"
)
})?,
16,
)?;
let b = u8::from_str_radix(
hex.get(4..6).with_context(|| {
format!(
"{INVALID_UNICODE}: b component of #rrggbb/#rrggbbaa for value: '{value}'"
)
})?,
16,
)?;
let a = if hex.len() == RRGGBBAA {
u8::from_str_radix(
hex.get(6..8).with_context(|| {
format!(
"{INVALID_UNICODE}: a component of #rrggbbaa for value: '{value}'"
)
})?,
16,
)?
} else {
0xff
};
(r, g, b, a)
}
_ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
};
Ok(Rgba {
r: r as f32 / 255.,
g: g as f32 / 255.,
b: b as f32 / 255.,
a: a as f32 / 255.,
})
}
}
/// An HSLA color
#[derive(Default, Copy, Clone, Debug)]
#[repr(C)]
pub struct Hsla {
/// Hue, in a range from 0 to 1
pub h: f32,
/// Saturation, in a range from 0 to 1
pub s: f32,
/// Lightness, in a range from 0 to 1
pub l: f32,
/// Alpha, in a range from 0 to 1
pub a: f32,
}
impl PartialEq for Hsla {
fn eq(&self, other: &Self) -> bool {
self.h
.total_cmp(&other.h)
.then(self.s.total_cmp(&other.s))
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
.is_eq()
}
}
impl PartialOrd for Hsla {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Hsla {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.h
.total_cmp(&other.h)
.then(self.s.total_cmp(&other.s))
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
}
}
impl Eq for Hsla {}
impl Hash for Hsla {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u32(u32::from_be_bytes(self.h.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.s.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.l.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.a.to_be_bytes()));
}
}
impl Display for Hsla {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"hsla({:.2}, {:.2}%, {:.2}%, {:.2})",
self.h * 360.,
self.s * 100.,
self.l * 100.,
self.a
)
}
}
/// Construct an [`Hsla`] object from plain values
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
Hsla {
h: h.clamp(0., 1.),
s: s.clamp(0., 1.),
l: l.clamp(0., 1.),
a: a.clamp(0., 1.),
}
}
/// Pure black in [`Hsla`]
pub const fn black() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 1.,
}
}
/// Transparent black in [`Hsla`]
pub const fn transparent_black() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.,
}
}
/// Transparent white in [`Hsla`]
pub const fn transparent_white() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 1.,
a: 0.,
}
}
/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: lightness.clamp(0., 1.),
a: opacity.clamp(0., 1.),
}
}
/// Pure white in [`Hsla`]
pub const fn white() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 1.,
a: 1.,
}
}
/// The color red in [`Hsla`]
pub const fn red() -> Hsla {
Hsla {
h: 0.,
s: 1.,
l: 0.5,
a: 1.,
}
}
/// The color blue in [`Hsla`]
pub const fn blue() -> Hsla {
Hsla {
h: 0.6666666667,
s: 1.,
l: 0.5,
a: 1.,
}
}
/// The color green in [`Hsla`]
pub const fn green() -> Hsla {
Hsla {
h: 0.3333333333,
s: 1.,
l: 0.25,
a: 1.,
}
}
/// The color yellow in [`Hsla`]
pub const fn yellow() -> Hsla {
Hsla {
h: 0.1666666667,
s: 1.,
l: 0.5,
a: 1.,
}
}
impl Hsla {
/// Converts this HSLA color to an RGBA color.
pub fn to_rgb(self) -> Rgba {
self.into()
}
/// The color red
pub const fn red() -> Self {
red()
}
/// The color green
pub const fn green() -> Self {
green()
}
/// The color blue
pub const fn blue() -> Self {
blue()
}
/// The color black
pub const fn black() -> Self {
black()
}
/// The color white
pub const fn white() -> Self {
white()
}
/// The color transparent black
pub const fn transparent_black() -> Self {
transparent_black()
}
/// Returns true if the HSLA color is fully transparent, false otherwise.
pub fn is_transparent(&self) -> bool {
self.a == 0.0
}
/// Returns true if the HSLA color is fully opaque, false otherwise.
pub fn is_opaque(&self) -> bool {
self.a == 1.0
}
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
///
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
/// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
/// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
///
/// Assumptions:
/// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value.
/// - RGB color components are contained in the range [0, 1].
/// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
pub fn blend(self, other: Hsla) -> Hsla {
let alpha = other.a;
if alpha >= 1.0 {
other
} else if alpha <= 0.0 {
self
} else {
let converted_self = Rgba::from(self);
let converted_other = Rgba::from(other);
let blended_rgb = converted_self.blend(converted_other);
Hsla::from(blended_rgb)
}
}
/// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
pub fn grayscale(&self) -> Self {
Hsla {
h: self.h,
s: 0.,
l: self.l,
a: self.a,
}
}
/// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
/// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
pub fn fade_out(&mut self, factor: f32) {
self.a *= 1.0 - factor.clamp(0., 1.);
}
/// Multiplies the alpha value of the color by a given factor
/// and returns a new HSLA color.
///
/// Useful for transforming colors with dynamic opacity,
/// like a color from an external source.
///
/// Example:
/// ```
/// let color = gpui::red();
/// let faded_color = color.opacity(0.5);
/// assert_eq!(faded_color.a, 0.5);
/// ```
///
/// This will return a red color with half the opacity.
///
/// Example:
/// ```
/// use gpui::hsla;
/// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
/// let faded_color = color.opacity(0.16);
/// assert!((faded_color.a - 0.112).abs() < 1e-6);
/// ```
///
/// This will return a blue color with around ~10% opacity,
/// suitable for an element's hover or selected state.
///
pub fn opacity(&self, factor: f32) -> Self {
Hsla {
h: self.h,
s: self.s,
l: self.l,
a: self.a * factor.clamp(0., 1.),
}
}
/// Returns a new HSLA color with the same hue, saturation,
/// and lightness, but with a new alpha value.
///
/// Example:
/// ```
/// let color = gpui::red();
/// let red_color = color.alpha(0.25);
/// assert_eq!(red_color.a, 0.25);
/// ```
///
/// This will return a red color with half the opacity.
///
/// Example:
/// ```
/// use gpui::hsla;
/// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
/// let faded_color = color.alpha(0.25);
/// assert_eq!(faded_color.a, 0.25);
/// ```
///
/// This will return a blue color with 25% opacity.
pub fn alpha(&self, a: f32) -> Self {
Hsla {
h: self.h,
s: self.s,
l: self.l,
a: a.clamp(0., 1.),
}
}
}
impl From<Rgba> for Hsla {
fn from(color: Rgba) -> Self {
let r = color.r;
let g = color.g;
let b = color.b;
let max = r.max(g.max(b));
let min = r.min(g.min(b));
let delta = max - min;
let l = (max + min) / 2.0;
let s = if l == 0.0 || l == 1.0 {
0.0
} else if l < 0.5 {
delta / (2.0 * l)
} else {
delta / (2.0 - 2.0 * l)
};
let h = if delta == 0.0 {
0.0
} else if max == r {
((g - b) / delta).rem_euclid(6.0) / 6.0
} else if max == g {
((b - r) / delta + 2.0) / 6.0
} else {
((r - g) / delta + 4.0) / 6.0
};
Hsla {
h,
s,
l,
a: color.a,
}
}
}
impl JsonSchema for Hsla {
fn schema_name() -> Cow<'static, str> {
Rgba::schema_name()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
Rgba::json_schema(generator)
}
}
impl Serialize for Hsla {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Rgba::from(*self).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Hsla {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Rgba::deserialize(deserializer)?.into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
pub(crate) enum BackgroundTag {
Solid = 0,
LinearGradient = 1,
PatternSlash = 2,
}
/// A color space for color interpolation.
///
/// References:
/// - <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
/// - <https://www.w3.org/TR/css-color-4/#typedef-color-space>
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
pub enum ColorSpace {
#[default]
/// The sRGB color space.
Srgb = 0,
/// The Oklab color space.
Oklab = 1,
}
impl Display for ColorSpace {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ColorSpace::Srgb => write!(f, "sRGB"),
ColorSpace::Oklab => write!(f, "Oklab"),
}
}
}
/// A background color, which can be either a solid color or a linear gradient.
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
pub struct Background {
pub(crate) tag: BackgroundTag,
pub(crate) color_space: ColorSpace,
pub(crate) solid: Hsla,
pub(crate) gradient_angle_or_pattern_height: f32,
pub(crate) colors: [LinearColorStop; 2],
/// Padding for alignment for repr(C) layout.
pad: u32,
}
impl std::fmt::Debug for Background {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.tag {
BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
BackgroundTag::LinearGradient => {
write!(
f,
"LinearGradient({}, {:?}, {:?})",
self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
)
}
BackgroundTag::PatternSlash => {
write!(
f,
"PatternSlash({:?}, {})",
self.solid, self.gradient_angle_or_pattern_height
)
}
}
}
}
impl Eq for Background {}
impl Default for Background {
fn default() -> Self {
Self {
tag: BackgroundTag::Solid,
solid: Hsla::default(),
color_space: ColorSpace::default(),
gradient_angle_or_pattern_height: 0.0,
colors: [LinearColorStop::default(), LinearColorStop::default()],
pad: 0,
}
}
}
/// Creates a hash pattern background
pub fn pattern_slash(color: Hsla, width: f32, interval: f32) -> Background {
let width_scaled = (width * 255.0) as u32;
let interval_scaled = (interval * 255.0) as u32;
let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
Background {
tag: BackgroundTag::PatternSlash,
solid: color,
gradient_angle_or_pattern_height: height,
..Default::default()
}
}
/// Creates a solid background color.
pub fn solid_background(color: impl Into<Hsla>) -> Background {
Background {
solid: color.into(),
..Default::default()
}
}
/// Creates a LinearGradient background color.
///
/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there.
///
/// The `angle` is in degrees value in the range 0.0 to 360.0.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
pub fn linear_gradient(
angle: f32,
from: impl Into<LinearColorStop>,
to: impl Into<LinearColorStop>,
) -> Background {
Background {
tag: BackgroundTag::LinearGradient,
gradient_angle_or_pattern_height: angle,
colors: [from.into(), to.into()],
..Default::default()
}
}
/// A color stop in a linear gradient.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop>
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
pub struct LinearColorStop {
/// The color of the color stop.
pub color: Hsla,
/// The percentage of the gradient, in the range 0.0 to 1.0.
pub percentage: f32,
}
/// Creates a new linear color stop.
///
/// The percentage of the gradient, in the range 0.0 to 1.0.
pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
LinearColorStop {
color: color.into(),
percentage,
}
}
impl LinearColorStop {
/// Returns a new color stop with the same color, but with a modified alpha value.
pub fn opacity(&self, factor: f32) -> Self {
Self {
percentage: self.percentage,
color: self.color.opacity(factor),
}
}
}
impl Background {
/// Use specified color space for color interpolation.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
pub fn color_space(mut self, color_space: ColorSpace) -> Self {
self.color_space = color_space;
self
}
/// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
pub fn opacity(&self, factor: f32) -> Self {
let mut background = *self;
background.solid = background.solid.opacity(factor);
background.colors = [
self.colors[0].opacity(factor),
self.colors[1].opacity(factor),
];
background
}
/// Returns whether the background color is transparent.
pub fn is_transparent(&self) -> bool {
match self.tag {
BackgroundTag::Solid => self.solid.is_transparent(),
BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
BackgroundTag::PatternSlash => self.solid.is_transparent(),
}
}
}
impl From<Hsla> for Background {
fn from(value: Hsla) -> Self {
Background {
tag: BackgroundTag::Solid,
solid: value,
..Default::default()
}
}
}
impl From<Rgba> for Background {
fn from(value: Rgba) -> Self {
Background {
tag: BackgroundTag::Solid,
solid: Hsla::from(value),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_deserialize_three_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_four_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_six_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_eight_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
assert_eq!(actual, rgba(0xf5f5f5ff))
}
#[test]
fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
assert_eq!(actual, rgba(0xdeadbeef))
}
#[test]
fn test_background_solid() {
let color = Hsla::from(rgba(0xff0099ff));
let mut background = Background::from(color);
assert_eq!(background.tag, BackgroundTag::Solid);
assert_eq!(background.solid, color);
assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
assert!(!background.is_transparent());
background.solid = hsla(0.0, 0.0, 0.0, 0.0);
assert!(background.is_transparent());
}
#[test]
fn test_background_linear_gradient() {
let from = linear_color_stop(rgba(0xff0099ff), 0.0);
let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
let background = linear_gradient(90.0, from, to);
assert_eq!(background.tag, BackgroundTag::LinearGradient);
assert_eq!(background.colors[0], from);
assert_eq!(background.colors[1], to);
assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
assert!(!background.is_transparent());
assert!(background.opacity(0.0).is_transparent());
}
}
+122
View File
@@ -0,0 +1,122 @@
use crate::{App, Global, Rgba, Window, WindowAppearance, rgb};
use std::ops::Deref;
use std::sync::Arc;
/// The default set of colors for gpui.
///
/// These are used for styling base components, examples and more.
#[derive(Clone, Debug)]
pub struct Colors {
/// Text color
pub text: Rgba,
/// Selected text color
pub selected_text: Rgba,
/// Background color
pub background: Rgba,
/// Disabled color
pub disabled: Rgba,
/// Selected color
pub selected: Rgba,
/// Border color
pub border: Rgba,
/// Separator color
pub separator: Rgba,
/// Container color
pub container: Rgba,
}
impl Default for Colors {
fn default() -> Self {
Self::light()
}
}
impl Colors {
/// Returns the default colors for the given window appearance.
pub fn for_appearance(window: &Window) -> Self {
match window.appearance() {
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(),
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(),
}
}
/// Returns the default dark colors.
pub fn dark() -> Self {
Self {
text: rgb(0xffffff),
selected_text: rgb(0xffffff),
disabled: rgb(0x565656),
selected: rgb(0x2457ca),
background: rgb(0x222222),
border: rgb(0x000000),
separator: rgb(0xd9d9d9),
container: rgb(0x262626),
}
}
/// Returns the default light colors.
pub fn light() -> Self {
Self {
text: rgb(0x252525),
selected_text: rgb(0xffffff),
background: rgb(0xffffff),
disabled: rgb(0xb0b0b0),
selected: rgb(0x2a63d9),
border: rgb(0xd9d9d9),
separator: rgb(0xe6e6e6),
container: rgb(0xf4f5f5),
}
}
/// Get [Colors] from the global state
pub fn get_global(cx: &App) -> &Arc<Colors> {
&cx.global::<GlobalColors>().0
}
}
/// Get [Colors] from the global state
#[derive(Clone, Debug)]
pub struct GlobalColors(pub Arc<Colors>);
impl Deref for GlobalColors {
type Target = Arc<Colors>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Global for GlobalColors {}
/// Implement this trait to allow global [Colors] access via `cx.default_colors()`.
pub trait DefaultColors {
/// Returns the default [`Colors`]
fn default_colors(&self) -> &Arc<Colors>;
}
impl DefaultColors for App {
fn default_colors(&self) -> &Arc<Colors> {
&self.global::<GlobalColors>().0
}
}
/// The appearance of the base GPUI colors, used to style GPUI elements
///
/// Varies based on the system's current [`WindowAppearance`].
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum DefaultAppearance {
/// Use the set of colors for light appearances.
#[default]
Light,
/// Use the set of colors for dark appearances.
Dark,
}
impl From<WindowAppearance> for DefaultAppearance {
fn from(appearance: WindowAppearance) -> Self {
match appearance {
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
}
}
}
+769
View File
@@ -0,0 +1,769 @@
//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of
//! the contents of a window. Elements form a tree and are laid out according to the web layout
//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time,
//! you won't need to interact with this module or these APIs directly. Elements provide their
//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert
//! that element tree into the pixels you see on the screen.
//!
//! # Element Basics
//!
//! Elements are constructed by calling [`Render::render()`] on the root view of the window,
//! which recursively constructs the element tree from the current state of the application,.
//! These elements are then laid out by Taffy, and painted to the screen according to their own
//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element
//! tree and any callbacks they have registered with GPUI are dropped and the process repeats.
//!
//! But some state is too simple and voluminous to store in every view that needs it, e.g.
//! whether a hover has been started or not. For this, GPUI provides the [`Element::PrepaintState`], associated type.
//!
//! # Implementing your own elements
//!
//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding,
//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected
//! to stay in the bounds that their parent element gives them. But with [`Window::with_content_mask`],
//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays
//! and popups and anything else that shows up 'on top' of other elements.
//! With great power, comes great responsibility.
//!
//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of
//! elements that should cover most common use cases out of the box and it's recommended that you use those
//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement
//! elements when you need to take manual control of the layout and painting process, such as when using
//! your own custom layout algorithm or rendering a code editor.
use crate::{
App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ELEMENT_ARENA, ElementId,
FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window,
util::FluentBuilder,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{
any::{Any, type_name},
fmt::{self, Debug, Display},
mem, panic,
};
/// Implemented by types that participate in laying out and painting the contents of a window.
/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
/// You can create custom elements by implementing this trait, see the module-level documentation
/// for more details.
pub trait Element: 'static + IntoElement {
/// The type of state returned from [`Element::request_layout`]. A mutable reference to this state is subsequently
/// provided to [`Element::prepaint`] and [`Element::paint`].
type RequestLayoutState: 'static;
/// The type of state returned from [`Element::prepaint`]. A mutable reference to this state is subsequently
/// provided to [`Element::paint`].
type PrepaintState: 'static;
/// If this element has a unique identifier, return it here. This is used to track elements across frames, and
/// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods.
///
/// The global id can in turn be used to access state that's connected to an element with the same id across
/// frames. This id must be unique among children of the first containing element with an id.
fn id(&self) -> Option<ElementId>;
/// Source location where this element was constructed, used to disambiguate elements in the
/// inspector and navigate to their source code.
fn source_location(&self) -> Option<&'static panic::Location<'static>>;
/// Before an element can be painted, we need to know where it's going to be and how big it is.
/// Use this method to request a layout from Taffy and initialize the element's state.
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState);
/// After laying out an element, we need to commit its bounds to the current frame for hitbox
/// purposes. The state argument is the same state that was returned from [`Element::request_layout()`].
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState;
/// Once layout has been completed, this method will be called to paint the element to the screen.
/// The state argument is the same state that was returned from [`Element::request_layout()`].
fn paint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
);
/// Convert this element into a dynamically-typed [`AnyElement`].
fn into_any(self) -> AnyElement {
AnyElement::new(self)
}
}
/// Implemented by any type that can be converted into an element.
pub trait IntoElement: Sized {
/// The specific type of element into which the implementing type is converted.
/// Useful for converting other types into elements automatically, like Strings
type Element: Element;
/// Convert self into a type that implements [`Element`].
fn into_element(self) -> Self::Element;
/// Convert self into a dynamically-typed [`AnyElement`].
fn into_any_element(self) -> AnyElement {
self.into_element().into_any()
}
}
impl<T: IntoElement> FluentBuilder for T {}
/// An object that can be drawn to the screen. This is the trait that distinguishes "views" from
/// other entities. Views are `Entity`'s which `impl Render` and drawn to the screen.
pub trait Render: 'static + Sized {
/// Render this view into an element tree.
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement;
}
impl Render for Empty {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
Empty
}
}
/// You can derive [`IntoElement`] on any type that implements this trait.
/// It is used to construct reusable `components` out of plain data. Think of
/// components as a recipe for a certain pattern of elements. RenderOnce allows
/// you to invoke this pattern, without breaking the fluent builder pattern of
/// the element APIs.
pub trait RenderOnce: 'static {
/// Render this component into an element tree. Note that this method
/// takes ownership of self, as compared to [`Render::render()`] method
/// which takes a mutable reference.
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement;
}
/// This is a helper trait to provide a uniform interface for constructing elements that
/// can accept any number of any kind of child elements
pub trait ParentElement {
/// Extend this element's children with the given child elements.
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>);
/// Add a single child element to this element.
fn child(mut self, child: impl IntoElement) -> Self
where
Self: Sized,
{
self.extend(std::iter::once(child.into_element().into_any()));
self
}
/// Add multiple child elements to this element.
fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
where
Self: Sized,
{
self.extend(children.into_iter().map(|child| child.into_any_element()));
self
}
}
/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro
/// for [`RenderOnce`]
#[doc(hidden)]
pub struct Component<C: RenderOnce> {
component: Option<C>,
#[cfg(debug_assertions)]
source: &'static core::panic::Location<'static>,
}
impl<C: RenderOnce> Component<C> {
/// Create a new component from the given RenderOnce type.
#[track_caller]
pub fn new(component: C) -> Self {
Component {
component: Some(component),
#[cfg(debug_assertions)]
source: core::panic::Location::caller(),
}
}
}
impl<C: RenderOnce> Element for Component<C> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
#[cfg(debug_assertions)]
return Some(self.source);
#[cfg(not(debug_assertions))]
return None;
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
let mut element = self
.component
.take()
.unwrap()
.render(window, cx)
.into_any_element();
let layout_id = element.request_layout(window, cx);
(layout_id, element)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut AnyElement,
window: &mut Window,
cx: &mut App,
) {
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
element.prepaint(window, cx);
})
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
element.paint(window, cx);
})
}
}
impl<C: RenderOnce> IntoElement for Component<C> {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// A globally unique identifier for an element, used to track state across frames.
#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>);
impl Display for GlobalElementId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, element_id) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ".")?;
}
write!(f, "{}", element_id)?;
}
Ok(())
}
}
trait ElementObject {
fn inner_element(&mut self) -> &mut dyn Any;
fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId;
fn prepaint(&mut self, window: &mut Window, cx: &mut App);
fn paint(&mut self, window: &mut Window, cx: &mut App);
fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
window: &mut Window,
cx: &mut App,
) -> Size<Pixels>;
}
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
pub struct Drawable<E: Element> {
/// The drawn element.
pub element: E,
phase: ElementDrawPhase<E::RequestLayoutState, E::PrepaintState>,
}
#[derive(Default)]
enum ElementDrawPhase<RequestLayoutState, PrepaintState> {
#[default]
Start,
RequestLayout {
layout_id: LayoutId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
request_layout: RequestLayoutState,
},
LayoutComputed {
layout_id: LayoutId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
available_space: Size<AvailableSpace>,
request_layout: RequestLayoutState,
},
Prepaint {
node_id: DispatchNodeId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: RequestLayoutState,
prepaint: PrepaintState,
},
Painted,
}
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
impl<E: Element> Drawable<E> {
pub(crate) fn new(element: E) -> Self {
Drawable {
element,
phase: ElementDrawPhase::Start,
}
}
fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId {
match mem::take(&mut self.phase) {
ElementDrawPhase::Start => {
let global_id = self.element.id().map(|element_id| {
window.element_id_stack.push(element_id);
GlobalElementId(window.element_id_stack.clone())
});
let inspector_id;
#[cfg(any(feature = "inspector", debug_assertions))]
{
inspector_id = self.element.source_location().map(|source| {
let path = crate::InspectorElementPath {
global_id: GlobalElementId(window.element_id_stack.clone()),
source_location: source,
};
window.build_inspector_element_id(path)
});
}
#[cfg(not(any(feature = "inspector", debug_assertions)))]
{
inspector_id = None;
}
let (layout_id, request_layout) = self.element.request_layout(
global_id.as_ref(),
inspector_id.as_ref(),
window,
cx,
);
if global_id.is_some() {
window.element_id_stack.pop();
}
self.phase = ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
request_layout,
};
layout_id
}
_ => panic!("must call request_layout only once"),
}
}
pub(crate) fn prepaint(&mut self, window: &mut Window, cx: &mut App) {
match mem::take(&mut self.phase) {
ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
mut request_layout,
}
| ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
mut request_layout,
..
} => {
if let Some(element_id) = self.element.id() {
window.element_id_stack.push(element_id);
debug_assert_eq!(global_id.as_ref().unwrap().0, window.element_id_stack);
}
let bounds = window.layout_bounds(layout_id);
let node_id = window.next_frame.dispatch_tree.push_node();
let prepaint = self.element.prepaint(
global_id.as_ref(),
inspector_id.as_ref(),
bounds,
&mut request_layout,
window,
cx,
);
window.next_frame.dispatch_tree.pop_node();
if global_id.is_some() {
window.element_id_stack.pop();
}
self.phase = ElementDrawPhase::Prepaint {
node_id,
global_id,
inspector_id,
bounds,
request_layout,
prepaint,
};
}
_ => panic!("must call request_layout before prepaint"),
}
}
pub(crate) fn paint(
&mut self,
window: &mut Window,
cx: &mut App,
) -> (E::RequestLayoutState, E::PrepaintState) {
match mem::take(&mut self.phase) {
ElementDrawPhase::Prepaint {
node_id,
global_id,
inspector_id,
bounds,
mut request_layout,
mut prepaint,
..
} => {
if let Some(element_id) = self.element.id() {
window.element_id_stack.push(element_id);
debug_assert_eq!(global_id.as_ref().unwrap().0, window.element_id_stack);
}
window.next_frame.dispatch_tree.set_active_node(node_id);
self.element.paint(
global_id.as_ref(),
inspector_id.as_ref(),
bounds,
&mut request_layout,
&mut prepaint,
window,
cx,
);
if global_id.is_some() {
window.element_id_stack.pop();
}
self.phase = ElementDrawPhase::Painted;
(request_layout, prepaint)
}
_ => panic!("must call prepaint before paint"),
}
}
pub(crate) fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
window: &mut Window,
cx: &mut App,
) -> Size<Pixels> {
if matches!(&self.phase, ElementDrawPhase::Start) {
self.request_layout(window, cx);
}
let layout_id = match mem::take(&mut self.phase) {
ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
request_layout,
} => {
window.compute_layout(layout_id, available_space, cx);
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space,
request_layout,
};
layout_id
}
ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space: prev_available_space,
request_layout,
} => {
if available_space != prev_available_space {
window.compute_layout(layout_id, available_space, cx);
}
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space,
request_layout,
};
layout_id
}
_ => panic!("cannot measure after painting"),
};
window.layout_bounds(layout_id).size
}
}
impl<E> ElementObject for Drawable<E>
where
E: Element,
E::RequestLayoutState: 'static,
{
fn inner_element(&mut self) -> &mut dyn Any {
&mut self.element
}
fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId {
Drawable::request_layout(self, window, cx)
}
fn prepaint(&mut self, window: &mut Window, cx: &mut App) {
Drawable::prepaint(self, window, cx);
}
fn paint(&mut self, window: &mut Window, cx: &mut App) {
Drawable::paint(self, window, cx);
}
fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
window: &mut Window,
cx: &mut App,
) -> Size<Pixels> {
Drawable::layout_as_root(self, available_space, window, cx)
}
}
/// A dynamically typed element that can be used to store any element type.
pub struct AnyElement(ArenaBox<dyn ElementObject>);
impl AnyElement {
pub(crate) fn new<E>(element: E) -> Self
where
E: 'static + Element,
E::RequestLayoutState: Any,
{
let element = ELEMENT_ARENA
.with_borrow_mut(|arena| arena.alloc(|| Drawable::new(element)))
.map(|element| element as &mut dyn ElementObject);
AnyElement(element)
}
/// Attempt to downcast a reference to the boxed element to a specific type.
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.0.inner_element().downcast_mut::<T>()
}
/// Request the layout ID of the element stored in this `AnyElement`.
/// Used for laying out child elements in a parent element.
pub fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId {
self.0.request_layout(window, cx)
}
/// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and
/// request autoscroll before the final paint pass is confirmed.
pub fn prepaint(&mut self, window: &mut Window, cx: &mut App) -> Option<FocusHandle> {
let focus_assigned = window.next_frame.focus.is_some();
self.0.prepaint(window, cx);
if !focus_assigned && let Some(focus_id) = window.next_frame.focus {
return FocusHandle::for_id(focus_id, &cx.focus_handles);
}
None
}
/// Paints the element stored in this `AnyElement`.
pub fn paint(&mut self, window: &mut Window, cx: &mut App) {
self.0.paint(window, cx);
}
/// Performs layout for this element within the given available space and returns its size.
pub fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
window: &mut Window,
cx: &mut App,
) -> Size<Pixels> {
self.0.layout_as_root(available_space, window, cx)
}
/// Prepaints this element at the given absolute origin.
/// If any element in the subtree beneath this element is focused, its FocusHandle is returned.
pub fn prepaint_at(
&mut self,
origin: Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<FocusHandle> {
window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx))
}
/// Performs layout on this element in the available space, then prepaints it at the given absolute origin.
/// If any element in the subtree beneath this element is focused, its FocusHandle is returned.
pub fn prepaint_as_root(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
window: &mut Window,
cx: &mut App,
) -> Option<FocusHandle> {
self.layout_as_root(available_space, window, cx);
window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx))
}
}
impl Element for AnyElement {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let layout_id = self.request_layout(window, cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) {
self.prepaint(window, cx);
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.paint(window, cx);
}
}
impl IntoElement for AnyElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
fn into_any_element(self) -> AnyElement {
self
}
}
/// The empty element, which renders nothing.
pub struct Empty;
impl IntoElement for Empty {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for Empty {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
(window.request_layout(Style::default(), None, cx), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_state: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) {
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
_window: &mut Window,
_cx: &mut App,
) {
}
}
+292
View File
@@ -0,0 +1,292 @@
use smallvec::SmallVec;
use crate::{
AnyElement, App, Axis, Bounds, Corner, Display, Edges, Element, GlobalElementId,
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
Window, point, px,
};
/// The state that the anchored element element uses to track its children.
pub struct AnchoredState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
/// An anchored element that can be used to display UI that
/// will avoid overflowing the window bounds.
pub struct Anchored {
children: SmallVec<[AnyElement; 2]>,
anchor_corner: Corner,
fit_mode: AnchoredFitMode,
anchor_position: Option<Point<Pixels>>,
position_mode: AnchoredPositionMode,
offset: Option<Point<Pixels>>,
}
/// anchored gives you an element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues.
pub fn anchored() -> Anchored {
Anchored {
children: SmallVec::new(),
anchor_corner: Corner::TopLeft,
fit_mode: AnchoredFitMode::SwitchAnchor,
anchor_position: None,
position_mode: AnchoredPositionMode::Window,
offset: None,
}
}
impl Anchored {
/// Sets which corner of the anchored element should be anchored to the current position.
pub fn anchor(mut self, anchor: Corner) -> Self {
self.anchor_corner = anchor;
self
}
/// Sets the position in window coordinates
/// (otherwise the location the anchored element is rendered is used)
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
self.anchor_position = Some(anchor);
self
}
/// Offset the final position by this amount.
/// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu.
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
self.offset = Some(offset);
self
}
/// Sets the position mode for this anchored element. Local will have this
/// interpret its [`Anchored::position`] as relative to the parent element.
/// While Window will have it interpret the position as relative to the window.
pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
self.position_mode = mode;
self
}
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
pub fn snap_to_window(mut self) -> Self {
self.fit_mode = AnchoredFitMode::SnapToWindow;
self
}
/// Snap to window edge and leave some margins.
pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
self
}
}
impl ParentElement for Anchored {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl Element for Anchored {
type RequestLayoutState = AnchoredState;
type PrepaintState = ();
fn id(&self) -> Option<crate::ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
let child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(window, cx))
.collect::<SmallVec<_>>();
let anchored_style = Style {
position: Position::Absolute,
display: Display::Flex,
..Style::default()
};
let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx);
(layout_id, AnchoredState { child_layout_ids })
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) {
if request_layout.child_layout_ids.is_empty() {
return;
}
let mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
for child_layout_id in &request_layout.child_layout_ids {
let child_bounds = window.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.bottom_right());
}
let size: Size<Pixels> = (child_max - child_min).into();
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
self.anchor_position,
self.anchor_corner,
size,
bounds,
self.offset,
);
let limits = Bounds {
origin: Point::default(),
size: window.viewport_size(),
};
if self.fit_mode == AnchoredFitMode::SwitchAnchor {
let mut anchor_corner = self.anchor_corner;
if desired.left() < limits.left() || desired.right() > limits.right() {
let switched = Bounds::from_corner_and_size(
anchor_corner.other_side_corner_along(Axis::Horizontal),
origin,
size,
);
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal);
desired = switched
}
}
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
let switched = Bounds::from_corner_and_size(
anchor_corner.other_side_corner_along(Axis::Vertical),
origin,
size,
);
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
desired = switched;
}
}
}
let client_inset = window.client_inset.unwrap_or(px(0.));
let edges = match self.fit_mode {
AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
_ => Edges::default(),
}
.map(|edge| *edge + client_inset);
// Snap the horizontal edges of the anchored element to the horizontal edges of the window if
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
if desired.right() > limits.right() {
desired.origin.x -= desired.right() - limits.right() + edges.right;
}
if desired.left() < limits.left() {
desired.origin.x = limits.origin.x + edges.left;
}
// Snap the vertical edges of the anchored element to the vertical edges of the window if
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
if desired.bottom() > limits.bottom() {
desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
}
if desired.top() < limits.top() {
desired.origin.y = limits.origin.y + edges.top;
}
let offset = desired.origin - bounds.origin;
let offset = point(offset.x.round(), offset.y.round());
window.with_element_offset(offset, |window| {
for child in &mut self.children {
child.prepaint(window, cx);
}
})
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
for child in &mut self.children {
child.paint(window, cx);
}
}
}
impl IntoElement for Anchored {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// Which algorithm to use when fitting the anchored element to be inside the window.
#[derive(Copy, Clone, PartialEq)]
pub enum AnchoredFitMode {
/// Snap the anchored element to the window edge.
SnapToWindow,
/// Snap to window edge and leave some margins.
SnapToWindowWithMargin(Edges<Pixels>),
/// Switch which corner anchor this anchored element is attached to.
SwitchAnchor,
}
/// Which algorithm to use when positioning the anchored element.
#[derive(Copy, Clone, PartialEq)]
pub enum AnchoredPositionMode {
/// Position the anchored element relative to the window.
Window,
/// Position the anchored element relative to its parent.
Local,
}
impl AnchoredPositionMode {
fn get_position_and_bounds(
&self,
anchor_position: Option<Point<Pixels>>,
anchor_corner: Corner,
size: Size<Pixels>,
bounds: Bounds<Pixels>,
offset: Option<Point<Pixels>>,
) -> (Point<Pixels>, Bounds<Pixels>) {
let offset = offset.unwrap_or_default();
match self {
AnchoredPositionMode::Window => {
let anchor_position = anchor_position.unwrap_or(bounds.origin);
let bounds =
Bounds::from_corner_and_size(anchor_corner, anchor_position + offset, size);
(anchor_position, bounds)
}
AnchoredPositionMode::Local => {
let anchor_position = anchor_position.unwrap_or_default();
let bounds = Bounds::from_corner_and_size(
anchor_corner,
bounds.origin + anchor_position + offset,
size,
);
(anchor_position, bounds)
}
}
}
}
+263
View File
@@ -0,0 +1,263 @@
use std::{
rc::Rc,
time::{Duration, Instant},
};
use crate::{
AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window,
};
pub use easing::*;
use smallvec::SmallVec;
/// An animation that can be applied to an element.
#[derive(Clone)]
pub struct Animation {
/// The amount of time for which this animation should run
pub duration: Duration,
/// Whether to repeat this animation when it finishes
pub oneshot: bool,
/// A function that takes a delta between 0 and 1 and returns a new delta
/// between 0 and 1 based on the given easing function.
pub easing: Rc<dyn Fn(f32) -> f32>,
}
impl Animation {
/// Create a new animation with the given duration.
/// By default the animation will only run once and will use a linear easing function.
pub fn new(duration: Duration) -> Self {
Self {
duration,
oneshot: true,
easing: Rc::new(linear),
}
}
/// Set the animation to loop when it finishes.
pub fn repeat(mut self) -> Self {
self.oneshot = false;
self
}
/// Set the easing function to use for this animation.
/// The easing function will take a time delta between 0 and 1 and return a new delta
/// between 0 and 1
pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
self.easing = Rc::new(easing);
self
}
}
/// An extension trait for adding the animation wrapper to both Elements and Components
pub trait AnimationExt {
/// Render this component or element with an animation
fn with_animation(
self,
id: impl Into<ElementId>,
animation: Animation,
animator: impl Fn(Self, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(move |this, _, value| animator(this, value)),
animations: smallvec::smallvec![animation],
}
}
/// Render this component or element with a chain of animations
fn with_animations(
self,
id: impl Into<ElementId>,
animations: Vec<Animation>,
animator: impl Fn(Self, usize, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(animator),
animations: animations.into(),
}
}
}
impl<E: IntoElement + 'static> AnimationExt for E {}
/// A GPUI element that applies an animation to another element
pub struct AnimationElement<E> {
id: ElementId,
element: Option<E>,
animations: SmallVec<[Animation; 1]>,
animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
}
impl<E> AnimationElement<E> {
/// Returns a new [`AnimationElement<E>`] after applying the given function
/// to the element being animated.
pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
self.element = self.element.map(f);
self
}
}
impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
type Element = AnimationElement<E>;
fn into_element(self) -> Self::Element {
self
}
}
struct AnimationState {
start: Instant,
animation_ix: usize,
}
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
window.with_element_state(global_id.unwrap(), |state, window| {
let mut state = state.unwrap_or_else(|| AnimationState {
start: Instant::now(),
animation_ix: 0,
});
let animation_ix = state.animation_ix;
let mut delta = state.start.elapsed().as_secs_f32()
/ self.animations[animation_ix].duration.as_secs_f32();
let mut done = false;
if delta > 1.0 {
if self.animations[animation_ix].oneshot {
if animation_ix >= self.animations.len() - 1 {
done = true;
} else {
state.start = Instant::now();
state.animation_ix += 1;
}
delta = 1.0;
} else {
delta %= 1.0;
}
}
let delta = (self.animations[animation_ix].easing)(delta);
debug_assert!(
(0.0..=1.0).contains(&delta),
"delta should always be between 0 and 1"
);
let element = self.element.take().expect("should only be called once");
let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
if !done {
window.request_animation_frame();
}
((element.request_layout(window, cx), element), state)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
element.prepaint(window, cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx);
}
}
mod easing {
use std::f32::consts::PI;
/// The linear easing function, or delta itself
pub fn linear(delta: f32) -> f32 {
delta
}
/// The quadratic easing function, delta * delta
pub fn quadratic(delta: f32) -> f32 {
delta * delta
}
/// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
pub fn ease_in_out(delta: f32) -> f32 {
if delta < 0.5 {
2.0 * delta * delta
} else {
let x = -2.0 * delta + 2.0;
1.0 - x * x / 2.0
}
}
/// The Quint ease-out function, which starts quickly and decelerates to a stop
pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
move |delta| 1.0 - (1.0 - delta).powi(5)
}
/// Apply the given easing function, first in the forward direction and then in the reverse direction
pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
move |delta| {
if delta < 0.5 {
easing(delta * 2.0)
} else {
easing((1.0 - delta) * 2.0)
}
}
}
/// A custom easing function for pulsating alpha that slows down as it approaches 0.1
pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
let range = max - min;
move |delta| {
// Use a combination of sine and cubic functions for a more natural breathing rhythm
let t = (delta * 2.0 * PI).sin();
let breath = (t * t * t + t) / 2.0;
// Map the breath to our desired alpha range
let normalized_alpha = (breath + 1.0) / 2.0;
min + (normalized_alpha * range)
}
}
}
+95
View File
@@ -0,0 +1,95 @@
use refineable::Refineable as _;
use crate::{
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Pixels,
Style, StyleRefinement, Styled, Window,
};
/// Construct a canvas element with the given paint callback.
/// Useful for adding short term custom drawing to a view.
pub fn canvas<T>(
prepaint: impl 'static + FnOnce(Bounds<Pixels>, &mut Window, &mut App) -> T,
paint: impl 'static + FnOnce(Bounds<Pixels>, T, &mut Window, &mut App),
) -> Canvas<T> {
Canvas {
prepaint: Some(Box::new(prepaint)),
paint: Some(Box::new(paint)),
style: StyleRefinement::default(),
}
}
/// A canvas element, meant for accessing the low level paint API without defining a whole
/// custom element
pub struct Canvas<T> {
prepaint: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut Window, &mut App) -> T>>,
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, T, &mut Window, &mut App)>>,
style: StyleRefinement,
}
impl<T: 'static> IntoElement for Canvas<T> {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl<T: 'static> Element for Canvas<T> {
type RequestLayoutState = Style;
type PrepaintState = Option<T>;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = window.request_layout(style.clone(), [], cx);
(layout_id, style)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Style,
window: &mut Window,
cx: &mut App,
) -> Option<T> {
Some(self.prepaint.take().unwrap()(bounds, window, cx))
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
style: &mut Style,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let prepaint = prepaint.take().unwrap();
style.paint(bounds, window, cx, |window, cx| {
(self.paint.take().unwrap())(bounds, prepaint, window, cx)
});
}
}
impl<T> Styled for Canvas<T> {
fn style(&mut self) -> &mut crate::StyleRefinement {
&mut self.style
}
}
+96
View File
@@ -0,0 +1,96 @@
use crate::{
AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
Pixels, Window,
};
/// Builds a `Deferred` element, which delays the layout and paint of its child.
pub fn deferred(child: impl IntoElement) -> Deferred {
Deferred {
child: Some(child.into_any_element()),
priority: 0,
}
}
/// An element which delays the painting of its child until after all of
/// its ancestors, while keeping its layout as part of the current element tree.
pub struct Deferred {
child: Option<AnyElement>,
priority: usize,
}
impl Deferred {
/// Sets the `priority` value of the `deferred` element, which
/// determines the drawing order relative to other deferred elements,
/// with higher values being drawn on top.
pub fn with_priority(mut self, priority: usize) -> Self {
self.priority = priority;
self
}
}
impl Element for Deferred {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<crate::ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, ()) {
let layout_id = self.child.as_mut().unwrap().request_layout(window, cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
_cx: &mut App,
) {
let child = self.child.take().unwrap();
let element_offset = window.element_offset();
window.defer_draw(child, element_offset, self.priority)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
_window: &mut Window,
_cx: &mut App,
) {
}
}
impl IntoElement for Deferred {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Deferred {
/// Sets a priority for the element. A higher priority conceptually means painting the element
/// on top of deferred draws with a lower priority (i.e. closer to the viewer).
pub fn priority(mut self, priority: usize) -> Self {
self.priority = priority;
self
}
}
+3250
View File
File diff suppressed because it is too large Load Diff
+353
View File
@@ -0,0 +1,353 @@
use crate::{
AnyElement, AnyEntity, App, AppContext, Asset, AssetLogger, Bounds, Element, ElementId, Entity,
GlobalElementId, ImageAssetLoader, ImageCacheError, InspectorElementId, IntoElement, LayoutId,
ParentElement, Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Task, Window,
hash,
};
use futures::{FutureExt, future::Shared};
use refineable::Refineable;
use smallvec::SmallVec;
use std::{collections::HashMap, fmt, sync::Arc};
/// An image cache element, all its child img elements will use the cache specified by this element.
/// Note that this could as simple as passing an `Entity<T: ImageCache>`
pub fn image_cache(image_cache_provider: impl ImageCacheProvider) -> ImageCacheElement {
ImageCacheElement {
image_cache_provider: Box::new(image_cache_provider),
style: StyleRefinement::default(),
children: SmallVec::default(),
}
}
/// A dynamically typed image cache, which can be used to store any image cache
#[derive(Clone)]
pub struct AnyImageCache {
image_cache: AnyEntity,
load_fn: fn(
image_cache: &AnyEntity,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>>,
}
impl<I: ImageCache> From<Entity<I>> for AnyImageCache {
fn from(image_cache: Entity<I>) -> Self {
Self {
image_cache: image_cache.into_any(),
load_fn: any_image_cache::load::<I>,
}
}
}
impl AnyImageCache {
/// Load an image given a resource
/// returns the result of loading the image if it has finished loading, or None if it is still loading
pub fn load(
&self,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
(self.load_fn)(&self.image_cache, resource, window, cx)
}
}
mod any_image_cache {
use super::*;
pub(crate) fn load<I: 'static + ImageCache>(
image_cache: &AnyEntity,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
let image_cache = image_cache.clone().downcast::<I>().unwrap();
image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx))
}
}
/// An image cache element.
pub struct ImageCacheElement {
image_cache_provider: Box<dyn ImageCacheProvider>,
style: StyleRefinement,
children: SmallVec<[AnyElement; 2]>,
}
impl ParentElement for ImageCacheElement {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl Styled for ImageCacheElement {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl IntoElement for ImageCacheElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for ImageCacheElement {
type RequestLayoutState = SmallVec<[LayoutId; 4]>;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let image_cache = self.image_cache_provider.provide(window, cx);
window.with_image_cache(Some(image_cache), |window| {
let child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(window, cx))
.collect::<SmallVec<_>>();
let mut style = Style::default();
style.refine(&self.style);
let layout_id = window.request_layout(style, child_layout_ids.iter().copied(), cx);
(layout_id, child_layout_ids)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
for child in &mut self.children {
child.prepaint(window, cx);
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let image_cache = self.image_cache_provider.provide(window, cx);
window.with_image_cache(Some(image_cache), |window| {
for child in &mut self.children {
child.paint(window, cx);
}
})
}
}
/// An image loading task associated with an image cache.
pub type ImageLoadingTask = Shared<Task<Result<Arc<RenderImage>, ImageCacheError>>>;
/// An image cache item
pub enum ImageCacheItem {
/// The associated image is currently loading
Loading(ImageLoadingTask),
/// This item has loaded an image.
Loaded(Result<Arc<RenderImage>, ImageCacheError>),
}
impl std::fmt::Debug for ImageCacheItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = match self {
ImageCacheItem::Loading(_) => &"Loading...".to_string(),
ImageCacheItem::Loaded(render_image) => &format!("{:?}", render_image),
};
f.debug_struct("ImageCacheItem")
.field("status", status)
.finish()
}
}
impl ImageCacheItem {
/// Attempt to get the image from the cache item.
pub fn get(&mut self) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageCacheItem::Loading(task) => {
let res = task.now_or_never()?;
*self = ImageCacheItem::Loaded(res.clone());
Some(res)
}
ImageCacheItem::Loaded(res) => Some(res.clone()),
}
}
}
/// An object that can handle the caching and unloading of images.
/// Implementations of this trait should ensure that images are removed from all windows when they are no longer needed.
pub trait ImageCache: 'static {
/// Load an image given a resource
/// returns the result of loading the image if it has finished loading, or None if it is still loading
fn load(
&mut self,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>>;
}
/// An object that can create an ImageCache during the render phase.
/// See the ImageCache trait for more information.
pub trait ImageCacheProvider: 'static {
/// Called during the request_layout phase to create an ImageCache.
fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache;
}
impl<T: ImageCache> ImageCacheProvider for Entity<T> {
fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache {
self.clone().into()
}
}
/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full
pub struct RetainAllImageCache(HashMap<u64, ImageCacheItem>);
impl fmt::Debug for RetainAllImageCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HashMapImageCache")
.field("num_images", &self.0.len())
.finish()
}
}
impl RetainAllImageCache {
/// Create a new image cache.
#[inline]
pub fn new(cx: &mut App) -> Entity<Self> {
let e = cx.new(|_cx| RetainAllImageCache(HashMap::new()));
cx.observe_release(&e, |image_cache, cx| {
for (_, mut item) in std::mem::replace(&mut image_cache.0, HashMap::new()) {
if let Some(Ok(image)) = item.get() {
cx.drop_image(image, None);
}
}
})
.detach();
e
}
/// Load an image from the given source.
///
/// Returns `None` if the image is loading.
pub fn load(
&mut self,
source: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
let hash = hash(source);
if let Some(item) = self.0.get_mut(&hash) {
return item.get();
}
let fut = AssetLogger::<ImageAssetLoader>::load(source.clone(), cx);
let task = cx.background_executor().spawn(fut).shared();
self.0.insert(hash, ImageCacheItem::Loading(task.clone()));
let entity = window.current_view();
window
.spawn(cx, {
async move |cx| {
_ = task.await;
cx.on_next_frame(move |_, cx| {
cx.notify(entity);
});
}
})
.detach();
None
}
/// Clear the image cache.
pub fn clear(&mut self, window: &mut Window, cx: &mut App) {
for (_, mut item) in std::mem::replace(&mut self.0, HashMap::new()) {
if let Some(Ok(image)) = item.get() {
cx.drop_image(image, Some(window));
}
}
}
/// Remove the image from the cache by the given source.
pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) {
let hash = hash(source);
if let Some(mut item) = self.0.remove(&hash)
&& let Some(Ok(image)) = item.get()
{
cx.drop_image(image, Some(window));
}
}
/// Returns the number of images in the cache.
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns true if the cache is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl ImageCache for RetainAllImageCache {
fn load(
&mut self,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
RetainAllImageCache::load(self, resource, window, cx)
}
}
/// Constructs a retain-all image cache that uses the element state associated with the given ID.
pub fn retain_all(id: impl Into<ElementId>) -> RetainAllImageCacheProvider {
RetainAllImageCacheProvider { id: id.into() }
}
/// A provider struct for creating a retain-all image cache inline
pub struct RetainAllImageCacheProvider {
id: ElementId,
}
impl ImageCacheProvider for RetainAllImageCacheProvider {
fn provide(&mut self, window: &mut Window, cx: &mut App) -> AnyImageCache {
window
.with_global_id(self.id.clone(), |global_id, window| {
window.with_element_state::<Entity<RetainAllImageCache>, _>(
global_id,
|cache, _window| {
let mut cache = cache.unwrap_or_else(|| RetainAllImageCache::new(cx));
(cache.clone(), cache)
},
)
})
.into()
}
}
+767
View File
@@ -0,0 +1,767 @@
use crate::{
AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
Window, px, swap_rgba_pa_to_bgra,
};
use anyhow::{Context as _, Result};
use futures::{AsyncReadExt, Future};
use image::{
AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba,
codecs::{gif::GifDecoder, webp::WebPDecoder},
};
use smallvec::SmallVec;
use std::{
fs,
io::{self, Cursor},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
use thiserror::Error;
use util::ResultExt;
use super::{Stateful, StatefulInteractiveElement};
/// The delay before showing the loading state.
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
/// A type alias to the resource loader that the `img()` element uses.
///
/// Note: that this is only for Resources, like URLs or file paths.
/// Custom loaders, or external images will not use this asset loader
pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
/// A source of image content.
#[derive(Clone)]
pub enum ImageSource {
/// The image content will be loaded from some resource location
Resource(Resource),
/// Cached image data
Render(Arc<RenderImage>),
/// Cached image data
Image(Arc<Image>),
/// A custom loading function to use
Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
}
fn is_uri(uri: &str) -> bool {
http_client::Uri::from_str(uri).is_ok()
}
impl From<SharedUri> for ImageSource {
fn from(value: SharedUri) -> Self {
Self::Resource(Resource::Uri(value))
}
}
impl<'a> From<&'a str> for ImageSource {
fn from(s: &'a str) -> Self {
if is_uri(s) {
Self::Resource(Resource::Uri(s.to_string().into()))
} else {
Self::Resource(Resource::Embedded(s.to_string().into()))
}
}
}
impl From<String> for ImageSource {
fn from(s: String) -> Self {
if is_uri(&s) {
Self::Resource(Resource::Uri(s.into()))
} else {
Self::Resource(Resource::Embedded(s.into()))
}
}
}
impl From<SharedString> for ImageSource {
fn from(s: SharedString) -> Self {
s.as_ref().into()
}
}
impl From<&Path> for ImageSource {
fn from(value: &Path) -> Self {
Self::Resource(value.to_path_buf().into())
}
}
impl From<Arc<Path>> for ImageSource {
fn from(value: Arc<Path>) -> Self {
Self::Resource(value.into())
}
}
impl From<PathBuf> for ImageSource {
fn from(value: PathBuf) -> Self {
Self::Resource(value.into())
}
}
impl From<Arc<RenderImage>> for ImageSource {
fn from(value: Arc<RenderImage>) -> Self {
Self::Render(value)
}
}
impl From<Arc<Image>> for ImageSource {
fn from(value: Arc<Image>) -> Self {
Self::Image(value)
}
}
impl<F> From<F> for ImageSource
where
F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
{
fn from(value: F) -> Self {
Self::Custom(Arc::new(value))
}
}
/// The style of an image element.
pub struct ImageStyle {
grayscale: bool,
object_fit: ObjectFit,
loading: Option<Box<dyn Fn() -> AnyElement>>,
fallback: Option<Box<dyn Fn() -> AnyElement>>,
}
impl Default for ImageStyle {
fn default() -> Self {
Self {
grayscale: false,
object_fit: ObjectFit::Contain,
loading: None,
fallback: None,
}
}
}
/// Style an image element.
pub trait StyledImage: Sized {
/// Get a mutable [ImageStyle] from the element.
fn image_style(&mut self) -> &mut ImageStyle;
/// Set the image to be displayed in grayscale.
fn grayscale(mut self, grayscale: bool) -> Self {
self.image_style().grayscale = grayscale;
self
}
/// Set the object fit for the image.
fn object_fit(mut self, object_fit: ObjectFit) -> Self {
self.image_style().object_fit = object_fit;
self
}
/// Set the object fit for the image.
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().fallback = Some(Box::new(fallback));
self
}
/// Set the object fit for the image.
fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().loading = Some(Box::new(loading));
self
}
}
impl StyledImage for Img {
fn image_style(&mut self) -> &mut ImageStyle {
&mut self.style
}
}
impl StyledImage for Stateful<Img> {
fn image_style(&mut self) -> &mut ImageStyle {
&mut self.element.style
}
}
/// An image element.
pub struct Img {
interactivity: Interactivity,
source: ImageSource,
style: ImageStyle,
image_cache: Option<AnyImageCache>,
}
/// Create a new image element.
#[track_caller]
pub fn img(source: impl Into<ImageSource>) -> Img {
Img {
interactivity: Interactivity::new(),
source: source.into(),
style: ImageStyle::default(),
image_cache: None,
}
}
impl Img {
/// A list of all format extensions currently supported by this img element
pub fn extensions() -> &'static [&'static str] {
// This is the list in [image::ImageFormat::from_extension] + `svg`
&[
"avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
]
}
/// Sets the image cache for the current node.
///
/// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
///
/// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used.
/// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
///
/// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
/// while ensuring a default behavior when no cache is explicitly specified.
#[inline]
pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
Self {
image_cache: Some(image_cache.clone().into()),
..self
}
}
}
impl Deref for Stateful<Img> {
type Target = Img;
fn deref(&self) -> &Self::Target {
&self.element
}
}
impl DerefMut for Stateful<Img> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.element
}
}
/// The image state between frames
struct ImgState {
frame_index: usize,
last_frame_time: Option<Instant>,
started_loading: Option<(Instant, Task<()>)>,
}
/// The image layout state between frames
pub struct ImgLayoutState {
frame_index: usize,
replacement: Option<AnyElement>,
}
impl Element for Img {
type RequestLayoutState = ImgLayoutState;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
self.interactivity.source_location()
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut layout_state = ImgLayoutState {
frame_index: 0,
replacement: None,
};
window.with_optional_element_state(global_id, |state, window| {
let mut state = state.map(|state| {
state.unwrap_or(ImgState {
frame_index: 0,
last_frame_time: None,
started_loading: None,
})
});
let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
let layout_id = self.interactivity.request_layout(
global_id,
inspector_id,
window,
cx,
|mut style, window, cx| {
let mut replacement_id = None;
match self.source.use_data(
self.image_cache
.clone()
.or_else(|| window.image_cache_stack.last().cloned()),
window,
cx,
) {
Some(Ok(data)) => {
if let Some(state) = &mut state {
let frame_count = data.frame_count();
if frame_count > 1 {
let current_time = Instant::now();
if let Some(last_frame_time) = state.last_frame_time {
let elapsed = current_time - last_frame_time;
let frame_duration =
Duration::from(data.delay(state.frame_index));
if elapsed >= frame_duration {
state.frame_index =
(state.frame_index + 1) % frame_count;
state.last_frame_time =
Some(current_time - (elapsed - frame_duration));
}
} else {
state.last_frame_time = Some(current_time);
}
}
state.started_loading = None;
}
let image_size = data.render_size(frame_index);
style.aspect_ratio = Some(image_size.width / image_size.height);
if let Length::Auto = style.size.width {
style.size.width = match style.size.height {
Length::Definite(DefiniteLength::Absolute(abs_length)) => {
let height_px = abs_length.to_pixels(window.rem_size());
Length::Definite(
px(image_size.width.0 * height_px.0
/ image_size.height.0)
.into(),
)
}
_ => Length::Definite(image_size.width.into()),
};
}
if let Length::Auto = style.size.height {
style.size.height = match style.size.width {
Length::Definite(DefiniteLength::Absolute(abs_length)) => {
let width_px = abs_length.to_pixels(window.rem_size());
Length::Definite(
px(image_size.height.0 * width_px.0
/ image_size.width.0)
.into(),
)
}
_ => Length::Definite(image_size.height.into()),
};
}
if global_id.is_some() && data.frame_count() > 1 {
window.request_animation_frame();
}
}
Some(_err) => {
if let Some(fallback) = self.style.fallback.as_ref() {
let mut element = fallback();
replacement_id = Some(element.request_layout(window, cx));
layout_state.replacement = Some(element);
}
if let Some(state) = &mut state {
state.started_loading = None;
}
}
None => {
if let Some(state) = &mut state {
if let Some((started_loading, _)) = state.started_loading {
if started_loading.elapsed() > LOADING_DELAY
&& let Some(loading) = self.style.loading.as_ref()
{
let mut element = loading();
replacement_id = Some(element.request_layout(window, cx));
layout_state.replacement = Some(element);
}
} else {
let current_view = window.current_view();
let task = window.spawn(cx, async move |cx| {
cx.background_executor().timer(LOADING_DELAY).await;
cx.update(move |_, cx| {
cx.notify(current_view);
})
.ok();
});
state.started_loading = Some((Instant::now(), task));
}
}
}
}
window.request_layout(style, replacement_id, cx)
},
);
layout_state.frame_index = frame_index;
((layout_id, layout_state), state)
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
bounds.size,
window,
cx,
|_, _, hitbox, window, cx| {
if let Some(replacement) = &mut request_layout.replacement {
replacement.prepaint(window, cx);
}
hitbox
},
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let source = self.source.clone();
self.interactivity.paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|style, window, cx| {
if let Some(Ok(data)) = source.use_data(
self.image_cache
.clone()
.or_else(|| window.image_cache_stack.last().cloned()),
window,
cx,
) {
let new_bounds = self
.style
.object_fit
.get_bounds(bounds, data.size(layout_state.frame_index));
let corner_radii = style
.corner_radii
.to_pixels(window.rem_size())
.clamp_radii_for_quad_size(new_bounds.size);
window
.paint_image(
new_bounds,
corner_radii,
data,
layout_state.frame_index,
self.style.grayscale,
)
.log_err();
} else if let Some(replacement) = &mut layout_state.replacement {
replacement.paint(window, cx);
}
},
)
}
}
impl Styled for Img {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl InteractiveElement for Img {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
impl IntoElement for Img {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl StatefulInteractiveElement for Img {}
impl ImageSource {
pub(crate) fn use_data(
&self,
cache: Option<AnyImageCache>,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageSource::Resource(resource) => {
if let Some(cache) = cache {
cache.load(resource, window, cx)
} else {
window.use_asset::<ImgResourceLoader>(resource, cx)
}
}
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
ImageSource::Render(data) => Some(Ok(data.to_owned())),
ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
}
}
pub(crate) fn get_data(
&self,
cache: Option<AnyImageCache>,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageSource::Resource(resource) => {
if let Some(cache) = cache {
cache.load(resource, window, cx)
} else {
window.get_asset::<ImgResourceLoader>(resource, cx)
}
}
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
ImageSource::Render(data) => Some(Ok(data.to_owned())),
ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
}
}
/// Remove this image source from the asset system
pub fn remove_asset(&self, cx: &mut App) {
match self {
ImageSource::Resource(resource) => {
cx.remove_asset::<ImgResourceLoader>(resource);
}
ImageSource::Custom(_) | ImageSource::Render(_) => {}
ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
}
}
}
#[derive(Clone)]
enum ImageDecoder {}
impl Asset for ImageDecoder {
type Source = Arc<Image>;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
let renderer = cx.svg_renderer();
async move { source.to_image_data(renderer).map_err(Into::into) }
}
}
/// An image loader for the GPUI asset system
#[derive(Clone)]
pub enum ImageAssetLoader {}
impl Asset for ImageAssetLoader {
type Source = Resource;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
let client = cx.http_client();
// TODO: Can we make SVGs always rescale?
// let scale_factor = cx.scale_factor();
let svg_renderer = cx.svg_renderer();
let asset_source = cx.asset_source().clone();
async move {
let bytes = match source.clone() {
Resource::Path(uri) => fs::read(uri.as_ref())?,
Resource::Uri(uri) => {
let mut response = client
.get(uri.as_ref(), ().into(), true)
.await
.with_context(|| format!("loading image asset from {uri:?}"))?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() {
let mut body = String::from_utf8_lossy(&body).into_owned();
let first_line = body.lines().next().unwrap_or("").trim_end();
body.truncate(first_line.len());
return Err(ImageCacheError::BadStatus {
uri,
status: response.status(),
body,
});
}
body
}
Resource::Embedded(path) => {
let data = asset_source.load(&path).ok().flatten();
if let Some(data) = data {
data.to_vec()
} else {
return Err(ImageCacheError::Asset(
format!("Embedded resource not found: {}", path).into(),
));
}
}
};
let data = if let Ok(format) = image::guess_format(&bytes) {
let data = match format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
let mut frames = SmallVec::new();
for frame in decoder.into_frames() {
let mut frame = frame?;
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
frames
}
ImageFormat::WebP => {
let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
if decoder.has_animation() {
let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
let mut frames = SmallVec::new();
for frame in decoder.into_frames() {
let mut frame = frame?;
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
frames
} else {
let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
}
_ => {
let mut data =
image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
};
RenderImage::new(data)
} else {
let pixmap =
// TODO: Can we make svgs always rescale?
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
let mut buffer =
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
for pixel in buffer.chunks_exact_mut(4) {
swap_rgba_pa_to_bgra(pixel);
}
let mut image = RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1));
image.scale_factor = SMOOTH_SVG_SCALE_FACTOR;
image
};
Ok(Arc::new(data))
}
}
}
/// An error that can occur when interacting with the image cache.
#[derive(Debug, Error, Clone)]
pub enum ImageCacheError {
/// Some other kind of error occurred
#[error("error: {0}")]
Other(#[from] Arc<anyhow::Error>),
/// An error that occurred while reading the image from disk.
#[error("IO error: {0}")]
Io(Arc<std::io::Error>),
/// An error that occurred while processing an image.
#[error("unexpected http status for {uri}: {status}, body: {body}")]
BadStatus {
/// The URI of the image.
uri: SharedUri,
/// The HTTP status code.
status: http_client::StatusCode,
/// The HTTP response body.
body: String,
},
/// An error that occurred while processing an asset.
#[error("asset error: {0}")]
Asset(SharedString),
/// An error that occurred while processing an image.
#[error("image error: {0}")]
Image(Arc<ImageError>),
/// An error that occurred while processing an SVG.
#[error("svg error: {0}")]
Usvg(Arc<usvg::Error>),
}
impl From<anyhow::Error> for ImageCacheError {
fn from(value: anyhow::Error) -> Self {
Self::Other(Arc::new(value))
}
}
impl From<io::Error> for ImageCacheError {
fn from(value: io::Error) -> Self {
Self::Io(Arc::new(value))
}
}
impl From<usvg::Error> for ImageCacheError {
fn from(value: usvg::Error) -> Self {
Self::Usvg(Arc::new(value))
}
}
impl From<image::ImageError> for ImageCacheError {
fn from(value: image::ImageError) -> Self {
Self::Image(Arc::new(value))
}
}
+1287
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
mod anchored;
mod animation;
mod canvas;
mod deferred;
mod div;
mod image_cache;
mod img;
mod list;
mod surface;
mod svg;
mod text;
mod uniform_list;
pub use anchored::*;
pub use animation::*;
pub use canvas::*;
pub use deferred::*;
pub use div::*;
pub use image_cache::*;
pub use img::*;
pub use list::*;
pub use surface::*;
pub use svg::*;
pub use text::*;
pub use uniform_list::*;
+120
View File
@@ -0,0 +1,120 @@
use crate::{
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
ObjectFit, Pixels, Style, StyleRefinement, Styled, Window,
};
#[cfg(target_os = "macos")]
use core_video::pixel_buffer::CVPixelBuffer;
use refineable::Refineable;
/// A source of a surface's content.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurfaceSource {
/// A macOS image buffer from CoreVideo
#[cfg(target_os = "macos")]
Surface(CVPixelBuffer),
}
#[cfg(target_os = "macos")]
impl From<CVPixelBuffer> for SurfaceSource {
fn from(value: CVPixelBuffer) -> Self {
SurfaceSource::Surface(value)
}
}
/// A surface element.
pub struct Surface {
source: SurfaceSource,
object_fit: ObjectFit,
style: StyleRefinement,
}
/// Create a new surface element.
pub fn surface(source: impl Into<SurfaceSource>) -> Surface {
Surface {
source: source.into(),
object_fit: ObjectFit::Contain,
style: Default::default(),
}
}
impl Surface {
/// Set the object fit for the image.
pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
self.object_fit = object_fit;
self
}
}
impl Element for Surface {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = window.request_layout(style, [], cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) -> Self::PrepaintState {
}
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))] bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))] window: &mut Window,
_: &mut App,
) {
match &self.source {
#[cfg(target_os = "macos")]
SurfaceSource::Surface(surface) => {
let size = crate::size(surface.get_width().into(), surface.get_height().into());
let new_bounds = self.object_fit.get_bounds(bounds, size);
// TODO: Add support for corner_radii
window.paint_surface(new_bounds, surface.clone());
}
#[allow(unreachable_patterns)]
_ => {}
}
}
}
impl IntoElement for Surface {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for Surface {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
+221
View File
@@ -0,0 +1,221 @@
use crate::{
App, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
radians, size,
};
use util::ResultExt;
/// An SVG element.
pub struct Svg {
interactivity: Interactivity,
transformation: Option<Transformation>,
path: Option<SharedString>,
}
/// Create a new SVG element.
#[track_caller]
pub fn svg() -> Svg {
Svg {
interactivity: Interactivity::new(),
transformation: None,
path: None,
}
}
impl Svg {
/// Set the path to the SVG file for this element.
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into());
self
}
/// Transform the SVG element with the given transformation.
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
self.transformation = Some(transformation);
self
}
}
impl Element for Svg {
type RequestLayoutState = ();
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<crate::ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
self.interactivity.source_location()
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let layout_id = self.interactivity.request_layout(
global_id,
inspector_id,
window,
cx,
|style, window, cx| window.request_layout(style, None, cx),
);
(layout_id, ())
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Option<Hitbox> {
self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
bounds.size,
window,
cx,
|_, _, hitbox, _, _| hitbox,
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
hitbox: &mut Option<Hitbox>,
window: &mut Window,
cx: &mut App,
) where
Self: Sized,
{
self.interactivity.paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|style, window, cx| {
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
let transformation = self
.transformation
.as_ref()
.map(|transformation| {
transformation.into_matrix(bounds.center(), window.scale_factor())
})
.unwrap_or_default();
window
.paint_svg(bounds, path.clone(), transformation, color, cx)
.log_err();
}
},
)
}
}
impl IntoElement for Svg {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for Svg {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl InteractiveElement for Svg {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
/// A transformation to apply to an SVG element.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transformation {
scale: Size<f32>,
translate: Point<Pixels>,
rotate: Radians,
}
impl Default for Transformation {
fn default() -> Self {
Self {
scale: size(1.0, 1.0),
translate: point(px(0.0), px(0.0)),
rotate: radians(0.0),
}
}
}
impl Transformation {
/// Create a new Transformation with the specified scale along each axis.
pub fn scale(scale: Size<f32>) -> Self {
Self {
scale,
translate: point(px(0.0), px(0.0)),
rotate: radians(0.0),
}
}
/// Create a new Transformation with the specified translation.
pub fn translate(translate: Point<Pixels>) -> Self {
Self {
scale: size(1.0, 1.0),
translate,
rotate: radians(0.0),
}
}
/// Create a new Transformation with the specified rotation in radians.
pub fn rotate(rotate: impl Into<Radians>) -> Self {
let rotate = rotate.into();
Self {
scale: size(1.0, 1.0),
translate: point(px(0.0), px(0.0)),
rotate,
}
}
/// Update the scaling factor of this transformation.
pub fn with_scaling(mut self, scale: Size<f32>) -> Self {
self.scale = scale;
self
}
/// Update the translation value of this transformation.
pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
self.translate = translate;
self
}
/// Update the rotation angle of this transformation.
pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
self.rotate = rotate.into();
self
}
fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
//Note: if you read this as a sequence of matrix multiplications, start from the bottom
TransformationMatrix::unit()
.translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
.rotate(self.rotate)
.scale(self.scale)
.translate(center.scale(scale_factor).negate())
}
}
+914
View File
@@ -0,0 +1,914 @@
use crate::{
ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
register_tooltip_mouse_handlers, set_tooltip_on_window,
};
use anyhow::Context as _;
use smallvec::SmallVec;
use std::{
cell::{Cell, RefCell},
mem,
ops::Range,
rc::Rc,
sync::Arc,
};
use util::ResultExt;
impl Element for &'static str {
type RequestLayoutState = TextLayout;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextLayout::default();
let layout_id = state.layout(SharedString::from(*self), None, window, cx);
(layout_id, state)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) {
text_layout.prepaint(bounds, self)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
text_layout: &mut TextLayout,
_: &mut (),
window: &mut Window,
cx: &mut App,
) {
text_layout.paint(self, window, cx)
}
}
impl IntoElement for &'static str {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl IntoElement for String {
type Element = SharedString;
fn into_element(self) -> Self::Element {
self.into()
}
}
impl Element for SharedString {
type RequestLayoutState = TextLayout;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextLayout::default();
let layout_id = state.layout(self.clone(), None, window, cx);
(layout_id, state)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) {
text_layout.prepaint(bounds, self.as_ref())
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
text_layout.paint(self.as_ref(), window, cx)
}
}
impl IntoElement for SharedString {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// Renders text with runs of different styles.
///
/// Callers are responsible for setting the correct style for each run.
/// For text with a uniform style, you can usually avoid calling this constructor
/// and just pass text directly.
pub struct StyledText {
text: SharedString,
runs: Option<Vec<TextRun>>,
delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
layout: TextLayout,
}
impl StyledText {
/// Construct a new styled text element from the given string.
pub fn new(text: impl Into<SharedString>) -> Self {
StyledText {
text: text.into(),
runs: None,
delayed_highlights: None,
layout: TextLayout::default(),
}
}
/// Get the layout for this element. This can be used to map indices to pixels and vice versa.
pub fn layout(&self) -> &TextLayout {
&self.layout
}
/// Set the styling attributes for the given text, as well as
/// as any ranges of text that have had their style customized.
pub fn with_default_highlights(
mut self,
default_style: &TextStyle,
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
) -> Self {
debug_assert!(
self.delayed_highlights.is_none(),
"Can't use `with_default_highlights` and `with_highlights`"
);
let runs = Self::compute_runs(&self.text, default_style, highlights);
self.with_runs(runs)
}
/// Set the styling attributes for the given text, as well as
/// as any ranges of text that have had their style customized.
pub fn with_highlights(
mut self,
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
) -> Self {
debug_assert!(
self.runs.is_none(),
"Can't use `with_highlights` and `with_default_highlights`"
);
self.delayed_highlights = Some(
highlights
.into_iter()
.inspect(|(run, _)| {
debug_assert!(self.text.is_char_boundary(run.start));
debug_assert!(self.text.is_char_boundary(run.end));
})
.collect::<Vec<_>>(),
);
self
}
fn compute_runs(
text: &str,
default_style: &TextStyle,
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
) -> Vec<TextRun> {
let mut runs = Vec::new();
let mut ix = 0;
for (range, highlight) in highlights {
if ix < range.start {
debug_assert!(text.is_char_boundary(range.start));
runs.push(default_style.clone().to_run(range.start - ix));
}
debug_assert!(text.is_char_boundary(range.end));
runs.push(
default_style
.clone()
.highlight(highlight)
.to_run(range.len()),
);
ix = range.end;
}
if ix < text.len() {
runs.push(default_style.to_run(text.len() - ix));
}
runs
}
/// Set the text runs for this piece of text.
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
let mut text = &**self.text;
for run in &runs {
text = text.get(run.len..).expect("invalid text run");
}
assert!(text.is_empty(), "invalid text run");
self.runs = Some(runs);
self
}
}
impl Element for StyledText {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let runs = self.runs.take().or_else(|| {
self.delayed_highlights.take().map(|delayed_highlights| {
Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
})
});
let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) {
self.layout.prepaint(bounds, &self.text)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.layout.paint(&self.text, window, cx)
}
}
impl IntoElement for StyledText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
#[derive(Default, Clone)]
pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
struct TextLayoutInner {
len: usize,
lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
wrap_width: Option<Pixels>,
size: Option<Size<Pixels>>,
bounds: Option<Bounds<Pixels>>,
}
impl TextLayout {
fn layout(
&self,
text: SharedString,
runs: Option<Vec<TextRun>>,
window: &mut Window,
_: &mut App,
) -> LayoutId {
let text_style = window.text_style();
let font_size = text_style.font_size.to_pixels(window.rem_size());
let line_height = text_style
.line_height
.to_pixels(font_size.into(), window.rem_size());
let mut runs = if let Some(runs) = runs {
runs
} else {
vec![text_style.to_run(text.len())]
};
window.request_measured_layout(Default::default(), {
let element_state = self.clone();
move |known_dimensions, available_space, window, cx| {
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => Some(x),
_ => None,
})
} else {
None
};
let (truncate_width, truncation_suffix) =
if let Some(text_overflow) = text_style.text_overflow.clone() {
let width = known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
Some(max_lines) => Some(x * max_lines),
None => Some(x),
},
_ => None,
});
match text_overflow {
TextOverflow::Truncate(s) => (width, s),
}
} else {
(None, "".into())
};
if let Some(text_layout) = element_state.0.borrow().as_ref()
&& text_layout.size.is_some()
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
{
return text_layout.size.unwrap();
}
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
let text = if let Some(truncate_width) = truncate_width {
line_wrapper.truncate_line(
text.clone(),
truncate_width,
&truncation_suffix,
&mut runs,
)
} else {
text.clone()
};
let len = text.len();
let Some(lines) = window
.text_system()
.shape_text(
text,
font_size,
&runs,
wrap_width, // Wrap if we know the width.
text_style.line_clamp, // Limit the number of lines if line_clamp is set.
)
.log_err()
else {
element_state.0.borrow_mut().replace(TextLayoutInner {
lines: Default::default(),
len: 0,
line_height,
wrap_width,
size: Some(Size::default()),
bounds: None,
});
return Size::default();
};
let mut size: Size<Pixels> = Size::default();
for line in &lines {
let line_size = line.size(line_height);
size.height += line_size.height;
size.width = size.width.max(line_size.width).ceil();
}
element_state.0.borrow_mut().replace(TextLayoutInner {
lines,
len,
line_height,
wrap_width,
size: Some(size),
bounds: None,
});
size
}
})
}
fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
let mut element_state = self.0.borrow_mut();
let element_state = element_state
.as_mut()
.with_context(|| format!("measurement has not been performed on {text}"))
.unwrap();
element_state.bounds = Some(bounds);
}
fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
let element_state = self.0.borrow();
let element_state = element_state
.as_ref()
.with_context(|| format!("measurement has not been performed on {text}"))
.unwrap();
let bounds = element_state
.bounds
.with_context(|| format!("prepaint has not been performed on {text}"))
.unwrap();
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let text_style = window.text_style();
for line in &element_state.lines {
line.paint_background(
line_origin,
line_height,
text_style.text_align,
Some(bounds),
window,
cx,
)
.log_err();
line.paint(
line_origin,
line_height,
text_style.text_align,
Some(bounds),
window,
cx,
)
.log_err();
line_origin.y += line.size(line_height).height;
}
}
/// Get the byte index into the input of the pixel position.
pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
let element_state = self.0.borrow();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
if position.y < bounds.top() {
return Err(0);
}
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_bottom = line_origin.y + line.size(line_height).height;
if position.y > line_bottom {
line_origin.y = line_bottom;
line_start_ix += line.len() + 1;
} else {
let position_within_line = position - line_origin;
match line.index_for_position(position_within_line, line_height) {
Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
Err(index_within_line) => return Err(line_start_ix + index_within_line),
}
}
}
Err(line_start_ix.saturating_sub(1))
}
/// Get the pixel position for the given byte index.
pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
let element_state = self.0.borrow();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_end_ix = line_start_ix + line.len();
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_origin.y += line.size(line_height).height;
line_start_ix = line_end_ix + 1;
continue;
} else {
let ix_within_line = index - line_start_ix;
return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
}
}
None
}
/// Retrieve the layout for the line containing the given byte index.
pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
let element_state = self.0.borrow();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_end_ix = line_start_ix + line.len();
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_origin.y += line.size(line_height).height;
line_start_ix = line_end_ix + 1;
continue;
} else {
return Some(line.layout.clone());
}
}
None
}
/// The bounds of this layout.
pub fn bounds(&self) -> Bounds<Pixels> {
self.0.borrow().as_ref().unwrap().bounds.unwrap()
}
/// The line height for this layout.
pub fn line_height(&self) -> Pixels {
self.0.borrow().as_ref().unwrap().line_height
}
/// The UTF-8 length of the underlying text.
pub fn len(&self) -> usize {
self.0.borrow().as_ref().unwrap().len
}
/// The text for this layout.
pub fn text(&self) -> String {
self.0
.borrow()
.as_ref()
.unwrap()
.lines
.iter()
.map(|s| s.text.to_string())
.collect::<Vec<_>>()
.join("\n")
}
/// The text for this layout (with soft-wraps as newlines)
pub fn wrapped_text(&self) -> String {
let mut lines = Vec::new();
for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
let mut seen = 0;
for boundary in wrapped.layout.wrap_boundaries.iter() {
let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
[boundary.glyph_ix]
.index;
lines.push(wrapped.text[seen..index].to_string());
seen = index;
}
lines.push(wrapped.text[seen..].to_string());
}
lines.join("\n")
}
}
/// A text element that can be interacted with.
pub struct InteractiveText {
element_id: ElementId,
text: StyledText,
click_listener:
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
tooltip_id: Option<TooltipId>,
clickable_ranges: Vec<Range<usize>>,
}
struct InteractiveTextClickEvent {
mouse_down_index: usize,
mouse_up_index: usize,
}
#[doc(hidden)]
#[derive(Default)]
pub struct InteractiveTextState {
mouse_down_index: Rc<Cell<Option<usize>>>,
hovered_index: Rc<Cell<Option<usize>>>,
active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
}
/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
impl InteractiveText {
/// Creates a new InteractiveText from the given text.
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
Self {
element_id: id.into(),
text,
click_listener: None,
hover_listener: None,
tooltip_builder: None,
tooltip_id: None,
clickable_ranges: Vec::new(),
}
}
/// on_click is called when the user clicks on one of the given ranges, passing the index of
/// the clicked range.
pub fn on_click(
mut self,
ranges: Vec<Range<usize>>,
listener: impl Fn(usize, &mut Window, &mut App) + 'static,
) -> Self {
self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
for (range_ix, range) in ranges.iter().enumerate() {
if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
{
listener(range_ix, window, cx);
}
}
}));
self.clickable_ranges = ranges;
self
}
/// on_hover is called when the mouse moves over a character within the text, passing the
/// index of the hovered character, or None if the mouse leaves the text.
pub fn on_hover(
mut self,
listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.hover_listener = Some(Box::new(listener));
self
}
/// tooltip lets you specify a tooltip for a given character index in the string.
pub fn tooltip(
mut self,
builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
) -> Self {
self.tooltip_builder = Some(Rc::new(builder));
self
}
}
impl Element for InteractiveText {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
Some(self.element_id.clone())
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
self.text.request_layout(None, inspector_id, window, cx)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Hitbox {
window.with_optional_element_state::<InteractiveTextState, _>(
global_id,
|interactive_state, window| {
let mut interactive_state = interactive_state
.map(|interactive_state| interactive_state.unwrap_or_default());
if let Some(interactive_state) = interactive_state.as_mut() {
if self.tooltip_builder.is_some() {
self.tooltip_id =
set_tooltip_on_window(&interactive_state.active_tooltip, window);
} else {
// If there is no longer a tooltip builder, remove the active tooltip.
interactive_state.active_tooltip.take();
}
}
self.text
.prepaint(None, inspector_id, bounds, state, window, cx);
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
(hitbox, interactive_state)
},
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App,
) {
let current_view = window.current_view();
let text_layout = self.text.layout().clone();
window.with_element_state::<InteractiveTextState, _>(
global_id.unwrap(),
|interactive_state, window| {
let mut interactive_state = interactive_state.unwrap_or_default();
if let Some(click_listener) = self.click_listener.take() {
let mouse_position = window.mouse_position();
if let Ok(ix) = text_layout.index_for_position(mouse_position)
&& self
.clickable_ranges
.iter()
.any(|range| range.contains(&ix))
{
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
}
let text_layout = text_layout.clone();
let mouse_down = interactive_state.mouse_down_index.clone();
if let Some(mouse_down_index) = mouse_down.get() {
let hitbox = hitbox.clone();
let clickable_ranges = mem::take(&mut self.clickable_ranges);
window.on_mouse_event(
move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
if let Ok(mouse_up_index) =
text_layout.index_for_position(event.position)
{
click_listener(
&clickable_ranges,
InteractiveTextClickEvent {
mouse_down_index,
mouse_up_index,
},
window,
cx,
)
}
mouse_down.take();
window.refresh();
}
},
);
} else {
let hitbox = hitbox.clone();
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
if phase == DispatchPhase::Bubble
&& hitbox.is_hovered(window)
&& let Ok(mouse_down_index) =
text_layout.index_for_position(event.position)
{
mouse_down.set(Some(mouse_down_index));
window.refresh();
}
});
}
}
window.on_mouse_event({
let mut hover_listener = self.hover_listener.take();
let hitbox = hitbox.clone();
let text_layout = text_layout.clone();
let hovered_index = interactive_state.hovered_index.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
let current = hovered_index.get();
let updated = text_layout.index_for_position(event.position).ok();
if current != updated {
hovered_index.set(updated);
if let Some(hover_listener) = hover_listener.as_ref() {
hover_listener(updated, event.clone(), window, cx);
}
cx.notify(current_view);
}
}
}
});
if let Some(tooltip_builder) = self.tooltip_builder.clone() {
let active_tooltip = interactive_state.active_tooltip.clone();
let build_tooltip = Rc::new({
let tooltip_is_hoverable = false;
let text_layout = text_layout.clone();
move |window: &mut Window, cx: &mut App| {
text_layout
.index_for_position(window.mouse_position())
.ok()
.and_then(|position| tooltip_builder(position, window, cx))
.map(|view| (view, tooltip_is_hoverable))
}
});
// Use bounds instead of testing hitbox since this is called during prepaint.
let check_is_hovered_during_prepaint = Rc::new({
let source_bounds = hitbox.bounds;
let text_layout = text_layout.clone();
let pending_mouse_down = interactive_state.mouse_down_index.clone();
move |window: &Window| {
text_layout
.index_for_position(window.mouse_position())
.is_ok()
&& source_bounds.contains(&window.mouse_position())
&& pending_mouse_down.get().is_none()
}
});
let check_is_hovered = Rc::new({
let hitbox = hitbox.clone();
let text_layout = text_layout.clone();
let pending_mouse_down = interactive_state.mouse_down_index.clone();
move |window: &Window| {
text_layout
.index_for_position(window.mouse_position())
.is_ok()
&& hitbox.is_hovered(window)
&& pending_mouse_down.get().is_none()
}
});
register_tooltip_mouse_handlers(
&active_tooltip,
self.tooltip_id,
build_tooltip,
check_is_hovered,
check_is_hovered_during_prepaint,
window,
);
}
self.text
.paint(None, inspector_id, bounds, &mut (), &mut (), window, cx);
((), interactive_state)
},
);
}
}
impl IntoElement for InteractiveText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
+710
View File
@@ -0,0 +1,710 @@
//! A scrollable list of elements with uniform height, optimized for large lists.
//! Rather than use the full taffy layout system, uniform_list simply measures
//! the first element and then lays out all remaining elements in a line based on that
//! measurement. This is much faster than the full layout system, but only works for
//! elements with uniform height.
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity,
GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size,
StyleRefinement, Styled, Window, point, size,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
use super::ListHorizontalSizingBehavior;
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
/// uniform_list will only render the visible subset of items.
#[track_caller]
pub fn uniform_list<R>(
id: impl Into<ElementId>,
item_count: usize,
f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
) -> UniformList
where
R: IntoElement,
{
let id = id.into();
let mut base_style = StyleRefinement::default();
base_style.overflow.y = Some(Overflow::Scroll);
let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
f(range, window, cx)
.into_iter()
.map(|component| component.into_any_element())
.collect()
};
UniformList {
item_count,
item_to_measure_index: 0,
render_items: Box::new(render_range),
decorations: Vec::new(),
interactivity: Interactivity {
element_id: Some(id),
base_style: Box::new(base_style),
..Interactivity::new()
},
scroll_handle: None,
sizing_behavior: ListSizingBehavior::default(),
horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(),
}
}
/// A list element for efficiently laying out and displaying a list of uniform-height elements.
pub struct UniformList {
item_count: usize,
item_to_measure_index: usize,
render_items: Box<
dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
>,
decorations: Vec<Box<dyn UniformListDecoration>>,
interactivity: Interactivity,
scroll_handle: Option<UniformListScrollHandle>,
sizing_behavior: ListSizingBehavior,
horizontal_sizing_behavior: ListHorizontalSizingBehavior,
}
/// Frame state used by the [UniformList].
pub struct UniformListFrameState {
items: SmallVec<[AnyElement; 32]>,
decorations: SmallVec<[AnyElement; 2]>,
}
/// A handle for controlling the scroll position of a uniform list.
/// This should be stored in your view and passed to the uniform_list on each frame.
#[derive(Clone, Debug, Default)]
pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
/// Where to place the element scrolled to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollStrategy {
/// Place the element at the top of the list's viewport.
Top,
/// Attempt to place the element in the middle of the list's viewport.
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Center,
/// Attempt to place the element at the bottom of the list's viewport.
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Bottom,
}
#[derive(Clone, Copy, Debug)]
#[allow(missing_docs)]
pub struct DeferredScrollToItem {
/// The item index to scroll to
pub item_index: usize,
/// The scroll strategy to use
pub strategy: ScrollStrategy,
/// The offset in number of items
pub offset: usize,
pub scroll_strict: bool,
}
#[derive(Clone, Debug, Default)]
#[allow(missing_docs)]
pub struct UniformListScrollState {
pub base_handle: ScrollHandle,
pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
/// Size of the item, captured during last layout.
pub last_item_size: Option<ItemSize>,
/// Whether the list was vertically flipped during last layout.
pub y_flipped: bool,
}
#[derive(Copy, Clone, Debug, Default)]
/// The size of the item and its contents.
pub struct ItemSize {
/// The size of the item.
pub item: Size<Pixels>,
/// The size of the item's contents, which may be larger than the item itself,
/// if the item was bounded by a parent element.
pub contents: Size<Pixels>,
}
impl UniformListScrollHandle {
/// Create a new scroll handle to bind to a uniform list.
pub fn new() -> Self {
Self(Rc::new(RefCell::new(UniformListScrollState {
base_handle: ScrollHandle::new(),
deferred_scroll_to_item: None,
last_item_size: None,
y_flipped: false,
})))
}
/// Scroll the list so that the given item index is visible.
///
/// This uses non-strict scrolling: if the item is already fully visible, no scrolling occurs.
/// If the item is out of view, it scrolls the minimum amount to bring it into view according
/// to the strategy.
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset: 0,
scroll_strict: false,
});
}
/// Scroll the list so that the given item index is at scroll strategy position.
///
/// This uses strict scrolling: the item will always be scrolled to match the strategy position,
/// even if it's already visible. Use this when you need precise positioning.
pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset: 0,
scroll_strict: true,
});
}
/// Scroll the list to the given item index with an offset in number of items.
///
/// This uses non-strict scrolling: if the item is already visible within the offset region,
/// no scrolling occurs.
///
/// The offset parameter shrinks the effective viewport by the specified number of items
/// from the corresponding edge, then applies the scroll strategy within that reduced viewport:
/// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top
/// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport
/// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom
pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset,
scroll_strict: false,
});
}
/// Scroll the list so that the given item index is at the exact scroll strategy position with an offset.
///
/// This uses strict scrolling: the item will always be scrolled to match the strategy position,
/// even if it's already visible.
///
/// The offset parameter shrinks the effective viewport by the specified number of items
/// from the corresponding edge, then applies the scroll strategy within that reduced viewport:
/// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top
/// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport
/// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom
pub fn scroll_to_item_strict_with_offset(
&self,
ix: usize,
strategy: ScrollStrategy,
offset: usize,
) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset,
scroll_strict: true,
});
}
/// Check if the list is flipped vertically.
pub fn y_flipped(&self) -> bool {
self.0.borrow().y_flipped
}
/// Get the index of the topmost visible child.
#[cfg(any(test, feature = "test-support"))]
pub fn logical_scroll_top_index(&self) -> usize {
let this = self.0.borrow();
this.deferred_scroll_to_item
.as_ref()
.map(|deferred| deferred.item_index)
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
}
/// Checks if the list can be scrolled vertically.
pub fn is_scrollable(&self) -> bool {
if let Some(size) = self.0.borrow().last_item_size {
size.contents.height > size.item.height
} else {
false
}
}
}
impl Styled for UniformList {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl Element for UniformList {
type RequestLayoutState = UniformListFrameState;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let max_items = self.item_count;
let item_size = self.measure_item(None, window, cx);
let layout_id = self.interactivity.request_layout(
global_id,
inspector_id,
window,
cx,
|style, window, cx| match self.sizing_behavior {
ListSizingBehavior::Infer => {
window.with_text_style(style.text_style().cloned(), |window| {
window.request_measured_layout(
style,
move |known_dimensions, available_space, _window, _cx| {
let desired_height = item_size.height * max_items;
let width = known_dimensions.width.unwrap_or(match available_space
.width
{
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
}
});
let height = match available_space.height {
AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height
}
};
size(width, height)
},
)
})
}
ListSizingBehavior::Auto => window
.with_text_style(style.text_style().cloned(), |window| {
window.request_layout(style, None, cx)
}),
},
);
(
layout_id,
UniformListFrameState {
items: SmallVec::new(),
decorations: SmallVec::new(),
},
)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
frame_state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Option<Hitbox> {
let style = self
.interactivity
.compute_style(global_id, None, window, cx);
let border = style.border_widths.to_pixels(window.rem_size());
let padding = style
.padding
.to_pixels(bounds.size.into(), window.rem_size());
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top + padding.top),
bounds.bottom_right()
- point(border.right + padding.right, border.bottom + padding.bottom),
);
let can_scroll_horizontally = matches!(
self.horizontal_sizing_behavior,
ListHorizontalSizingBehavior::Unconstrained
);
let longest_item_size = self.measure_item(None, window, cx);
let content_width = if can_scroll_horizontally {
padded_bounds.size.width.max(longest_item_size.width)
} else {
padded_bounds.size.width
};
let content_size = Size {
width: content_width,
height: longest_item_size.height * self.item_count + padding.top + padding.bottom,
};
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
let item_height = longest_item_size.height;
let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
let mut handle = handle.0.borrow_mut();
handle.last_item_size = Some(ItemSize {
item: padded_bounds.size,
contents: content_size,
});
handle.deferred_scroll_to_item.take()
});
self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
content_size,
window,
cx,
|_style, mut scroll_offset, hitbox, window, cx| {
let y_flipped = if let Some(scroll_handle) = &self.scroll_handle {
let scroll_state = scroll_handle.0.borrow();
scroll_state.y_flipped
} else {
false
};
if self.item_count > 0 {
let content_height =
item_height * self.item_count + padding.top + padding.bottom;
let is_scrolled_vertically = !scroll_offset.y.is_zero();
let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
scroll_offset.y = min_vertical_scroll_offset;
}
let content_width = content_size.width + padding.left + padding.right;
let is_scrolled_horizontally =
can_scroll_horizontally && !scroll_offset.x.is_zero();
if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
scroll_offset.x = Pixels::ZERO;
}
if let Some(deferred_scroll) = shared_scroll_to_item {
let mut ix = deferred_scroll.item_index;
if y_flipped {
ix = self.item_count.saturating_sub(ix + 1);
}
let list_height = padded_bounds.size.height;
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
let item_top = item_height * ix + padding.top;
let item_bottom = item_top + item_height;
let scroll_top = -updated_scroll_offset.y;
let offset_pixels = item_height * deferred_scroll.offset;
let mut scrolled_to_top = false;
if item_top < scroll_top + padding.top + offset_pixels {
scrolled_to_top = true;
updated_scroll_offset.y = -(item_top) + padding.top + offset_pixels;
} else if item_bottom > scroll_top + list_height - padding.bottom {
scrolled_to_top = true;
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
}
if deferred_scroll.scroll_strict
|| (scrolled_to_top
&& (item_top < scroll_top + offset_pixels
|| item_bottom > scroll_top + list_height))
{
match deferred_scroll.strategy {
ScrollStrategy::Top => {
updated_scroll_offset.y = -(item_top - offset_pixels)
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
ScrollStrategy::Center => {
let item_center = item_top + item_height / 2.0;
let viewport_height = list_height - offset_pixels;
let viewport_center = offset_pixels + viewport_height / 2.0;
let target_scroll_top = item_center - viewport_center;
updated_scroll_offset.y = -target_scroll_top
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
ScrollStrategy::Bottom => {
updated_scroll_offset.y = -(item_bottom - list_height
+ offset_pixels)
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
}
}
scroll_offset = *updated_scroll_offset
}
let first_visible_element_ix =
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
/ item_height)
.ceil() as usize;
let visible_range = first_visible_element_ix
..cmp::min(last_visible_element_ix, self.item_count);
let items = if y_flipped {
let flipped_range = self.item_count.saturating_sub(visible_range.end)
..self.item_count.saturating_sub(visible_range.start);
let mut items = (self.render_items)(flipped_range, window, cx);
items.reverse();
items
} else {
(self.render_items)(visible_range.clone(), window, cx)
};
let content_mask = ContentMask { bounds };
window.with_content_mask(Some(content_mask), |window| {
for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
let item_origin = padded_bounds.origin
+ point(
if can_scroll_horizontally {
scroll_offset.x + padding.left
} else {
scroll_offset.x
},
item_height * ix + scroll_offset.y + padding.top,
);
let available_width = if can_scroll_horizontally {
padded_bounds.size.width + scroll_offset.x.abs()
} else {
padded_bounds.size.width
};
let available_space = size(
AvailableSpace::Definite(available_width),
AvailableSpace::Definite(item_height),
);
item.layout_as_root(available_space, window, cx);
item.prepaint_at(item_origin, window, cx);
frame_state.items.push(item);
}
let bounds = Bounds::new(
padded_bounds.origin
+ point(
if can_scroll_horizontally {
scroll_offset.x + padding.left
} else {
scroll_offset.x
},
scroll_offset.y + padding.top,
),
padded_bounds.size,
);
for decoration in &self.decorations {
let mut decoration = decoration.as_ref().compute(
visible_range.clone(),
bounds,
scroll_offset,
item_height,
self.item_count,
window,
cx,
);
let available_space = size(
AvailableSpace::Definite(bounds.size.width),
AvailableSpace::Definite(bounds.size.height),
);
decoration.layout_as_root(available_space, window, cx);
decoration.prepaint_at(bounds.origin, window, cx);
frame_state.decorations.push(decoration);
}
});
}
hitbox
},
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<crate::Pixels>,
request_layout: &mut Self::RequestLayoutState,
hitbox: &mut Option<Hitbox>,
window: &mut Window,
cx: &mut App,
) {
self.interactivity.paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|_, window, cx| {
for item in &mut request_layout.items {
item.paint(window, cx);
}
for decoration in &mut request_layout.decorations {
decoration.paint(window, cx);
}
},
)
}
}
impl IntoElement for UniformList {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// A decoration for a [`UniformList`]. This can be used for various things,
/// such as rendering indent guides, or other visual effects.
pub trait UniformListDecoration {
/// Compute the decoration element, given the visible range of list items,
/// the bounds of the list, and the height of each item.
fn compute(
&self,
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
item_height: Pixels,
item_count: usize,
window: &mut Window,
cx: &mut App,
) -> AnyElement;
}
impl<T: UniformListDecoration + 'static> UniformListDecoration for Entity<T> {
fn compute(
&self,
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
item_height: Pixels,
item_count: usize,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
self.update(cx, |inner, cx| {
inner.compute(
visible_range,
bounds,
scroll_offset,
item_height,
item_count,
window,
cx,
)
})
}
}
impl UniformList {
/// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0);
self
}
/// Sets the sizing behavior, similar to the `List` element.
pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
self.sizing_behavior = behavior;
self
}
/// Sets the horizontal sizing behavior, controlling the way list items laid out horizontally.
/// With [`ListHorizontalSizingBehavior::Unconstrained`] behavior, every item and the list itself will
/// have the size of the widest item and lay out pushing the `end_slot` to the right end.
pub fn with_horizontal_sizing_behavior(
mut self,
behavior: ListHorizontalSizingBehavior,
) -> Self {
self.horizontal_sizing_behavior = behavior;
match behavior {
ListHorizontalSizingBehavior::FitList => {
self.interactivity.base_style.overflow.x = None;
}
ListHorizontalSizingBehavior::Unconstrained => {
self.interactivity.base_style.overflow.x = Some(Overflow::Scroll);
}
}
self
}
/// Adds a decoration element to the list.
pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self {
self.decorations.push(Box::new(decoration));
self
}
fn measure_item(
&self,
list_width: Option<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Size<Pixels> {
if self.item_count == 0 {
return Size::default();
}
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
let Some(mut item_to_measure) = items.pop() else {
return Size::default();
};
let available_space = size(
list_width.map_or(AvailableSpace::MinContent, |width| {
AvailableSpace::Definite(width)
}),
AvailableSpace::MinContent,
);
item_to_measure.layout_as_root(available_space, window, cx)
}
/// Track and render scroll state of this list with reference to the given scroll handle.
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone());
self.scroll_handle = Some(handle);
self
}
/// Sets whether the list is flipped vertically, such that item 0 appears at the bottom.
pub fn y_flipped(mut self, y_flipped: bool) -> Self {
if let Some(ref scroll_handle) = self.scroll_handle {
let mut scroll_state = scroll_handle.0.borrow_mut();
let mut base_handle = &scroll_state.base_handle;
let offset = base_handle.offset();
match scroll_state.last_item_size {
Some(last_size) if scroll_state.y_flipped != y_flipped => {
let new_y_offset =
-(offset.y + last_size.contents.height - last_size.item.height);
base_handle.set_offset(point(offset.x, new_y_offset));
scroll_state.y_flipped = y_flipped;
}
// Handle case where list is initially flipped.
None if y_flipped => {
base_handle.set_offset(point(offset.x, Pixels::MIN));
scroll_state.y_flipped = y_flipped;
}
_ => {}
}
}
self
}
}
impl InteractiveElement for UniformList {
fn interactivity(&mut self) -> &mut crate::Interactivity {
&mut self.interactivity
}
}
+611
View File
@@ -0,0 +1,611 @@
use crate::{App, PlatformDispatcher};
use async_task::Runnable;
use futures::channel::mpsc;
use smol::prelude::*;
use std::mem::ManuallyDrop;
use std::panic::Location;
use std::thread::{self, ThreadId};
use std::{
fmt::Debug,
marker::PhantomData,
mem,
num::NonZeroUsize,
pin::Pin,
rc::Rc,
sync::{
Arc,
atomic::{AtomicUsize, Ordering::SeqCst},
},
task::{Context, Poll},
time::{Duration, Instant},
};
use util::TryFutureExt;
use waker_fn::waker_fn;
#[cfg(any(test, feature = "test-support"))]
use rand::rngs::StdRng;
/// A pointer to the executor that is currently running,
/// for spawning background tasks.
#[derive(Clone)]
pub struct BackgroundExecutor {
#[doc(hidden)]
pub dispatcher: Arc<dyn PlatformDispatcher>,
}
/// A pointer to the executor that is currently running,
/// for spawning tasks on the main thread.
///
/// This is intentionally `!Send` via the `not_send` marker field. This is because
/// `ForegroundExecutor::spawn` does not require `Send` but checks at runtime that the future is
/// only polled from the same thread it was spawned from. These checks would fail when spawning
/// foreground tasks from from background threads.
#[derive(Clone)]
pub struct ForegroundExecutor {
#[doc(hidden)]
pub dispatcher: Arc<dyn PlatformDispatcher>,
not_send: PhantomData<Rc<()>>,
}
/// Task is a primitive that allows work to happen in the background.
///
/// It implements [`Future`] so you can `.await` on it.
///
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
/// the task to continue running, but with no way to return a value.
#[must_use]
#[derive(Debug)]
pub struct Task<T>(TaskState<T>);
#[derive(Debug)]
enum TaskState<T> {
/// A task that is ready to return a value
Ready(Option<T>),
/// A task that is currently running.
Spawned(async_task::Task<T>),
}
impl<T> Task<T> {
/// Creates a new task that will resolve with the value
pub fn ready(val: T) -> Self {
Task(TaskState::Ready(Some(val)))
}
/// Detaching a task runs it to completion in the background
pub fn detach(self) {
match self {
Task(TaskState::Ready(_)) => {}
Task(TaskState::Spawned(task)) => task.detach(),
}
}
}
impl<E, T> Task<Result<T, E>>
where
T: 'static,
E: 'static + Debug,
{
/// Run the task to completion in the background and log any
/// errors that occur.
#[track_caller]
pub fn detach_and_log_err(self, cx: &App) {
let location = core::panic::Location::caller();
cx.foreground_executor()
.spawn(self.log_tracked_err(*location))
.detach();
}
}
impl<T> Future for Task<T> {
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match unsafe { self.get_unchecked_mut() } {
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()),
Task(TaskState::Spawned(task)) => task.poll(cx),
}
}
}
/// A task label is an opaque identifier that you can use to
/// refer to a task in tests.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TaskLabel(NonZeroUsize);
impl Default for TaskLabel {
fn default() -> Self {
Self::new()
}
}
impl TaskLabel {
/// Construct a new task label.
pub fn new() -> Self {
static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
}
}
type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
/// BackgroundExecutor lets you run things on background threads.
/// In production this is a thread pool with no ordering guarantees.
/// In tests this is simulated by running tasks one by one in a deterministic
/// (but arbitrary) order controlled by the `SEED` environment variable.
impl BackgroundExecutor {
#[doc(hidden)]
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self { dispatcher }
}
/// Enqueues the given future to be run to completion on a background thread.
pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.spawn_internal::<R>(Box::pin(future), None)
}
/// Enqueues the given future to be run to completion on a background thread.
/// The given label can be used to control the priority of the task in tests.
pub fn spawn_labeled<R>(
&self,
label: TaskLabel,
future: impl Future<Output = R> + Send + 'static,
) -> Task<R>
where
R: Send + 'static,
{
self.spawn_internal::<R>(Box::pin(future), Some(label))
}
fn spawn_internal<R: Send + 'static>(
&self,
future: AnyFuture<R>,
label: Option<TaskLabel>,
) -> Task<R> {
let dispatcher = self.dispatcher.clone();
let (runnable, task) =
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
runnable.schedule();
Task(TaskState::Spawned(task))
}
/// Used by the test harness to run an async test in a synchronous fashion.
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
if let Ok(value) = self.block_internal(false, future, None) {
value
} else {
unreachable!()
}
}
/// Block the current thread until the given future resolves.
/// Consider using `block_with_timeout` instead.
pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
if let Ok(value) = self.block_internal(true, future, None) {
value
} else {
unreachable!()
}
}
#[cfg(not(any(test, feature = "test-support")))]
pub(crate) fn block_internal<Fut: Future>(
&self,
_background_only: bool,
future: Fut,
timeout: Option<Duration>,
) -> Result<Fut::Output, impl Future<Output = Fut::Output> + use<Fut>> {
use std::time::Instant;
let mut future = Box::pin(future);
if timeout == Some(Duration::ZERO) {
return Err(future);
}
let deadline = timeout.map(|timeout| Instant::now() + timeout);
let parker = parking::Parker::new();
let unparker = parker.unparker();
let waker = waker_fn(move || {
unparker.unpark();
});
let mut cx = std::task::Context::from_waker(&waker);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(result) => return Ok(result),
Poll::Pending => {
let timeout =
deadline.map(|deadline| deadline.saturating_duration_since(Instant::now()));
if let Some(timeout) = timeout {
if !parker.park_timeout(timeout)
&& deadline.is_some_and(|deadline| deadline < Instant::now())
{
return Err(future);
}
} else {
parker.park();
}
}
}
}
}
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub(crate) fn block_internal<Fut: Future>(
&self,
background_only: bool,
future: Fut,
timeout: Option<Duration>,
) -> Result<Fut::Output, impl Future<Output = Fut::Output> + use<Fut>> {
use std::sync::atomic::AtomicBool;
use parking::Parker;
let mut future = Box::pin(future);
if timeout == Some(Duration::ZERO) {
return Err(future);
}
let Some(dispatcher) = self.dispatcher.as_test() else {
return Err(future);
};
let mut max_ticks = if timeout.is_some() {
dispatcher.gen_block_on_ticks()
} else {
usize::MAX
};
let parker = Parker::new();
let unparker = parker.unparker();
let awoken = Arc::new(AtomicBool::new(false));
let waker = waker_fn({
let awoken = awoken.clone();
let unparker = unparker.clone();
move || {
awoken.store(true, SeqCst);
unparker.unpark();
}
});
let mut cx = std::task::Context::from_waker(&waker);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(result) => return Ok(result),
Poll::Pending => {
if max_ticks == 0 {
return Err(future);
}
max_ticks -= 1;
if !dispatcher.tick(background_only) {
if awoken.swap(false, SeqCst) {
continue;
}
if !dispatcher.parking_allowed() {
if dispatcher.advance_clock_to_next_delayed() {
continue;
}
let mut backtrace_message = String::new();
let mut waiting_message = String::new();
if let Some(backtrace) = dispatcher.waiting_backtrace() {
backtrace_message =
format!("\nbacktrace of waiting future:\n{:?}", backtrace);
}
if let Some(waiting_hint) = dispatcher.waiting_hint() {
waiting_message = format!("\n waiting on: {}\n", waiting_hint);
}
panic!(
"parked with nothing left to run{waiting_message}{backtrace_message}",
)
}
dispatcher.set_unparker(unparker.clone());
parker.park();
}
}
}
}
}
/// Block the current thread until the given future resolves
/// or `duration` has elapsed.
pub fn block_with_timeout<Fut: Future>(
&self,
duration: Duration,
future: Fut,
) -> Result<Fut::Output, impl Future<Output = Fut::Output> + use<Fut>> {
self.block_internal(true, future, Some(duration))
}
/// Scoped lets you start a number of tasks and waits
/// for all of them to complete before returning.
pub async fn scoped<'scope, F>(&self, scheduler: F)
where
F: FnOnce(&mut Scope<'scope>),
{
let mut scope = Scope::new(self.clone());
(scheduler)(&mut scope);
let spawned = mem::take(&mut scope.futures)
.into_iter()
.map(|f| self.spawn(f))
.collect::<Vec<_>>();
for task in spawned {
task.await;
}
}
/// Get the current time.
///
/// Calling this instead of `std::time::Instant::now` allows the use
/// of fake timers in tests.
pub fn now(&self) -> Instant {
self.dispatcher.now()
}
/// Returns a task that will complete after the given duration.
/// Depending on other concurrent tasks the elapsed duration may be longer
/// than requested.
pub fn timer(&self, duration: Duration) -> Task<()> {
if duration.is_zero() {
return Task::ready(());
}
let (runnable, task) = async_task::spawn(async move {}, {
let dispatcher = self.dispatcher.clone();
move |runnable| dispatcher.dispatch_after(duration, runnable)
});
runnable.schedule();
Task(TaskState::Spawned(task))
}
/// in tests, start_waiting lets you indicate which task is waiting (for debugging only)
#[cfg(any(test, feature = "test-support"))]
pub fn start_waiting(&self) {
self.dispatcher.as_test().unwrap().start_waiting();
}
/// in tests, removes the debugging data added by start_waiting
#[cfg(any(test, feature = "test-support"))]
pub fn finish_waiting(&self) {
self.dispatcher.as_test().unwrap().finish_waiting();
}
/// in tests, run an arbitrary number of tasks (determined by the SEED environment variable)
#[cfg(any(test, feature = "test-support"))]
pub fn simulate_random_delay(&self) -> impl Future<Output = ()> + use<> {
self.dispatcher.as_test().unwrap().simulate_random_delay()
}
/// in tests, indicate that a given task from `spawn_labeled` should run after everything else
#[cfg(any(test, feature = "test-support"))]
pub fn deprioritize(&self, task_label: TaskLabel) {
self.dispatcher.as_test().unwrap().deprioritize(task_label)
}
/// in tests, move time forward. This does not run any tasks, but does make `timer`s ready.
#[cfg(any(test, feature = "test-support"))]
pub fn advance_clock(&self, duration: Duration) {
self.dispatcher.as_test().unwrap().advance_clock(duration)
}
/// in tests, run one task.
#[cfg(any(test, feature = "test-support"))]
pub fn tick(&self) -> bool {
self.dispatcher.as_test().unwrap().tick(false)
}
/// in tests, run all tasks that are ready to run. If after doing so
/// the test still has outstanding tasks, this will panic. (See also [`Self::allow_parking`])
#[cfg(any(test, feature = "test-support"))]
pub fn run_until_parked(&self) {
self.dispatcher.as_test().unwrap().run_until_parked()
}
/// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks.
/// This is useful when you are integrating other (non-GPUI) futures, like disk access, that
/// do take real async time to run.
#[cfg(any(test, feature = "test-support"))]
pub fn allow_parking(&self) {
self.dispatcher.as_test().unwrap().allow_parking();
}
/// undoes the effect of [`Self::allow_parking`].
#[cfg(any(test, feature = "test-support"))]
pub fn forbid_parking(&self) {
self.dispatcher.as_test().unwrap().forbid_parking();
}
/// adds detail to the "parked with nothing let to run" message.
#[cfg(any(test, feature = "test-support"))]
pub fn set_waiting_hint(&self, msg: Option<String>) {
self.dispatcher.as_test().unwrap().set_waiting_hint(msg);
}
/// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable
#[cfg(any(test, feature = "test-support"))]
pub fn rng(&self) -> StdRng {
self.dispatcher.as_test().unwrap().rng()
}
/// How many CPUs are available to the dispatcher.
pub fn num_cpus(&self) -> usize {
#[cfg(any(test, feature = "test-support"))]
return 4;
#[cfg(not(any(test, feature = "test-support")))]
return num_cpus::get();
}
/// Whether we're on the main thread.
pub fn is_main_thread(&self) -> bool {
self.dispatcher.is_main_thread()
}
#[cfg(any(test, feature = "test-support"))]
/// in tests, control the number of ticks that `block_with_timeout` will run before timing out.
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
}
}
/// ForegroundExecutor runs things on the main thread.
impl ForegroundExecutor {
/// Creates a new ForegroundExecutor from the given PlatformDispatcher.
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self {
dispatcher,
not_send: PhantomData,
}
}
/// Enqueues the given Task to run on the main thread at some point in the future.
#[track_caller]
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
where
R: 'static,
{
let dispatcher = self.dispatcher.clone();
#[track_caller]
fn inner<R: 'static>(
dispatcher: Arc<dyn PlatformDispatcher>,
future: AnyLocalFuture<R>,
) -> Task<R> {
let (runnable, task) = spawn_local_with_source_location(future, move |runnable| {
dispatcher.dispatch_on_main_thread(runnable)
});
runnable.schedule();
Task(TaskState::Spawned(task))
}
inner::<R>(dispatcher, Box::pin(future))
}
}
/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics.
///
/// Copy-modified from:
/// <https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405>
#[track_caller]
fn spawn_local_with_source_location<Fut, S>(
future: Fut,
schedule: S,
) -> (Runnable<()>, async_task::Task<Fut::Output, ()>)
where
Fut: Future + 'static,
Fut::Output: 'static,
S: async_task::Schedule<()> + Send + Sync + 'static,
{
#[inline]
fn thread_id() -> ThreadId {
std::thread_local! {
static ID: ThreadId = thread::current().id();
}
ID.try_with(|id| *id)
.unwrap_or_else(|_| thread::current().id())
}
struct Checked<F> {
id: ThreadId,
inner: ManuallyDrop<F>,
location: &'static Location<'static>,
}
impl<F> Drop for Checked<F> {
fn drop(&mut self) {
assert!(
self.id == thread_id(),
"local task dropped by a thread that didn't spawn it. Task spawned at {}",
self.location
);
unsafe { ManuallyDrop::drop(&mut self.inner) };
}
}
impl<F: Future> Future for Checked<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
assert!(
self.id == thread_id(),
"local task polled by a thread that didn't spawn it. Task spawned at {}",
self.location
);
unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) }
}
}
// Wrap the future into one that checks which thread it's on.
let future = Checked {
id: thread_id(),
inner: ManuallyDrop::new(future),
location: Location::caller(),
};
unsafe { async_task::spawn_unchecked(future, schedule) }
}
/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
pub struct Scope<'a> {
executor: BackgroundExecutor,
futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
tx: Option<mpsc::Sender<()>>,
rx: mpsc::Receiver<()>,
lifetime: PhantomData<&'a ()>,
}
impl<'a> Scope<'a> {
fn new(executor: BackgroundExecutor) -> Self {
let (tx, rx) = mpsc::channel(1);
Self {
executor,
tx: Some(tx),
rx,
futures: Default::default(),
lifetime: PhantomData,
}
}
/// How many CPUs are available to the dispatcher.
pub fn num_cpus(&self) -> usize {
self.executor.num_cpus()
}
/// Spawn a future into this scope.
pub fn spawn<F>(&mut self, f: F)
where
F: Future<Output = ()> + Send + 'a,
{
let tx = self.tx.clone().unwrap();
// SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because
// dropping this `Scope` blocks until all of the futures have resolved.
let f = unsafe {
mem::transmute::<
Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
>(Box::pin(async move {
f.await;
drop(tx);
}))
};
self.futures.push(f);
}
}
impl Drop for Scope<'_> {
fn drop(&mut self) {
self.tx.take().unwrap();
// Wait until the channel is closed, which means that all of the spawned
// futures have resolved.
self.executor.block(self.rx.next());
}
}
+3912
View File
File diff suppressed because it is too large Load Diff
+75
View File
@@ -0,0 +1,75 @@
use crate::{App, BorrowAppContext};
/// A marker trait for types that can be stored in GPUI's global state.
///
/// This trait exists to provide type-safe access to globals by ensuring only
/// types that implement [`Global`] can be used with the accessor methods. For
/// example, trying to access a global with a type that does not implement
/// [`Global`] will result in a compile-time error.
///
/// Implement this on types you want to store in the context as a global.
///
/// ## Restricting Access to Globals
///
/// In some situations you may need to store some global state, but want to
/// restrict access to reading it or writing to it.
///
/// In these cases, Rust's visibility system can be used to restrict access to
/// a global value. For example, you can create a private struct that implements
/// [`Global`] and holds the global state. Then create a newtype struct that wraps
/// the global type and create custom accessor methods to expose the desired subset
/// of operations.
pub trait Global: 'static {
// This trait is intentionally left empty, by virtue of being a marker trait.
//
// Use additional traits with blanket implementations to attach functionality
// to types that implement `Global`.
}
/// A trait for reading a global value from the context.
pub trait ReadGlobal {
/// Returns the global instance of the implementing type.
///
/// Panics if a global for that type has not been assigned.
fn global(cx: &App) -> &Self;
}
impl<T: Global> ReadGlobal for T {
fn global(cx: &App) -> &Self {
cx.global::<T>()
}
}
/// A trait for updating a global value in the context.
pub trait UpdateGlobal {
/// Updates the global instance of the implementing type using the provided closure.
///
/// This method provides the closure with mutable access to the context and the global simultaneously.
fn update_global<C, F, R>(cx: &mut C, update: F) -> R
where
C: BorrowAppContext,
F: FnOnce(&mut Self, &mut C) -> R;
/// Set the global instance of the implementing type.
fn set_global<C>(cx: &mut C, global: Self)
where
C: BorrowAppContext;
}
impl<T: Global> UpdateGlobal for T {
#[track_caller]
fn update_global<C, F, R>(cx: &mut C, update: F) -> R
where
C: BorrowAppContext,
F: FnOnce(&mut Self, &mut C) -> R,
{
cx.update_global(update)
}
fn set_global<C>(cx: &mut C, global: Self)
where
C: BorrowAppContext,
{
cx.set_global(global)
}
}
+311
View File
@@ -0,0 +1,311 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![allow(clippy::type_complexity)] // Not useful, GPUI makes heavy use of callbacks
#![allow(clippy::collapsible_else_if)] // False positives in platform specific code
#![allow(unused_mut)] // False positives in platform specific code
extern crate self as gpui;
#[macro_use]
mod action;
mod app;
mod arena;
mod asset_cache;
mod assets;
mod bounds_tree;
mod color;
/// The default colors used by GPUI.
pub mod colors;
mod element;
mod elements;
mod executor;
mod geometry;
mod global;
mod input;
mod inspector;
mod interactive;
mod key_dispatch;
mod keymap;
mod path_builder;
mod platform;
pub mod prelude;
mod scene;
mod shared_string;
mod shared_uri;
mod style;
mod styled;
mod subscription;
mod svg_renderer;
mod tab_stop;
mod taffy;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
mod text_system;
mod util;
mod view;
mod window;
#[cfg(doc)]
pub mod _ownership_and_data_flow;
/// Do not touch, here be dragons for use by gpui_macros and such.
#[doc(hidden)]
pub mod private {
pub use anyhow;
pub use inventory;
pub use schemars;
pub use serde;
pub use serde_json;
}
mod seal {
/// A mechanism for restricting implementations of a trait to only those in GPUI.
/// See: <https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/>
pub trait Sealed {}
}
pub use action::*;
pub use anyhow::Result;
pub use app::*;
pub(crate) use arena::*;
pub use asset_cache::*;
pub use assets::*;
pub use color::*;
pub use ctor::ctor;
pub use element::*;
pub use elements::*;
pub use executor::*;
pub use geometry::*;
pub use global::*;
pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test};
pub use http_client;
pub use input::*;
pub use inspector::*;
pub use interactive::*;
use key_dispatch::*;
pub use keymap::*;
pub use path_builder::*;
pub use platform::*;
pub use refineable::*;
pub use scene::*;
pub use shared_string::*;
pub use shared_uri::*;
pub use smol::Timer;
pub use style::*;
pub use styled::*;
pub use subscription::*;
use svg_renderer::*;
pub(crate) use tab_stop::*;
pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
pub use text_system::*;
#[cfg(any(test, feature = "test-support"))]
pub use util::smol_timeout;
pub use util::{FutureExt, Timeout, arc_cow::ArcCow};
pub use view::*;
pub use window::*;
use std::{any::Any, borrow::BorrowMut, future::Future};
use taffy::TaffyLayoutEngine;
/// The context trait, allows the different contexts in GPUI to be used
/// interchangeably for certain operations.
pub trait AppContext {
/// The result type for this context, used for async contexts that
/// can't hold a direct reference to the application context.
type Result<T>;
/// Create a new entity in the app context.
#[expect(
clippy::wrong_self_convention,
reason = "`App::new` is an ubiquitous function for creating entities"
)]
fn new<T: 'static>(
&mut self,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Self::Result<Entity<T>>;
/// Reserve a slot for a entity to be inserted later.
/// The returned [Reservation] allows you to obtain the [EntityId] for the future entity.
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<Reservation<T>>;
/// Insert a new entity in the app context based on a [Reservation] previously obtained from [`reserve_entity`].
///
/// [`reserve_entity`]: Self::reserve_entity
fn insert_entity<T: 'static>(
&mut self,
reservation: Reservation<T>,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Self::Result<Entity<T>>;
/// Update a entity in the app context.
fn update_entity<T, R>(
&mut self,
handle: &Entity<T>,
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> Self::Result<R>
where
T: 'static;
/// Update a entity in the app context.
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<GpuiBorrow<'a, T>>
where
T: 'static;
/// Read a entity from the app context.
fn read_entity<T, R>(
&self,
handle: &Entity<T>,
read: impl FnOnce(&T, &App) -> R,
) -> Self::Result<R>
where
T: 'static;
/// Update a window for the given handle.
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut Window, &mut App) -> T;
/// Read a window off of the application context.
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(Entity<T>, &App) -> R,
) -> Result<R>
where
T: 'static;
/// Spawn a future on a background thread
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static;
/// Read a global from this app context
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
where
G: Global;
}
/// Returned by [Context::reserve_entity] to later be passed to [Context::insert_entity].
/// Allows you to obtain the [EntityId] for a entity before it is created.
pub struct Reservation<T>(pub(crate) Slot<T>);
impl<T: 'static> Reservation<T> {
/// Returns the [EntityId] that will be associated with the entity once it is inserted.
pub fn entity_id(&self) -> EntityId {
self.0.entity_id()
}
}
/// This trait is used for the different visual contexts in GPUI that
/// require a window to be present.
pub trait VisualContext: AppContext {
/// Returns the handle of the window associated with this context.
fn window_handle(&self) -> AnyWindowHandle;
/// Update a view with the given callback
fn update_window_entity<T: 'static, R>(
&mut self,
entity: &Entity<T>,
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
) -> Self::Result<R>;
/// Create a new entity, with access to `Window`.
fn new_window_entity<T: 'static>(
&mut self,
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
) -> Self::Result<Entity<T>>;
/// Replace the root view of a window with a new view.
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
) -> Self::Result<Entity<V>>
where
V: 'static + Render;
/// Focus a entity in the window, if it implements the [`Focusable`] trait.
fn focus<V>(&mut self, entity: &Entity<V>) -> Self::Result<()>
where
V: Focusable;
}
/// A trait for tying together the types of a GPUI entity and the events it can
/// emit.
pub trait EventEmitter<E: Any>: 'static {}
/// A helper trait for auto-implementing certain methods on contexts that
/// can be used interchangeably.
pub trait BorrowAppContext {
/// Set a global value on the context.
fn set_global<T: Global>(&mut self, global: T);
/// Updates the global state of the given type.
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global;
/// Updates the global state of the given type, creating a default if it didn't exist before.
fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global + Default;
}
impl<C> BorrowAppContext for C
where
C: BorrowMut<App>,
{
fn set_global<G: Global>(&mut self, global: G) {
self.borrow_mut().set_global(global)
}
#[track_caller]
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global,
{
let mut global = self.borrow_mut().lease_global::<G>();
let result = f(&mut global, self);
self.borrow_mut().end_global_lease(global);
result
}
fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global + Default,
{
self.borrow_mut().default_global::<G>();
self.update_global(f)
}
}
/// A flatten equivalent for anyhow `Result`s.
pub trait Flatten<T> {
/// Convert this type into a simple `Result<T>`.
fn flatten(self) -> Result<T>;
}
impl<T> Flatten<T> for Result<Result<T>> {
fn flatten(self) -> Result<T> {
self?
}
}
impl<T> Flatten<T> for Result<T> {
fn flatten(self) -> Result<T> {
self
}
}
/// Information about the GPU GPUI is running on.
#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct GpuSpecs {
/// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU.
pub is_software_emulated: bool,
/// The name of the device, as reported by Vulkan.
pub device_name: String,
/// The name of the driver, as reported by Vulkan.
pub driver_name: String,
/// Further information about the driver, as reported by Vulkan.
pub driver_info: String,
}
+180
View File
@@ -0,0 +1,180 @@
use crate::{App, Bounds, Context, Entity, InputHandler, Pixels, UTF16Selection, Window};
use std::ops::Range;
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
///
/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
/// This input handler can then be assigned during paint by calling [`Window::handle_input`].
///
/// See [`InputHandler`] for details on how to implement each method.
pub trait EntityInputHandler: 'static + Sized {
/// See [`InputHandler::text_for_range`] for details
fn text_for_range(
&mut self,
range: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<String>;
/// See [`InputHandler::selected_text_range`] for details
fn selected_text_range(
&mut self,
ignore_disabled_input: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<UTF16Selection>;
/// See [`InputHandler::marked_text_range`] for details
fn marked_text_range(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Range<usize>>;
/// See [`InputHandler::unmark_text`] for details
fn unmark_text(&mut self, window: &mut Window, cx: &mut Context<Self>);
/// See [`InputHandler::replace_text_in_range`] for details
fn replace_text_in_range(
&mut self,
range: Option<Range<usize>>,
text: &str,
window: &mut Window,
cx: &mut Context<Self>,
);
/// See [`InputHandler::replace_and_mark_text_in_range`] for details
fn replace_and_mark_text_in_range(
&mut self,
range: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
window: &mut Window,
cx: &mut Context<Self>,
);
/// See [`InputHandler::bounds_for_range`] for details
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
element_bounds: Bounds<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>>;
/// See [`InputHandler::character_index_for_point`] for details
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize>;
}
/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`]
/// with an instance during your element's paint.
pub struct ElementInputHandler<V> {
view: Entity<V>,
element_bounds: Bounds<Pixels>,
}
impl<V: 'static> ElementInputHandler<V> {
/// Used in [`Element::paint`][element_paint] with the element's bounds, a `Window`, and a `App` context.
///
/// [element_paint]: crate::Element::paint
pub fn new(element_bounds: Bounds<Pixels>, view: Entity<V>) -> Self {
ElementInputHandler {
view,
element_bounds,
}
}
}
impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
fn selected_text_range(
&mut self,
ignore_disabled_input: bool,
window: &mut Window,
cx: &mut App,
) -> Option<UTF16Selection> {
self.view.update(cx, |view, cx| {
view.selected_text_range(ignore_disabled_input, window, cx)
})
}
fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>> {
self.view
.update(cx, |view, cx| view.marked_text_range(window, cx))
}
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
window: &mut Window,
cx: &mut App,
) -> Option<String> {
self.view.update(cx, |view, cx| {
view.text_for_range(range_utf16, adjusted_range, window, cx)
})
}
fn replace_text_in_range(
&mut self,
replacement_range: Option<Range<usize>>,
text: &str,
window: &mut Window,
cx: &mut App,
) {
self.view.update(cx, |view, cx| {
view.replace_text_in_range(replacement_range, text, window, cx)
});
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
window: &mut Window,
cx: &mut App,
) {
self.view.update(cx, |view, cx| {
view.replace_and_mark_text_in_range(
range_utf16,
new_text,
new_selected_range,
window,
cx,
)
});
}
fn unmark_text(&mut self, window: &mut Window, cx: &mut App) {
self.view
.update(cx, |view, cx| view.unmark_text(window, cx));
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> Option<Bounds<Pixels>> {
self.view.update(cx, |view, cx| {
view.bounds_for_range(range_utf16, self.element_bounds, window, cx)
})
}
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<usize> {
self.view.update(cx, |view, cx| {
view.character_index_for_point(point, window, cx)
})
}
}
+254
View File
@@ -0,0 +1,254 @@
/// A unique identifier for an element that can be inspected.
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct InspectorElementId {
/// Stable part of the ID.
#[cfg(any(feature = "inspector", debug_assertions))]
pub path: std::rc::Rc<InspectorElementPath>,
/// Disambiguates elements that have the same path.
#[cfg(any(feature = "inspector", debug_assertions))]
pub instance_id: usize,
}
impl Into<InspectorElementId> for &InspectorElementId {
fn into(self) -> InspectorElementId {
self.clone()
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
pub use conditional::*;
#[cfg(any(feature = "inspector", debug_assertions))]
mod conditional {
use super::*;
use crate::{AnyElement, App, Context, Empty, IntoElement, Render, Window};
use collections::FxHashMap;
use std::any::{Any, TypeId};
/// `GlobalElementId` qualified by source location of element construction.
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct InspectorElementPath {
/// The path to the nearest ancestor element that has an `ElementId`.
#[cfg(any(feature = "inspector", debug_assertions))]
pub global_id: crate::GlobalElementId,
/// Source location where this element was constructed.
#[cfg(any(feature = "inspector", debug_assertions))]
pub source_location: &'static std::panic::Location<'static>,
}
impl Clone for InspectorElementPath {
fn clone(&self) -> Self {
Self {
global_id: crate::GlobalElementId(self.global_id.0.clone()),
source_location: self.source_location,
}
}
}
impl Into<InspectorElementPath> for &InspectorElementPath {
fn into(self) -> InspectorElementPath {
self.clone()
}
}
/// Function set on `App` to render the inspector UI.
pub type InspectorRenderer =
Box<dyn Fn(&mut Inspector, &mut Window, &mut Context<Inspector>) -> AnyElement>;
/// Manages inspector state - which element is currently selected and whether the inspector is
/// in picking mode.
pub struct Inspector {
active_element: Option<InspectedElement>,
pub(crate) pick_depth: Option<f32>,
}
struct InspectedElement {
id: InspectorElementId,
states: FxHashMap<TypeId, Box<dyn Any>>,
}
impl InspectedElement {
fn new(id: InspectorElementId) -> Self {
InspectedElement {
id,
states: FxHashMap::default(),
}
}
}
impl Inspector {
pub(crate) fn new() -> Self {
Self {
active_element: None,
pick_depth: Some(0.0),
}
}
pub(crate) fn select(&mut self, id: InspectorElementId, window: &mut Window) {
self.set_active_element_id(id, window);
self.pick_depth = None;
}
pub(crate) fn hover(&mut self, id: InspectorElementId, window: &mut Window) {
if self.is_picking() {
let changed = self.set_active_element_id(id, window);
if changed {
self.pick_depth = Some(0.0);
}
}
}
pub(crate) fn set_active_element_id(
&mut self,
id: InspectorElementId,
window: &mut Window,
) -> bool {
let changed = Some(&id) != self.active_element_id();
if changed {
self.active_element = Some(InspectedElement::new(id));
window.refresh();
}
changed
}
/// ID of the currently hovered or selected element.
pub fn active_element_id(&self) -> Option<&InspectorElementId> {
self.active_element.as_ref().map(|e| &e.id)
}
pub(crate) fn with_active_element_state<T: 'static, R>(
&mut self,
window: &mut Window,
f: impl FnOnce(&mut Option<T>, &mut Window) -> R,
) -> R {
let Some(active_element) = &mut self.active_element else {
return f(&mut None, window);
};
let type_id = TypeId::of::<T>();
let mut inspector_state = active_element
.states
.remove(&type_id)
.map(|state| *state.downcast().unwrap());
let result = f(&mut inspector_state, window);
if let Some(inspector_state) = inspector_state {
active_element
.states
.insert(type_id, Box::new(inspector_state));
}
result
}
/// Starts element picking mode, allowing the user to select elements by clicking.
pub fn start_picking(&mut self) {
self.pick_depth = Some(0.0);
}
/// Returns whether the inspector is currently in picking mode.
pub fn is_picking(&self) -> bool {
self.pick_depth.is_some()
}
/// Renders elements for all registered inspector states of the active inspector element.
pub fn render_inspector_states(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
let mut elements = Vec::new();
if let Some(active_element) = self.active_element.take() {
for (type_id, state) in &active_element.states {
if let Some(render_inspector) = cx
.inspector_element_registry
.renderers_by_type_id
.remove(type_id)
{
let mut element = (render_inspector)(
active_element.id.clone(),
state.as_ref(),
window,
cx,
);
elements.push(element);
cx.inspector_element_registry
.renderers_by_type_id
.insert(*type_id, render_inspector);
}
}
self.active_element = Some(active_element);
}
elements
}
}
impl Render for Inspector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(inspector_renderer) = cx.inspector_renderer.take() {
let result = inspector_renderer(self, window, cx);
cx.inspector_renderer = Some(inspector_renderer);
result
} else {
Empty.into_any_element()
}
}
}
#[derive(Default)]
pub(crate) struct InspectorElementRegistry {
renderers_by_type_id: FxHashMap<
TypeId,
Box<dyn Fn(InspectorElementId, &dyn Any, &mut Window, &mut App) -> AnyElement>,
>,
}
impl InspectorElementRegistry {
pub fn register<T: 'static, R: IntoElement>(
&mut self,
f: impl 'static + Fn(InspectorElementId, &T, &mut Window, &mut App) -> R,
) {
self.renderers_by_type_id.insert(
TypeId::of::<T>(),
Box::new(move |id, value, window, cx| {
let value = value.downcast_ref().unwrap();
f(id, value, window, cx).into_any_element()
}),
);
}
}
}
/// Provides definitions used by `#[derive_inspector_reflection]`.
#[cfg(any(feature = "inspector", debug_assertions))]
pub mod inspector_reflection {
use std::any::Any;
/// Reification of a function that has the signature `fn some_fn(T) -> T`. Provides the name,
/// documentation, and ability to invoke the function.
#[derive(Clone, Copy)]
pub struct FunctionReflection<T> {
/// The name of the function
pub name: &'static str,
/// The method
pub function: fn(Box<dyn Any>) -> Box<dyn Any>,
/// Documentation for the function
pub documentation: Option<&'static str>,
/// `PhantomData` for the type of the argument and result
pub _type: std::marker::PhantomData<T>,
}
impl<T: 'static> FunctionReflection<T> {
/// Invoke this method on a value and return the result.
pub fn invoke(&self, value: T) -> T {
let boxed = Box::new(value) as Box<dyn Any>;
let result = (self.function)(boxed);
*result
.downcast::<T>()
.expect("Type mismatch in reflection invoke")
}
}
}

Some files were not shown because too many files have changed in this diff Show More