Present Servo BGRA hardware surfaces
This commit is contained in:
Generated
-2
@@ -3312,8 +3312,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",
|
||||
|
||||
@@ -35,6 +35,10 @@ ureq = "2.12.1"
|
||||
url = "2.5.4"
|
||||
uuid = { version = "1.12.1", features = ["v7"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# Local GPUI 0.2.2 patch: macOS `surface(CVPixelBuffer)` accepts Servo's BGRA IOSurfaces.
|
||||
gpui = { path = "third_party/gpui" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ cargo fmt --all --check
|
||||
cargo check --workspace --all-targets
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo test --workspace --all-targets
|
||||
cargo check -p ely_servo_host --features servo-engine --all-targets
|
||||
cargo clippy -p ely_servo_host --features servo-engine --all-targets -- -D warnings
|
||||
cargo check -p ely_servo_host --features servo-engine,hardware-render --all-targets
|
||||
cargo clippy -p ely_servo_host --features servo-engine,hardware-render --all-targets -- -D warnings
|
||||
cargo test -p ely_servo_host --features servo-engine --test software_host
|
||||
scripts/verify_prd_site_rendering.sh
|
||||
scripts/verify_windows_app_manifest.sh
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
//!
|
||||
//! `T10.4` originally imported the IOSurface into an `MTLTexture`
|
||||
//! directly. GPUI 0.2.2 exposes `Window::paint_surface` /
|
||||
//! `elements::surface::Surface` for `CVPixelBuffer`, and that public
|
||||
//! path is wired for NV12 video frames. Servo's hardware renderer
|
||||
//! publishes BGRA IOSurfaces, so this cache stays as verified
|
||||
//! cross-process plumbing until the presenter accepts BGRA surfaces.
|
||||
//! `elements::surface::Surface` for `CVPixelBuffer`; the local GPUI
|
||||
//! patch adds a BGRA fragment pipeline for Servo's hardware
|
||||
//! IOSurfaces, so this cache is the renderer-side handoff point.
|
||||
//!
|
||||
//! Lifetime contract:
|
||||
//!
|
||||
@@ -161,11 +160,14 @@ mod tests {
|
||||
const TEST_HEIGHT: u32 = 48;
|
||||
|
||||
/// Build a CPU-backed IOSurface from scratch, the same way
|
||||
/// surfman's macOS backend does — BGRA8 (four-cc '32BGRA'), width
|
||||
/// + height + bytes_per_element + bytes_per_row in a Core
|
||||
/// Foundation properties dictionary. The pointer-casts mirror
|
||||
/// surfman's macOS backend does.
|
||||
///
|
||||
/// BGRA8 (four-cc '32BGRA'), width + height + bytes_per_element
|
||||
/// + bytes_per_row live in a Core Foundation properties dictionary.
|
||||
///
|
||||
/// The pointer-casts mirror
|
||||
/// `surfman::platform::macos::system::surface::create_io_surface`.
|
||||
fn build_local_iosurface() -> CFRetained<IOSurfaceRef> {
|
||||
fn build_local_iosurface() -> Result<CFRetained<IOSurfaceRef>, String> {
|
||||
let pixel_format: i32 = i32::from_be_bytes(*b"BGRA");
|
||||
let bytes_per_element: i32 = 4;
|
||||
let bytes_per_row: i32 = (TEST_WIDTH as i32) * bytes_per_element;
|
||||
@@ -196,27 +198,25 @@ mod tests {
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks,
|
||||
)
|
||||
.expect("CFDictionaryCreate must succeed for the properties dict");
|
||||
.ok_or_else(|| "CFDictionaryCreate returned null".to_string())?;
|
||||
IOSurfaceRef::new(&properties)
|
||||
.expect("IOSurfaceCreate must succeed for a well-formed properties dict")
|
||||
.ok_or_else(|| "IOSurfaceCreate returned null".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_local_iosurface_into_pixel_buffer() {
|
||||
fn imports_local_iosurface_into_pixel_buffer() -> Result<(), String> {
|
||||
let mut cache = IOSurfaceCache::new();
|
||||
let iosurface = build_local_iosurface();
|
||||
let iosurface = build_local_iosurface()?;
|
||||
let mach_port = iosurface.create_mach_port();
|
||||
assert!(mach_port != 0, "IOSurfaceCreateMachPort must yield a real port");
|
||||
let surface_id: u64 = 0xDEAD_BEEFu64;
|
||||
|
||||
cache
|
||||
.import(mach_port, surface_id)
|
||||
.expect("local IOSurface must round-trip into a CVPixelBuffer");
|
||||
cache.import(mach_port, surface_id).map_err(|error| error.to_string())?;
|
||||
|
||||
let pixel_buffer = cache
|
||||
.pixel_buffer_for(surface_id)
|
||||
.expect("imported pixel buffer must be retrievable by surface_id");
|
||||
.ok_or_else(|| "imported pixel buffer was missing".to_string())?;
|
||||
assert_eq!(
|
||||
pixel_buffer.get_width() as u32,
|
||||
TEST_WIDTH,
|
||||
@@ -228,20 +228,22 @@ mod tests {
|
||||
"CVPixelBuffer height must match the source IOSurface",
|
||||
);
|
||||
assert_eq!(cache.cached_surface_count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_import_with_same_surface_id_is_idempotent() {
|
||||
fn second_import_with_same_surface_id_is_idempotent() -> Result<(), String> {
|
||||
let mut cache = IOSurfaceCache::new();
|
||||
let iosurface = build_local_iosurface();
|
||||
let iosurface = build_local_iosurface()?;
|
||||
let port_a = iosurface.create_mach_port();
|
||||
let port_b = iosurface.create_mach_port();
|
||||
assert!(port_a != 0 && port_b != 0 && port_a != port_b);
|
||||
|
||||
cache.import(port_a, 0xAAAA_AAAA).expect("first import");
|
||||
cache.import(port_a, 0xAAAA_AAAA).map_err(|error| error.to_string())?;
|
||||
// Same surface_id → defensive dedup path; port_b is deallocated
|
||||
// without minting a duplicate CVPixelBuffer.
|
||||
cache.import(port_b, 0xAAAA_AAAA).expect("duplicate import is idempotent");
|
||||
cache.import(port_b, 0xAAAA_AAAA).map_err(|error| error.to_string())?;
|
||||
assert_eq!(cache.cached_surface_count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
/// (default — bit-identical to pre-flag builds) and `hardware` (real
|
||||
/// GPU adapter via the vendored `HardwareOffscreenContext`; requires
|
||||
/// the sidecar binary to be compiled with the `hardware-render`
|
||||
/// feature and a GPUI BGRA surface presenter). Anything else is
|
||||
/// feature and the local GPUI BGRA surface presenter). Anything else is
|
||||
/// silently dropped and the sidecar defaults to software so a typo'd
|
||||
/// value never blocks the browser from starting; the sidecar's own
|
||||
/// arg parser still errors loudly on an unrecognised value when set
|
||||
@@ -143,8 +143,8 @@ impl ServoLiveClient {
|
||||
// header so a buggy or hostile sidecar can't park us on
|
||||
// `read_exact` for an arbitrarily-sized buffer. The honest
|
||||
// upper limit is `width * height * 4` (RGBA8); `0` is the
|
||||
// explicit "hardware path active, sample the IOSurface
|
||||
// instead" signal — anything else is a protocol violation.
|
||||
// explicit "hardware path active, sample the IOSurface"
|
||||
// signal; any other byte count is a protocol violation.
|
||||
let pixel_byte_count =
|
||||
(report.width as u64).saturating_mul(report.height as u64).saturating_mul(4);
|
||||
let advertised = report.rgba_byte_count as u64;
|
||||
@@ -157,12 +157,10 @@ impl ServoLiveClient {
|
||||
});
|
||||
}
|
||||
|
||||
// Raw frame bytes follow the JSON header on the same pipe
|
||||
// ONLY when the sidecar didn't drop the payload for the
|
||||
// hardware path. `read_exact` drains BufReader's buffer first
|
||||
// Raw frame bytes follow the JSON header on the same pipe for
|
||||
// software frames. `read_exact` drains BufReader's buffer first
|
||||
// (the line read never crosses the `\n` boundary) and then
|
||||
// pulls the rest straight from the child's stdout — no
|
||||
// fs::read, no temp file.
|
||||
// pulls the rest straight from the child's stdout.
|
||||
let mut rgba_bytes = vec![0u8; report.rgba_byte_count];
|
||||
if report.rgba_byte_count > 0 {
|
||||
self.stdout.read_exact(&mut rgba_bytes).map_err(ServoLiveError::FrameRead)?;
|
||||
@@ -189,10 +187,8 @@ impl Drop for ServoLiveClient {
|
||||
#[cfg(target_os = "macos")]
|
||||
impl ServoLiveClient {
|
||||
/// Convert the sidecar's `surface_handle` into a `CVPixelBuffer`
|
||||
/// in the local cache. Failures are logged but don't error the
|
||||
/// request — the renderer falls back to the existing software
|
||||
/// `Arc<RenderImage>` path when no pixel buffer is available, so
|
||||
/// the user always sees a frame.
|
||||
/// in the local cache. A later frame with a missing pixel buffer
|
||||
/// becomes a web-surface error instead of a blank ready frame.
|
||||
fn import_iosurface_handle(&mut self, handle: &LiveSurfaceHandle) {
|
||||
match self.iosurface_cache.import(handle.mach_port_name, handle.surface_id) {
|
||||
Ok(()) => tracing::info!(
|
||||
@@ -266,10 +262,8 @@ pub(crate) struct ServoLiveFrame {
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
sample_hash: u64,
|
||||
rgba_bytes: Vec<u8>,
|
||||
/// Hardware-path companion: the imported IOSurface published by
|
||||
/// the sidecar. GPUI 0.2.2 presents `surface(...)` through its
|
||||
/// NV12 video path, so the current BGRA Servo surface stays as
|
||||
/// observability plumbing until a BGRA presenter lands.
|
||||
/// Hardware-path surface: the imported IOSurface published by the
|
||||
/// sidecar, wrapped as a CVPixelBuffer for GPUI's `surface(...)`.
|
||||
#[cfg(target_os = "macos")]
|
||||
pixel_buffer: Option<CVPixelBuffer>,
|
||||
}
|
||||
@@ -295,9 +289,7 @@ impl ServoLiveFrame {
|
||||
}
|
||||
|
||||
/// Returns the imported `CVPixelBuffer` matching the frame's
|
||||
/// current hardware surface, if any. The renderer keeps this as
|
||||
/// wire-path evidence while GPUI's public `surface(...)` element
|
||||
/// remains NV12-only.
|
||||
/// current hardware surface.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[must_use]
|
||||
pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> {
|
||||
@@ -371,6 +363,29 @@ impl ServoLiveFrame {
|
||||
pixel_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "macos"))]
|
||||
pub(crate) fn for_test_with_pixel_buffer(
|
||||
width: u32,
|
||||
height: u32,
|
||||
pixel_buffer: CVPixelBuffer,
|
||||
) -> Self {
|
||||
Self {
|
||||
loaded_url: Some("https://example.com/".to_string()),
|
||||
title: Some("Example".to_string()),
|
||||
render_state: "complete".to_string(),
|
||||
width,
|
||||
height,
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
non_white_pixel_count: 0,
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
content_pixel_count: 0,
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
sample_hash: 0,
|
||||
rgba_bytes: Vec::new(),
|
||||
pixel_buffer: Some(pixel_buffer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -417,13 +432,6 @@ fn rendering_context_from_env() -> Option<&'static str> {
|
||||
let raw = env::var(RENDERING_CONTEXT_ENV).ok()?;
|
||||
match rendering_context_selection(raw.as_str()) {
|
||||
RenderingContextSelection::Forward(value) => Some(value),
|
||||
RenderingContextSelection::HoldHardware => {
|
||||
tracing::warn!(
|
||||
target: "ely::servo::iosurface",
|
||||
"hardware rendering context requested; GPUI 0.2.2 surface presenter accepts NV12 CVPixelBuffers; Servo publishes BGRA IOSurfaces; using software rendering context",
|
||||
);
|
||||
None
|
||||
}
|
||||
RenderingContextSelection::Ignore => None,
|
||||
}
|
||||
}
|
||||
@@ -431,14 +439,13 @@ fn rendering_context_from_env() -> Option<&'static str> {
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum RenderingContextSelection {
|
||||
Forward(&'static str),
|
||||
HoldHardware,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
fn rendering_context_selection(raw: &str) -> RenderingContextSelection {
|
||||
match raw.to_lowercase().as_str() {
|
||||
"software" => RenderingContextSelection::Forward("software"),
|
||||
"hardware" => RenderingContextSelection::HoldHardware,
|
||||
"hardware" => RenderingContextSelection::Forward("hardware"),
|
||||
_ => RenderingContextSelection::Ignore,
|
||||
}
|
||||
}
|
||||
@@ -448,10 +455,10 @@ mod tests {
|
||||
use super::{RenderingContextSelection, rendering_context_selection};
|
||||
|
||||
#[test]
|
||||
fn hardware_env_is_held_until_gpui_can_present_bgra_surfaces() {
|
||||
fn hardware_env_forwards_to_the_sidecar() {
|
||||
assert_eq!(
|
||||
rendering_context_selection("hardware"),
|
||||
RenderingContextSelection::HoldHardware
|
||||
RenderingContextSelection::Forward("hardware")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ use std::{
|
||||
use thiserror::Error;
|
||||
|
||||
const SIDECAR_PATH_ENV: &str = "ELY_SERVO_SIDECAR";
|
||||
const RENDERING_CONTEXT_ENV: &str = "ELY_SERVO_RENDERING_CONTEXT";
|
||||
const SOFTWARE_SIDECAR_FEATURES: &str = "servo-engine";
|
||||
const HARDWARE_SIDECAR_FEATURES: &str = "servo-engine,hardware-render";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) enum SidecarCommandTarget {
|
||||
@@ -28,7 +31,7 @@ impl SidecarCommandTarget {
|
||||
.arg("-p")
|
||||
.arg("ely_servo_host")
|
||||
.arg("--features")
|
||||
.arg("servo-engine")
|
||||
.arg(sidecar_features_from_env())
|
||||
.arg("--bin")
|
||||
.arg("ely_servo_sidecar")
|
||||
.arg("--");
|
||||
@@ -64,13 +67,17 @@ pub(super) fn default_sidecar_command() -> Result<SidecarCommandTarget, SidecarC
|
||||
SidecarCommandError::CurrentExecutableDirectoryUnavailable { path: current_exe.clone() }
|
||||
})?;
|
||||
let adjacent_sidecar = exe_dir.join(sidecar_binary_name());
|
||||
if adjacent_sidecar.is_file() {
|
||||
let workspace_manifest = workspace_manifest_path();
|
||||
let prefer_cargo_hardware_sidecar = hardware_rendering_context_requested()
|
||||
&& workspace_manifest.as_ref().is_some_and(|path| path.is_file());
|
||||
if adjacent_sidecar.is_file() && !prefer_cargo_hardware_sidecar {
|
||||
return Ok(SidecarCommandTarget::Binary(adjacent_sidecar));
|
||||
}
|
||||
|
||||
if let Some(manifest_path) = workspace_manifest_path() {
|
||||
if let Some(manifest_path) = workspace_manifest {
|
||||
if let Some(target_sidecar) = workspace_target_sidecar_path(&manifest_path)
|
||||
&& target_sidecar.is_file()
|
||||
&& !prefer_cargo_hardware_sidecar
|
||||
{
|
||||
return Ok(SidecarCommandTarget::Binary(target_sidecar));
|
||||
}
|
||||
@@ -86,6 +93,30 @@ fn workspace_manifest_path() -> Option<PathBuf> {
|
||||
option_env!("ELY_WORKSPACE_MANIFEST").map(PathBuf::from)
|
||||
}
|
||||
|
||||
fn hardware_rendering_context_requested() -> bool {
|
||||
env::var(RENDERING_CONTEXT_ENV).ok().as_deref().is_some_and(rendering_context_requests_hardware)
|
||||
}
|
||||
|
||||
fn sidecar_features_from_env() -> &'static str {
|
||||
env::var(RENDERING_CONTEXT_ENV)
|
||||
.ok()
|
||||
.as_deref()
|
||||
.map(sidecar_features_for_rendering_context)
|
||||
.unwrap_or(SOFTWARE_SIDECAR_FEATURES)
|
||||
}
|
||||
|
||||
fn sidecar_features_for_rendering_context(raw: &str) -> &'static str {
|
||||
if rendering_context_requests_hardware(raw) {
|
||||
HARDWARE_SIDECAR_FEATURES
|
||||
} else {
|
||||
SOFTWARE_SIDECAR_FEATURES
|
||||
}
|
||||
}
|
||||
|
||||
fn rendering_context_requests_hardware(raw: &str) -> bool {
|
||||
raw.eq_ignore_ascii_case("hardware")
|
||||
}
|
||||
|
||||
fn workspace_target_sidecar_path(manifest_path: &Path) -> Option<PathBuf> {
|
||||
let profile = if cfg!(debug_assertions) { "debug" } else { "release" };
|
||||
Some(manifest_path.parent()?.join("target").join(profile).join(sidecar_binary_name()))
|
||||
@@ -94,3 +125,23 @@ fn workspace_target_sidecar_path(manifest_path: &Path) -> Option<PathBuf> {
|
||||
fn sidecar_binary_name() -> String {
|
||||
format!("ely_servo_sidecar{}", env::consts::EXE_SUFFIX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
HARDWARE_SIDECAR_FEATURES, SOFTWARE_SIDECAR_FEATURES,
|
||||
sidecar_features_for_rendering_context,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn hardware_rendering_context_enables_hardware_sidecar_feature() {
|
||||
assert_eq!(sidecar_features_for_rendering_context("hardware"), HARDWARE_SIDECAR_FEATURES);
|
||||
assert_eq!(sidecar_features_for_rendering_context("HARDWARE"), HARDWARE_SIDECAR_FEATURES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn software_and_unknown_contexts_use_software_sidecar_feature() {
|
||||
assert_eq!(sidecar_features_for_rendering_context("software"), SOFTWARE_SIDECAR_FEATURES);
|
||||
assert_eq!(sidecar_features_for_rendering_context("garbage"), SOFTWARE_SIDECAR_FEATURES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,12 +177,12 @@ impl WebSurfaceStore {
|
||||
position: Point<Pixels>,
|
||||
scale_factor: f32,
|
||||
) -> WebSurfaceInputOutcome {
|
||||
let surface =
|
||||
self.surfaces.get_mut(tab_id).filter(|surface| surface.viewport_bounds.is_some());
|
||||
let Some(surface) = surface else {
|
||||
let Some(surface) = self.surfaces.get_mut(tab_id) else {
|
||||
return WebSurfaceInputOutcome::DroppedNoViewportBounds;
|
||||
};
|
||||
let Some(bounds) = surface.viewport_bounds else {
|
||||
return WebSurfaceInputOutcome::DroppedNoViewportBounds;
|
||||
};
|
||||
let bounds = surface.viewport_bounds.expect("viewport_bounds checked above");
|
||||
let Some(point) =
|
||||
WebSurfaceClickPoint::from_window_position(bounds, position, scale_factor)
|
||||
else {
|
||||
|
||||
@@ -57,14 +57,12 @@ pub(super) struct WebSurfaceFrame {
|
||||
content_pixel_count: u64,
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
sample_hash: u64,
|
||||
/// Software-path image. Current GPUI builds require this for every
|
||||
/// ready web frame because BGRA IOSurface presentation is still
|
||||
/// held at the protocol boundary.
|
||||
/// Software-path image built from RGBA bytes when the sidecar runs
|
||||
/// without hardware surface publication.
|
||||
pub(super) image: Option<Arc<RenderImage>>,
|
||||
/// Hardware-path companion imported from the sidecar. GPUI 0.2.2's
|
||||
/// public `surface(...)` presenter accepts NV12 video buffers, and
|
||||
/// Servo publishes BGRA IOSurfaces; this remains observability
|
||||
/// state until a BGRA presenter is available.
|
||||
/// Hardware-path surface imported from the sidecar's IOSurface.
|
||||
/// GPUI is patched locally to present BGRA CVPixelBuffers through
|
||||
/// `surface(...)`, so hardware frames can skip the RGBA pipe.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) pixel_buffer: Option<CVPixelBuffer>,
|
||||
}
|
||||
@@ -102,10 +100,18 @@ impl WebSurfaceFrame {
|
||||
}
|
||||
|
||||
fn from_parts(parts: WebSurfaceFrameParts) -> Result<Self, WebSurfaceError> {
|
||||
if parts.rgba_bytes.is_empty() {
|
||||
#[cfg(target_os = "macos")]
|
||||
let has_pixel_buffer = parts.pixel_buffer.is_some();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let has_pixel_buffer = false;
|
||||
|
||||
if parts.rgba_bytes.is_empty() && !has_pixel_buffer {
|
||||
return Err(WebSurfaceError::MissingRenderablePayload);
|
||||
}
|
||||
|
||||
let image = if parts.rgba_bytes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
// Servo's `read_pixels(gl::RGBA, gl::UNSIGNED_BYTE)` writes
|
||||
// R-G-B-A in memory order. GPUI's `RenderImage` is documented
|
||||
// as "in BGRA format" and uploads via
|
||||
@@ -118,7 +124,8 @@ impl WebSurfaceFrame {
|
||||
let mut bytes = parts.rgba_bytes;
|
||||
swap_red_blue_in_place(&mut bytes);
|
||||
let bytes_hash = rgba_hash(&bytes);
|
||||
let image = Some(resolve_render_image(parts.width, parts.height, bytes, bytes_hash)?);
|
||||
Some(resolve_render_image(parts.width, parts.height, bytes, bytes_hash)?)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
requested_url: parts.requested_url,
|
||||
@@ -240,9 +247,7 @@ struct WebSurfaceFrameParts {
|
||||
pub(super) enum WebSurfaceError {
|
||||
#[error("invalid servo frame buffer for {width}x{height}")]
|
||||
InvalidFrameBuffer { width: u32, height: u32 },
|
||||
#[error(
|
||||
"servo live frame did not include renderable pixels; BGRA IOSurface presentation is unavailable in GPUI 0.2.2"
|
||||
)]
|
||||
#[error("servo live frame did not include a software image or hardware IOSurface")]
|
||||
MissingRenderablePayload,
|
||||
}
|
||||
|
||||
@@ -269,11 +274,11 @@ fn resolve_render_image(
|
||||
) -> Result<Arc<RenderImage>, WebSurfaceError> {
|
||||
LAST_FRAME_IMAGE.with(|cache| -> Result<Arc<RenderImage>, WebSurfaceError> {
|
||||
let mut cache = cache.borrow_mut();
|
||||
if let Some((cached_hash, cached_image)) = cache.as_ref() {
|
||||
if *cached_hash == bytes_hash {
|
||||
if let Some((cached_hash, cached_image)) = cache.as_ref()
|
||||
&& *cached_hash == bytes_hash
|
||||
{
|
||||
return Ok(cached_image.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let image_buffer = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, rgba_bytes)
|
||||
.ok_or(WebSurfaceError::InvalidFrameBuffer { width, height })?;
|
||||
|
||||
@@ -384,7 +384,7 @@ fn live_frame_swaps_red_and_blue_bytes_for_gpui_bgra() -> Result<(), Box<dyn Err
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_live_frame_payload_is_rejected() {
|
||||
fn empty_live_frame_payload_is_rejected() -> Result<(), String> {
|
||||
use crate::services::servo_live::ServoLiveFrame;
|
||||
use crate::shell::web_surface_frame::WebSurfaceFrame;
|
||||
use crate::shell::web_surface_geometry::WebSurfaceScrollOffset;
|
||||
@@ -396,13 +396,40 @@ fn empty_live_frame_payload_is_rejected() {
|
||||
ServoLiveFrame::for_test(1, 1, Vec::new()),
|
||||
);
|
||||
|
||||
let Err(error) = result else {
|
||||
panic!("empty Servo frame payload must be rejected before it reaches Ready state");
|
||||
let error = match result {
|
||||
Ok(_) => return Err("empty Servo frame payload reached Ready state".to_string()),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"servo live frame did not include renderable pixels; BGRA IOSurface presentation is unavailable in GPUI 0.2.2",
|
||||
"servo live frame did not include a software image or hardware IOSurface",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn hardware_live_frame_with_pixel_buffer_skips_software_image() -> Result<(), String> {
|
||||
use core_video::pixel_buffer::{CVPixelBuffer, kCVPixelFormatType_32BGRA};
|
||||
|
||||
use crate::services::servo_live::ServoLiveFrame;
|
||||
use crate::shell::web_surface_frame::WebSurfaceFrame;
|
||||
use crate::shell::web_surface_geometry::WebSurfaceScrollOffset;
|
||||
|
||||
let pixel_buffer = CVPixelBuffer::new(kCVPixelFormatType_32BGRA, 1, 1, None)
|
||||
.map_err(|status| format!("CVPixelBufferCreate returned status {status}"))?;
|
||||
let live = ServoLiveFrame::for_test_with_pixel_buffer(1, 1, pixel_buffer);
|
||||
let frame = WebSurfaceFrame::from_live_frame(
|
||||
"https://example.com/".to_string(),
|
||||
WebSurfaceScrollOffset::default(),
|
||||
100,
|
||||
live,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
assert!(frame.image.is_none(), "hardware frame should use the CVPixelBuffer surface path");
|
||||
assert!(frame.pixel_buffer.is_some(), "hardware frame should carry the imported CVPixelBuffer");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn web_bounds() -> Bounds<gpui::Pixels> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ely_domain::{BrowserTab, TabId};
|
||||
use gpui::{
|
||||
AnyElement, App, Entity, ImageSource, InteractiveElement, IntoElement, MouseButton, ObjectFit,
|
||||
ParentElement, Styled, StyledImage, Window, canvas, div, img, px, rgb,
|
||||
ParentElement, Styled, StyledImage, Window, canvas, div, img, px, rgb, surface,
|
||||
};
|
||||
|
||||
use super::{ElyShell, web_surface_frame::WebSurfaceFrame};
|
||||
@@ -12,22 +12,15 @@ pub(super) fn render_ready_web_surface(
|
||||
tab: &BrowserTab,
|
||||
state_entity: Entity<ElyShell>,
|
||||
) -> AnyElement {
|
||||
// T14: the `gpui::surface(...)` hardware path is held.
|
||||
//
|
||||
// GPUI 0.2.2's Blade Metal renderer hard-asserts that any
|
||||
// CVPixelBuffer handed to `surface(...)` is NV12 YUV
|
||||
// (kCVPixelFormatType_420YpCbCr8BiPlanarFullRange). See
|
||||
// ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/
|
||||
// gpui-0.2.2/src/platform/blade/blade_renderer.rs:832
|
||||
// for the assert. Our sidecar produces BGRA IOSurfaces, so
|
||||
// calling `surface()` with that buffer panics the renderer on
|
||||
// the first frame.
|
||||
//
|
||||
// We keep `services::iosurface_metal` and
|
||||
// `WebSurfaceFrame::pixel_buffer` intact so the wire-side
|
||||
// IOSurfaceHandle import path stays exercised; once GPUI gains a
|
||||
// BGRA-capable Surface element this branch can come back. Until
|
||||
// then every frame must go through `img()` below.
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(pixel_buffer) = frame.pixel_buffer.as_ref() {
|
||||
return render_web_surface(
|
||||
tab,
|
||||
state_entity,
|
||||
surface(pixel_buffer.clone()).size_full().object_fit(ObjectFit::Fill),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(image) = frame.image.as_ref() {
|
||||
return render_web_surface(
|
||||
tab,
|
||||
|
||||
@@ -38,7 +38,7 @@ fn constructs_or_explains_why_not() {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn extracts_iosurface_mach_port_from_current_surface() {
|
||||
fn extracts_iosurface_mach_port_from_current_surface() -> Result<(), String> {
|
||||
let width = 256;
|
||||
let height = 192;
|
||||
let context = match HardwareOffscreenContext::new(PhysicalSize::new(width, height)) {
|
||||
@@ -48,13 +48,13 @@ fn extracts_iosurface_mach_port_from_current_surface() {
|
||||
"hardware GL adapter not available on this host \
|
||||
(acceptable in headless / no-GPU environments): {error:?}"
|
||||
);
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let first = context
|
||||
.current_iosurface_mach_port()
|
||||
.expect("first IOSurface mach port extraction must succeed");
|
||||
.map_err(|error| format!("first IOSurface mach port extraction failed: {error:?}"))?;
|
||||
assert!(
|
||||
first.mach_port_name != 0,
|
||||
"IOSurfaceCreateMachPort must return a non-null mach_port_t (got 0)"
|
||||
@@ -67,8 +67,9 @@ fn extracts_iosurface_mach_port_from_current_surface() {
|
||||
// a stale `Framebuffer::None`.
|
||||
let second = context
|
||||
.current_iosurface_mach_port()
|
||||
.expect("repeated mach port extraction must succeed after rebind");
|
||||
.map_err(|error| format!("repeated mach port extraction failed: {error:?}"))?;
|
||||
assert!(second.mach_port_name != 0);
|
||||
assert_eq!(second.width, width);
|
||||
assert_eq!(second.height, height);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ macos_dir="${contents_dir}/MacOS"
|
||||
resources_dir="${contents_dir}/Resources"
|
||||
|
||||
cargo build -p ely_app
|
||||
cargo build -p ely_servo_host --features servo-engine --bin ely_servo_sidecar
|
||||
cargo build -p ely_servo_host --features servo-engine,hardware-render --bin ely_servo_sidecar
|
||||
|
||||
rm -rf "${bundle_root}"
|
||||
mkdir -p "${macos_dir}" "${resources_dir}"
|
||||
|
||||
Vendored
+624
@@ -0,0 +1,624 @@
|
||||
# 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.
|
||||
#
|
||||
# Local copy trims registry examples/tests/docs; runtime dependencies
|
||||
# and library/build targets stay aligned with the published crate.
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
Vendored
+222
@@ -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
|
||||
Vendored
+66
@@ -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).
|
||||
Vendored
+457
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
#define RT_MANIFEST 24
|
||||
1 RT_MANIFEST "resources/windows/gpui.manifest.xml"
|
||||
+140
@@ -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);
|
||||
//! });
|
||||
//! ```
|
||||
Vendored
+440
@@ -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()
|
||||
}
|
||||
}
|
||||
Vendored
+2460
File diff suppressed because it is too large
Load Diff
+488
@@ -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);
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+824
@@ -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
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
+1047
File diff suppressed because it is too large
Load Diff
Vendored
+289
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+84
@@ -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()
|
||||
}
|
||||
Vendored
+107
@@ -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()
|
||||
}
|
||||
}
|
||||
Vendored
+337
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+934
@@ -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());
|
||||
}
|
||||
}
|
||||
Vendored
+122
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+769
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+3250
File diff suppressed because it is too large
Load Diff
+353
@@ -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()
|
||||
}
|
||||
}
|
||||
Vendored
+767
@@ -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
File diff suppressed because it is too large
Load Diff
Vendored
+25
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+221
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+611
@@ -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());
|
||||
}
|
||||
}
|
||||
Vendored
+3912
File diff suppressed because it is too large
Load Diff
Vendored
+75
@@ -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)
|
||||
}
|
||||
}
|
||||
Vendored
+311
@@ -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,
|
||||
}
|
||||
Vendored
+180
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+254
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+670
@@ -0,0 +1,670 @@
|
||||
use crate::{
|
||||
Bounds, Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render,
|
||||
Window, point, seal::Sealed,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf};
|
||||
|
||||
/// An event from a platform input source.
|
||||
pub trait InputEvent: Sealed + 'static {
|
||||
/// Convert this event into the platform input enum.
|
||||
fn to_platform_input(self) -> PlatformInput;
|
||||
}
|
||||
|
||||
/// A key event from the platform.
|
||||
pub trait KeyEvent: InputEvent {}
|
||||
|
||||
/// A mouse event from the platform.
|
||||
pub trait MouseEvent: InputEvent {}
|
||||
|
||||
/// The key down event equivalent for the platform.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct KeyDownEvent {
|
||||
/// The keystroke that was generated.
|
||||
pub keystroke: Keystroke,
|
||||
|
||||
/// Whether the key is currently held down.
|
||||
pub is_held: bool,
|
||||
}
|
||||
|
||||
impl Sealed for KeyDownEvent {}
|
||||
impl InputEvent for KeyDownEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::KeyDown(self)
|
||||
}
|
||||
}
|
||||
impl KeyEvent for KeyDownEvent {}
|
||||
|
||||
/// The key up event equivalent for the platform.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyUpEvent {
|
||||
/// The keystroke that was released.
|
||||
pub keystroke: Keystroke,
|
||||
}
|
||||
|
||||
impl Sealed for KeyUpEvent {}
|
||||
impl InputEvent for KeyUpEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::KeyUp(self)
|
||||
}
|
||||
}
|
||||
impl KeyEvent for KeyUpEvent {}
|
||||
|
||||
/// The modifiers changed event equivalent for the platform.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ModifiersChangedEvent {
|
||||
/// The new state of the modifier keys
|
||||
pub modifiers: Modifiers,
|
||||
/// The new state of the capslock key
|
||||
pub capslock: Capslock,
|
||||
}
|
||||
|
||||
impl Sealed for ModifiersChangedEvent {}
|
||||
impl InputEvent for ModifiersChangedEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::ModifiersChanged(self)
|
||||
}
|
||||
}
|
||||
impl KeyEvent for ModifiersChangedEvent {}
|
||||
|
||||
impl Deref for ModifiersChangedEvent {
|
||||
type Target = Modifiers;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// The phase of a touch motion event.
|
||||
/// Based on the winit enum of the same name.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum TouchPhase {
|
||||
/// The touch started.
|
||||
Started,
|
||||
/// The touch event is moving.
|
||||
#[default]
|
||||
Moved,
|
||||
/// The touch phase has ended
|
||||
Ended,
|
||||
}
|
||||
|
||||
/// A mouse down event from the platform
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseDownEvent {
|
||||
/// Which mouse button was pressed.
|
||||
pub button: MouseButton,
|
||||
|
||||
/// The position of the mouse on the window.
|
||||
pub position: Point<Pixels>,
|
||||
|
||||
/// The modifiers that were held down when the mouse was pressed.
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
/// The number of times the button has been clicked.
|
||||
pub click_count: usize,
|
||||
|
||||
/// Whether this is the first, focusing click.
|
||||
pub first_mouse: bool,
|
||||
}
|
||||
|
||||
impl Sealed for MouseDownEvent {}
|
||||
impl InputEvent for MouseDownEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MouseDown(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MouseDownEvent {}
|
||||
|
||||
/// A mouse up event from the platform
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseUpEvent {
|
||||
/// Which mouse button was released.
|
||||
pub button: MouseButton,
|
||||
|
||||
/// The position of the mouse on the window.
|
||||
pub position: Point<Pixels>,
|
||||
|
||||
/// The modifiers that were held down when the mouse was released.
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
/// The number of times the button has been clicked.
|
||||
pub click_count: usize,
|
||||
}
|
||||
|
||||
impl Sealed for MouseUpEvent {}
|
||||
impl InputEvent for MouseUpEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MouseUp(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MouseUpEvent {}
|
||||
|
||||
/// A click event, generated when a mouse button is pressed and released.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseClickEvent {
|
||||
/// The mouse event when the button was pressed.
|
||||
pub down: MouseDownEvent,
|
||||
|
||||
/// The mouse event when the button was released.
|
||||
pub up: MouseUpEvent,
|
||||
}
|
||||
|
||||
/// A click event that was generated by a keyboard button being pressed and released.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct KeyboardClickEvent {
|
||||
/// The keyboard button that was pressed to trigger the click.
|
||||
pub button: KeyboardButton,
|
||||
|
||||
/// The bounds of the element that was clicked.
|
||||
pub bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
/// A click event, generated when a mouse button or keyboard button is pressed and released.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ClickEvent {
|
||||
/// A click event trigger by a mouse button being pressed and released.
|
||||
Mouse(MouseClickEvent),
|
||||
/// A click event trigger by a keyboard button being pressed and released.
|
||||
Keyboard(KeyboardClickEvent),
|
||||
}
|
||||
|
||||
impl Default for ClickEvent {
|
||||
fn default() -> Self {
|
||||
ClickEvent::Keyboard(KeyboardClickEvent::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ClickEvent {
|
||||
/// Returns the modifiers that were held during the click event
|
||||
///
|
||||
/// `Keyboard`: The keyboard click events never have modifiers.
|
||||
/// `Mouse`: Modifiers that were held during the mouse key up event.
|
||||
pub fn modifiers(&self) -> Modifiers {
|
||||
match self {
|
||||
// Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default
|
||||
ClickEvent::Keyboard(_) => Modifiers::default(),
|
||||
// Click events on the web only reflect the modifiers for the keyup event,
|
||||
// tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138
|
||||
// under various combinations of modifiers and keyUp / keyDown events.
|
||||
ClickEvent::Mouse(event) => event.up.modifiers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the position of the click event
|
||||
///
|
||||
/// `Keyboard`: The bottom left corner of the clicked hitbox
|
||||
/// `Mouse`: The position of the mouse when the button was released.
|
||||
pub fn position(&self) -> Point<Pixels> {
|
||||
match self {
|
||||
ClickEvent::Keyboard(event) => event.bounds.bottom_left(),
|
||||
ClickEvent::Mouse(event) => event.up.position,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the mouse position of the click event
|
||||
///
|
||||
/// `Keyboard`: None
|
||||
/// `Mouse`: The position of the mouse when the button was released.
|
||||
pub fn mouse_position(&self) -> Option<Point<Pixels>> {
|
||||
match self {
|
||||
ClickEvent::Keyboard(_) => None,
|
||||
ClickEvent::Mouse(event) => Some(event.up.position),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if this was a right click
|
||||
///
|
||||
/// `Keyboard`: false
|
||||
/// `Mouse`: Whether the right button was pressed and released
|
||||
pub fn is_right_click(&self) -> bool {
|
||||
match self {
|
||||
ClickEvent::Keyboard(_) => false,
|
||||
ClickEvent::Mouse(event) => {
|
||||
event.down.button == MouseButton::Right && event.up.button == MouseButton::Right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the click was a standard click
|
||||
///
|
||||
/// `Keyboard`: Always true
|
||||
/// `Mouse`: Left button pressed and released
|
||||
pub fn standard_click(&self) -> bool {
|
||||
match self {
|
||||
ClickEvent::Keyboard(_) => true,
|
||||
ClickEvent::Mouse(event) => {
|
||||
event.down.button == MouseButton::Left && event.up.button == MouseButton::Left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the click focused the element
|
||||
///
|
||||
/// `Keyboard`: false, keyboard clicks only work if an element is already focused
|
||||
/// `Mouse`: Whether this was the first focusing click
|
||||
pub fn first_focus(&self) -> bool {
|
||||
match self {
|
||||
ClickEvent::Keyboard(_) => false,
|
||||
ClickEvent::Mouse(event) => event.down.first_mouse,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the click count of the click event
|
||||
///
|
||||
/// `Keyboard`: Always 1
|
||||
/// `Mouse`: Count of clicks from MouseUpEvent
|
||||
pub fn click_count(&self) -> usize {
|
||||
match self {
|
||||
ClickEvent::Keyboard(_) => 1,
|
||||
ClickEvent::Mouse(event) => event.up.click_count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the click event is generated by a keyboard event
|
||||
pub fn is_keyboard(&self) -> bool {
|
||||
match self {
|
||||
ClickEvent::Mouse(_) => false,
|
||||
ClickEvent::Keyboard(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum representing the keyboard button that was pressed for a click event.
|
||||
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
|
||||
pub enum KeyboardButton {
|
||||
/// Enter key was clicked
|
||||
#[default]
|
||||
Enter,
|
||||
/// Space key was clicked
|
||||
Space,
|
||||
}
|
||||
|
||||
/// An enum representing the mouse button that was pressed.
|
||||
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum MouseButton {
|
||||
/// The left mouse button.
|
||||
Left,
|
||||
|
||||
/// The right mouse button.
|
||||
Right,
|
||||
|
||||
/// The middle mouse button.
|
||||
Middle,
|
||||
|
||||
/// A navigation button, such as back or forward.
|
||||
Navigate(NavigationDirection),
|
||||
}
|
||||
|
||||
impl MouseButton {
|
||||
/// Get all the mouse buttons in a list.
|
||||
pub fn all() -> Vec<Self> {
|
||||
vec![
|
||||
MouseButton::Left,
|
||||
MouseButton::Right,
|
||||
MouseButton::Middle,
|
||||
MouseButton::Navigate(NavigationDirection::Back),
|
||||
MouseButton::Navigate(NavigationDirection::Forward),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MouseButton {
|
||||
fn default() -> Self {
|
||||
Self::Left
|
||||
}
|
||||
}
|
||||
|
||||
/// A navigation direction, such as back or forward.
|
||||
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum NavigationDirection {
|
||||
/// The back button.
|
||||
Back,
|
||||
|
||||
/// The forward button.
|
||||
Forward,
|
||||
}
|
||||
|
||||
impl Default for NavigationDirection {
|
||||
fn default() -> Self {
|
||||
Self::Back
|
||||
}
|
||||
}
|
||||
|
||||
/// A mouse move event from the platform
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseMoveEvent {
|
||||
/// The position of the mouse on the window.
|
||||
pub position: Point<Pixels>,
|
||||
|
||||
/// The mouse button that was pressed, if any.
|
||||
pub pressed_button: Option<MouseButton>,
|
||||
|
||||
/// The modifiers that were held down when the mouse was moved.
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Sealed for MouseMoveEvent {}
|
||||
impl InputEvent for MouseMoveEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MouseMove(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MouseMoveEvent {}
|
||||
|
||||
impl MouseMoveEvent {
|
||||
/// Returns true if the left mouse button is currently held down.
|
||||
pub fn dragging(&self) -> bool {
|
||||
self.pressed_button == Some(MouseButton::Left)
|
||||
}
|
||||
}
|
||||
|
||||
/// A mouse wheel event from the platform
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ScrollWheelEvent {
|
||||
/// The position of the mouse on the window.
|
||||
pub position: Point<Pixels>,
|
||||
|
||||
/// The change in scroll wheel position for this event.
|
||||
pub delta: ScrollDelta,
|
||||
|
||||
/// The modifiers that were held down when the mouse was moved.
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
/// The phase of the touch event.
|
||||
pub touch_phase: TouchPhase,
|
||||
}
|
||||
|
||||
impl Sealed for ScrollWheelEvent {}
|
||||
impl InputEvent for ScrollWheelEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::ScrollWheel(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for ScrollWheelEvent {}
|
||||
|
||||
impl Deref for ScrollWheelEvent {
|
||||
type Target = Modifiers;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// The scroll delta for a scroll wheel event.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ScrollDelta {
|
||||
/// An exact scroll delta in pixels.
|
||||
Pixels(Point<Pixels>),
|
||||
/// An inexact scroll delta in lines.
|
||||
Lines(Point<f32>),
|
||||
}
|
||||
|
||||
impl Default for ScrollDelta {
|
||||
fn default() -> Self {
|
||||
Self::Lines(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollDelta {
|
||||
/// Returns true if this is a precise scroll delta in pixels.
|
||||
pub fn precise(&self) -> bool {
|
||||
match self {
|
||||
ScrollDelta::Pixels(_) => true,
|
||||
ScrollDelta::Lines(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this scroll event into exact pixels.
|
||||
pub fn pixel_delta(&self, line_height: Pixels) -> Point<Pixels> {
|
||||
match self {
|
||||
ScrollDelta::Pixels(delta) => *delta,
|
||||
ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines two scroll deltas into one.
|
||||
/// If the signs of the deltas are the same (both positive or both negative),
|
||||
/// the deltas are added together. If the signs are opposite, the second delta
|
||||
/// (other) is used, effectively overriding the first delta.
|
||||
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
|
||||
match (self, other) {
|
||||
(ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => {
|
||||
let x = if a.x.signum() == b.x.signum() {
|
||||
a.x + b.x
|
||||
} else {
|
||||
b.x
|
||||
};
|
||||
|
||||
let y = if a.y.signum() == b.y.signum() {
|
||||
a.y + b.y
|
||||
} else {
|
||||
b.y
|
||||
};
|
||||
|
||||
ScrollDelta::Pixels(point(x, y))
|
||||
}
|
||||
|
||||
(ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => {
|
||||
let x = if a.x.signum() == b.x.signum() {
|
||||
a.x + b.x
|
||||
} else {
|
||||
b.x
|
||||
};
|
||||
|
||||
let y = if a.y.signum() == b.y.signum() {
|
||||
a.y + b.y
|
||||
} else {
|
||||
b.y
|
||||
};
|
||||
|
||||
ScrollDelta::Lines(point(x, y))
|
||||
}
|
||||
|
||||
_ => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A mouse exit event from the platform, generated when the mouse leaves the window.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseExitEvent {
|
||||
/// The position of the mouse relative to the window.
|
||||
pub position: Point<Pixels>,
|
||||
/// The mouse button that was pressed, if any.
|
||||
pub pressed_button: Option<MouseButton>,
|
||||
/// The modifiers that were held down when the mouse was moved.
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Sealed for MouseExitEvent {}
|
||||
impl InputEvent for MouseExitEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MouseExited(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MouseExitEvent {}
|
||||
|
||||
impl Deref for MouseExitEvent {
|
||||
type Target = Modifiers;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of paths from the platform, such as from a file drop.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
|
||||
|
||||
impl ExternalPaths {
|
||||
/// Convert this collection of paths into a slice.
|
||||
pub fn paths(&self) -> &[PathBuf] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExternalPaths {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
// the platform will render icons for the dragged files
|
||||
Empty
|
||||
}
|
||||
}
|
||||
|
||||
/// A file drop event from the platform, generated when files are dragged and dropped onto the window.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FileDropEvent {
|
||||
/// The files have entered the window.
|
||||
Entered {
|
||||
/// The position of the mouse relative to the window.
|
||||
position: Point<Pixels>,
|
||||
/// The paths of the files that are being dragged.
|
||||
paths: ExternalPaths,
|
||||
},
|
||||
/// The files are being dragged over the window
|
||||
Pending {
|
||||
/// The position of the mouse relative to the window.
|
||||
position: Point<Pixels>,
|
||||
},
|
||||
/// The files have been dropped onto the window.
|
||||
Submit {
|
||||
/// The position of the mouse relative to the window.
|
||||
position: Point<Pixels>,
|
||||
},
|
||||
/// The user has stopped dragging the files over the window.
|
||||
Exited,
|
||||
}
|
||||
|
||||
impl Sealed for FileDropEvent {}
|
||||
impl InputEvent for FileDropEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::FileDrop(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for FileDropEvent {}
|
||||
|
||||
/// An enum corresponding to all kinds of platform input events.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PlatformInput {
|
||||
/// A key was pressed.
|
||||
KeyDown(KeyDownEvent),
|
||||
/// A key was released.
|
||||
KeyUp(KeyUpEvent),
|
||||
/// The keyboard modifiers were changed.
|
||||
ModifiersChanged(ModifiersChangedEvent),
|
||||
/// The mouse was pressed.
|
||||
MouseDown(MouseDownEvent),
|
||||
/// The mouse was released.
|
||||
MouseUp(MouseUpEvent),
|
||||
/// The mouse was moved.
|
||||
MouseMove(MouseMoveEvent),
|
||||
/// The mouse exited the window.
|
||||
MouseExited(MouseExitEvent),
|
||||
/// The scroll wheel was used.
|
||||
ScrollWheel(ScrollWheelEvent),
|
||||
/// Files were dragged and dropped onto the window.
|
||||
FileDrop(FileDropEvent),
|
||||
}
|
||||
|
||||
impl PlatformInput {
|
||||
pub(crate) fn mouse_event(&self) -> Option<&dyn Any> {
|
||||
match self {
|
||||
PlatformInput::KeyDown { .. } => None,
|
||||
PlatformInput::KeyUp { .. } => None,
|
||||
PlatformInput::ModifiersChanged { .. } => None,
|
||||
PlatformInput::MouseDown(event) => Some(event),
|
||||
PlatformInput::MouseUp(event) => Some(event),
|
||||
PlatformInput::MouseMove(event) => Some(event),
|
||||
PlatformInput::MouseExited(event) => Some(event),
|
||||
PlatformInput::ScrollWheel(event) => Some(event),
|
||||
PlatformInput::FileDrop(event) => Some(event),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> {
|
||||
match self {
|
||||
PlatformInput::KeyDown(event) => Some(event),
|
||||
PlatformInput::KeyUp(event) => Some(event),
|
||||
PlatformInput::ModifiersChanged(event) => Some(event),
|
||||
PlatformInput::MouseDown(_) => None,
|
||||
PlatformInput::MouseUp(_) => None,
|
||||
PlatformInput::MouseMove(_) => None,
|
||||
PlatformInput::MouseExited(_) => None,
|
||||
PlatformInput::ScrollWheel(_) => None,
|
||||
PlatformInput::FileDrop(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use crate::{
|
||||
self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement,
|
||||
KeyBinding, Keystroke, ParentElement, Render, TestAppContext, Window, div,
|
||||
};
|
||||
|
||||
struct TestView {
|
||||
saw_key_down: bool,
|
||||
saw_action: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
actions!(test_only, [TestAction]);
|
||||
|
||||
impl Render for TestView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().id("testview").child(
|
||||
div()
|
||||
.key_context("parent")
|
||||
.on_key_down(cx.listener(|this, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
this.saw_key_down = true
|
||||
}))
|
||||
.on_action(cx.listener(|this: &mut TestView, _: &TestAction, _, _| {
|
||||
this.saw_action = true
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.key_context("nested")
|
||||
.track_focus(&self.focus_handle)
|
||||
.into_element(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_on_events(cx: &mut TestAppContext) {
|
||||
let window = cx.update(|cx| {
|
||||
cx.open_window(Default::default(), |_, cx| {
|
||||
cx.new(|cx| TestView {
|
||||
saw_key_down: false,
|
||||
saw_action: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]);
|
||||
});
|
||||
|
||||
window
|
||||
.update(cx, |test_view, window, _cx| {
|
||||
window.focus(&test_view.focus_handle)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
|
||||
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
|
||||
|
||||
window
|
||||
.update(cx, |test_view, _, _| {
|
||||
assert!(test_view.saw_key_down || test_view.saw_action);
|
||||
assert!(test_view.saw_key_down);
|
||||
assert!(test_view.saw_action);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Vendored
+843
@@ -0,0 +1,843 @@
|
||||
//! KeyDispatch is where GPUI deals with binding actions to key events.
|
||||
//!
|
||||
//! The key pieces to making a key binding work are to define an action,
|
||||
//! implement a method that takes that action as a type parameter,
|
||||
//! and then to register the action during render on a focused node
|
||||
//! with a keymap context:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! actions!(editor,[Undo, Redo]);
|
||||
//!
|
||||
//! impl Editor {
|
||||
//! fn undo(&mut self, _: &Undo, _window: &mut Window, _cx: &mut Context<Self>) { ... }
|
||||
//! fn redo(&mut self, _: &Redo, _window: &mut Window, _cx: &mut Context<Self>) { ... }
|
||||
//! }
|
||||
//!
|
||||
//! impl Render for Editor {
|
||||
//! fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
//! div()
|
||||
//! .track_focus(&self.focus_handle(cx))
|
||||
//! .key_context("Editor")
|
||||
//! .on_action(cx.listener(Editor::undo))
|
||||
//! .on_action(cx.listener(Editor::redo))
|
||||
//! ...
|
||||
//! }
|
||||
//! }
|
||||
//!```
|
||||
//!
|
||||
//! The keybindings themselves are managed independently by calling cx.bind_keys().
|
||||
//! (Though mostly when developing Zed itself, you just need to add a new line to
|
||||
//! assets/keymaps/default-{platform}.json).
|
||||
//!
|
||||
//! ```ignore
|
||||
//! cx.bind_keys([
|
||||
//! KeyBinding::new("cmd-z", Editor::undo, Some("Editor")),
|
||||
//! KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")),
|
||||
//! ])
|
||||
//! ```
|
||||
//!
|
||||
//! With all of this in place, GPUI will ensure that if you have an Editor that contains
|
||||
//! the focus, hitting cmd-z will Undo.
|
||||
//!
|
||||
//! In real apps, it is a little more complicated than this, because typically you have
|
||||
//! several nested views that each register keyboard handlers. In this case action matching
|
||||
//! bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined
|
||||
//! then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace.
|
||||
//!
|
||||
//! In GPUI, keybindings are not limited to just single keystrokes, you can define
|
||||
//! sequences by separating the keys with a space:
|
||||
//!
|
||||
//! KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane"))
|
||||
|
||||
use crate::{
|
||||
Action, ActionRegistry, App, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap,
|
||||
Keystroke, ModifiersChangedEvent, Window,
|
||||
};
|
||||
use collections::FxHashMap;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
mem,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// ID of a node within `DispatchTree`. Note that these are **not** stable between frames, and so a
|
||||
/// `DispatchNodeId` should only be used with the `DispatchTree` that provided it.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct DispatchNodeId(usize);
|
||||
|
||||
pub(crate) struct DispatchTree {
|
||||
node_stack: Vec<DispatchNodeId>,
|
||||
pub(crate) context_stack: Vec<KeyContext>,
|
||||
view_stack: Vec<EntityId>,
|
||||
nodes: Vec<DispatchNode>,
|
||||
focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>,
|
||||
view_node_ids: FxHashMap<EntityId, DispatchNodeId>,
|
||||
keymap: Rc<RefCell<Keymap>>,
|
||||
action_registry: Rc<ActionRegistry>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DispatchNode {
|
||||
pub key_listeners: Vec<KeyListener>,
|
||||
pub action_listeners: Vec<DispatchActionListener>,
|
||||
pub modifiers_changed_listeners: Vec<ModifiersChangedListener>,
|
||||
pub context: Option<KeyContext>,
|
||||
pub focus_id: Option<FocusId>,
|
||||
view_id: Option<EntityId>,
|
||||
parent: Option<DispatchNodeId>,
|
||||
}
|
||||
|
||||
pub(crate) struct ReusedSubtree {
|
||||
old_range: Range<usize>,
|
||||
new_range: Range<usize>,
|
||||
contains_focus: bool,
|
||||
}
|
||||
|
||||
impl ReusedSubtree {
|
||||
pub fn refresh_node_id(&self, node_id: DispatchNodeId) -> DispatchNodeId {
|
||||
debug_assert!(
|
||||
self.old_range.contains(&node_id.0),
|
||||
"node {} was not part of the reused subtree {:?}",
|
||||
node_id.0,
|
||||
self.old_range
|
||||
);
|
||||
DispatchNodeId((node_id.0 - self.old_range.start) + self.new_range.start)
|
||||
}
|
||||
|
||||
pub fn contains_focus(&self) -> bool {
|
||||
self.contains_focus
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct Replay {
|
||||
pub(crate) keystroke: Keystroke,
|
||||
pub(crate) bindings: SmallVec<[KeyBinding; 1]>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct DispatchResult {
|
||||
pub(crate) pending: SmallVec<[Keystroke; 1]>,
|
||||
pub(crate) bindings: SmallVec<[KeyBinding; 1]>,
|
||||
pub(crate) to_replay: SmallVec<[Replay; 1]>,
|
||||
pub(crate) context_stack: Vec<KeyContext>,
|
||||
}
|
||||
|
||||
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Window, &mut App)>;
|
||||
type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut Window, &mut App)>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DispatchActionListener {
|
||||
pub(crate) action_type: TypeId,
|
||||
pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl DispatchTree {
|
||||
pub fn new(keymap: Rc<RefCell<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
|
||||
Self {
|
||||
node_stack: Vec::new(),
|
||||
context_stack: Vec::new(),
|
||||
view_stack: Vec::new(),
|
||||
nodes: Vec::new(),
|
||||
focusable_node_ids: FxHashMap::default(),
|
||||
view_node_ids: FxHashMap::default(),
|
||||
keymap,
|
||||
action_registry,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.node_stack.clear();
|
||||
self.context_stack.clear();
|
||||
self.view_stack.clear();
|
||||
self.nodes.clear();
|
||||
self.focusable_node_ids.clear();
|
||||
self.view_node_ids.clear();
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.nodes.len()
|
||||
}
|
||||
|
||||
pub fn push_node(&mut self) -> DispatchNodeId {
|
||||
let parent = self.node_stack.last().copied();
|
||||
let node_id = DispatchNodeId(self.nodes.len());
|
||||
|
||||
self.nodes.push(DispatchNode {
|
||||
parent,
|
||||
..Default::default()
|
||||
});
|
||||
self.node_stack.push(node_id);
|
||||
node_id
|
||||
}
|
||||
|
||||
pub fn set_active_node(&mut self, node_id: DispatchNodeId) {
|
||||
let next_node_parent = self.nodes[node_id.0].parent;
|
||||
while self.node_stack.last().copied() != next_node_parent && !self.node_stack.is_empty() {
|
||||
self.pop_node();
|
||||
}
|
||||
|
||||
if self.node_stack.last().copied() == next_node_parent {
|
||||
self.node_stack.push(node_id);
|
||||
let active_node = &self.nodes[node_id.0];
|
||||
if let Some(view_id) = active_node.view_id {
|
||||
self.view_stack.push(view_id)
|
||||
}
|
||||
if let Some(context) = active_node.context.clone() {
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
} else {
|
||||
debug_assert_eq!(self.node_stack.len(), 0);
|
||||
|
||||
let mut current_node_id = Some(node_id);
|
||||
while let Some(node_id) = current_node_id {
|
||||
let node = &self.nodes[node_id.0];
|
||||
if let Some(context) = node.context.clone() {
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
if node.view_id.is_some() {
|
||||
self.view_stack.push(node.view_id.unwrap());
|
||||
}
|
||||
self.node_stack.push(node_id);
|
||||
current_node_id = node.parent;
|
||||
}
|
||||
|
||||
self.context_stack.reverse();
|
||||
self.view_stack.reverse();
|
||||
self.node_stack.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_key_context(&mut self, context: KeyContext) {
|
||||
self.active_node().context = Some(context.clone());
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
|
||||
pub fn set_focus_id(&mut self, focus_id: FocusId) {
|
||||
let node_id = *self.node_stack.last().unwrap();
|
||||
self.nodes[node_id.0].focus_id = Some(focus_id);
|
||||
self.focusable_node_ids.insert(focus_id, node_id);
|
||||
}
|
||||
|
||||
pub fn set_view_id(&mut self, view_id: EntityId) {
|
||||
if self.view_stack.last().copied() != Some(view_id) {
|
||||
let node_id = *self.node_stack.last().unwrap();
|
||||
self.nodes[node_id.0].view_id = Some(view_id);
|
||||
self.view_node_ids.insert(view_id, node_id);
|
||||
self.view_stack.push(view_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_node(&mut self) {
|
||||
let node = &self.nodes[self.active_node_id().unwrap().0];
|
||||
if node.context.is_some() {
|
||||
self.context_stack.pop();
|
||||
}
|
||||
if node.view_id.is_some() {
|
||||
self.view_stack.pop();
|
||||
}
|
||||
self.node_stack.pop();
|
||||
}
|
||||
|
||||
fn move_node(&mut self, source: &mut DispatchNode) {
|
||||
self.push_node();
|
||||
if let Some(context) = source.context.clone() {
|
||||
self.set_key_context(context);
|
||||
}
|
||||
if let Some(focus_id) = source.focus_id {
|
||||
self.set_focus_id(focus_id);
|
||||
}
|
||||
if let Some(view_id) = source.view_id {
|
||||
self.set_view_id(view_id);
|
||||
}
|
||||
|
||||
let target = self.active_node();
|
||||
target.key_listeners = mem::take(&mut source.key_listeners);
|
||||
target.action_listeners = mem::take(&mut source.action_listeners);
|
||||
target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners);
|
||||
}
|
||||
|
||||
pub fn reuse_subtree(
|
||||
&mut self,
|
||||
old_range: Range<usize>,
|
||||
source: &mut Self,
|
||||
focus: Option<FocusId>,
|
||||
) -> ReusedSubtree {
|
||||
let new_range = self.nodes.len()..self.nodes.len() + old_range.len();
|
||||
|
||||
let mut contains_focus = false;
|
||||
let mut source_stack = vec![];
|
||||
for (source_node_id, source_node) in source
|
||||
.nodes
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(old_range.start)
|
||||
.take(old_range.len())
|
||||
{
|
||||
let source_node_id = DispatchNodeId(source_node_id);
|
||||
while let Some(source_ancestor) = source_stack.last() {
|
||||
if source_node.parent == Some(*source_ancestor) {
|
||||
break;
|
||||
} else {
|
||||
source_stack.pop();
|
||||
self.pop_node();
|
||||
}
|
||||
}
|
||||
|
||||
source_stack.push(source_node_id);
|
||||
if source_node.focus_id.is_some() && source_node.focus_id == focus {
|
||||
contains_focus = true;
|
||||
}
|
||||
self.move_node(source_node);
|
||||
}
|
||||
|
||||
while !source_stack.is_empty() {
|
||||
source_stack.pop();
|
||||
self.pop_node();
|
||||
}
|
||||
|
||||
ReusedSubtree {
|
||||
old_range,
|
||||
new_range,
|
||||
contains_focus,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn truncate(&mut self, index: usize) {
|
||||
for node in &self.nodes[index..] {
|
||||
if let Some(focus_id) = node.focus_id {
|
||||
self.focusable_node_ids.remove(&focus_id);
|
||||
}
|
||||
|
||||
if let Some(view_id) = node.view_id {
|
||||
self.view_node_ids.remove(&view_id);
|
||||
}
|
||||
}
|
||||
self.nodes.truncate(index);
|
||||
}
|
||||
|
||||
pub fn on_key_event(&mut self, listener: KeyListener) {
|
||||
self.active_node().key_listeners.push(listener);
|
||||
}
|
||||
|
||||
pub fn on_modifiers_changed(&mut self, listener: ModifiersChangedListener) {
|
||||
self.active_node()
|
||||
.modifiers_changed_listeners
|
||||
.push(listener);
|
||||
}
|
||||
|
||||
pub fn on_action(
|
||||
&mut self,
|
||||
action_type: TypeId,
|
||||
listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Window, &mut App)>,
|
||||
) {
|
||||
self.active_node()
|
||||
.action_listeners
|
||||
.push(DispatchActionListener {
|
||||
action_type,
|
||||
listener,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
|
||||
if parent == child {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) {
|
||||
let mut current_node_id = self.focusable_node_ids.get(&child).copied();
|
||||
while let Some(node_id) = current_node_id {
|
||||
if node_id == *parent_node_id {
|
||||
return true;
|
||||
}
|
||||
current_node_id = self.nodes[node_id.0].parent;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> {
|
||||
let mut actions = Vec::<Box<dyn Action>>::new();
|
||||
for node_id in self.dispatch_path(target) {
|
||||
let node = &self.nodes[node_id.0];
|
||||
for DispatchActionListener { action_type, .. } in &node.action_listeners {
|
||||
if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id())
|
||||
{
|
||||
// Intentionally silence these errors without logging.
|
||||
// If an action cannot be built by default, it's not available.
|
||||
let action = self.action_registry.build_action_type(action_type).ok();
|
||||
if let Some(action) = action {
|
||||
actions.insert(ix, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
actions
|
||||
}
|
||||
|
||||
pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool {
|
||||
for node_id in self.dispatch_path(target) {
|
||||
let node = &self.nodes[node_id.0];
|
||||
if node
|
||||
.action_listeners
|
||||
.iter()
|
||||
.any(|listener| listener.action_type == action.as_any().type_id())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns key bindings that invoke an action on the currently focused element. Bindings are
|
||||
/// returned in the order they were added. For display, the last binding should take precedence.
|
||||
///
|
||||
/// Bindings are only included if they are the highest precedence match for their keystrokes, so
|
||||
/// shadowed bindings are not included.
|
||||
pub fn bindings_for_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
context_stack: &[KeyContext],
|
||||
) -> Vec<KeyBinding> {
|
||||
// Ideally this would return a `DoubleEndedIterator` to avoid `highest_precedence_*`
|
||||
// methods, but this can't be done very cleanly since keymap must be borrowed.
|
||||
let keymap = self.keymap.borrow();
|
||||
keymap
|
||||
.bindings_for_action(action)
|
||||
.filter(|binding| {
|
||||
Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the highest precedence binding for the given action and context stack. This is the
|
||||
/// same as the last result of `bindings_for_action`, but more efficient than getting all bindings.
|
||||
pub fn highest_precedence_binding_for_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
context_stack: &[KeyContext],
|
||||
) -> Option<KeyBinding> {
|
||||
let keymap = self.keymap.borrow();
|
||||
keymap
|
||||
.bindings_for_action(action)
|
||||
.rev()
|
||||
.find(|binding| {
|
||||
Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack)
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn binding_matches_predicate_and_not_shadowed(
|
||||
keymap: &Keymap,
|
||||
binding: &KeyBinding,
|
||||
context_stack: &[KeyContext],
|
||||
) -> bool {
|
||||
let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack);
|
||||
if let Some(found) = bindings.iter().next() {
|
||||
found.action.partial_eq(binding.action.as_ref())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||
) -> (SmallVec<[KeyBinding; 1]>, bool, Vec<KeyContext>) {
|
||||
let context_stack: Vec<KeyContext> = dispatch_path
|
||||
.iter()
|
||||
.filter_map(|node_id| self.node(*node_id).context.clone())
|
||||
.collect();
|
||||
|
||||
let (bindings, partial) = self
|
||||
.keymap
|
||||
.borrow()
|
||||
.bindings_for_input(input, &context_stack);
|
||||
(bindings, partial, context_stack)
|
||||
}
|
||||
|
||||
/// dispatch_key processes the keystroke
|
||||
/// input should be set to the value of `pending` from the previous call to dispatch_key.
|
||||
/// This returns three instructions to the input handler:
|
||||
/// - bindings: any bindings to execute before processing this keystroke
|
||||
/// - pending: the new set of pending keystrokes to store
|
||||
/// - to_replay: any keystroke that had been pushed to pending, but are no-longer matched,
|
||||
/// these should be replayed first.
|
||||
pub fn dispatch_key(
|
||||
&mut self,
|
||||
mut input: SmallVec<[Keystroke; 1]>,
|
||||
keystroke: Keystroke,
|
||||
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||
) -> DispatchResult {
|
||||
input.push(keystroke.clone());
|
||||
let (bindings, pending, context_stack) = self.bindings_for_input(&input, dispatch_path);
|
||||
|
||||
if pending {
|
||||
return DispatchResult {
|
||||
pending: input,
|
||||
context_stack,
|
||||
..Default::default()
|
||||
};
|
||||
} else if !bindings.is_empty() {
|
||||
return DispatchResult {
|
||||
bindings,
|
||||
context_stack,
|
||||
..Default::default()
|
||||
};
|
||||
} else if input.len() == 1 {
|
||||
return DispatchResult {
|
||||
context_stack,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
input.pop();
|
||||
|
||||
let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path);
|
||||
|
||||
let mut result = self.dispatch_key(suffix, keystroke, dispatch_path);
|
||||
to_replay.extend(result.to_replay);
|
||||
result.to_replay = to_replay;
|
||||
result
|
||||
}
|
||||
|
||||
/// If the user types a matching prefix of a binding and then waits for a timeout
|
||||
/// flush_dispatch() converts any previously pending input to replay events.
|
||||
pub fn flush_dispatch(
|
||||
&mut self,
|
||||
input: SmallVec<[Keystroke; 1]>,
|
||||
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||
) -> SmallVec<[Replay; 1]> {
|
||||
let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path);
|
||||
|
||||
if !suffix.is_empty() {
|
||||
to_replay.extend(self.flush_dispatch(suffix, dispatch_path))
|
||||
}
|
||||
|
||||
to_replay
|
||||
}
|
||||
|
||||
/// Converts the longest prefix of input to a replay event and returns the rest.
|
||||
fn replay_prefix(
|
||||
&self,
|
||||
mut input: SmallVec<[Keystroke; 1]>,
|
||||
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||
) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) {
|
||||
let mut to_replay: SmallVec<[Replay; 1]> = Default::default();
|
||||
for last in (0..input.len()).rev() {
|
||||
let (bindings, _, _) = self.bindings_for_input(&input[0..=last], dispatch_path);
|
||||
if !bindings.is_empty() {
|
||||
to_replay.push(Replay {
|
||||
keystroke: input.drain(0..=last).next_back().unwrap(),
|
||||
bindings,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if to_replay.is_empty() {
|
||||
to_replay.push(Replay {
|
||||
keystroke: input.remove(0),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
(input, to_replay)
|
||||
}
|
||||
|
||||
pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {
|
||||
let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new();
|
||||
let mut current_node_id = Some(target);
|
||||
while let Some(node_id) = current_node_id {
|
||||
dispatch_path.push(node_id);
|
||||
current_node_id = self.nodes.get(node_id.0).and_then(|node| node.parent);
|
||||
}
|
||||
dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||
dispatch_path
|
||||
}
|
||||
|
||||
pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> {
|
||||
let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new();
|
||||
let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied();
|
||||
while let Some(node_id) = current_node_id {
|
||||
let node = self.node(node_id);
|
||||
if let Some(focus_id) = node.focus_id {
|
||||
focus_path.push(focus_id);
|
||||
}
|
||||
current_node_id = node.parent;
|
||||
}
|
||||
focus_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||
focus_path
|
||||
}
|
||||
|
||||
pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> {
|
||||
let mut view_path: SmallVec<[EntityId; 8]> = SmallVec::new();
|
||||
let mut current_node_id = self.view_node_ids.get(&view_id).copied();
|
||||
while let Some(node_id) = current_node_id {
|
||||
let node = self.node(node_id);
|
||||
if let Some(view_id) = node.view_id {
|
||||
view_path.push(view_id);
|
||||
}
|
||||
current_node_id = node.parent;
|
||||
}
|
||||
view_path.reverse(); // Reverse the path so it goes from the root to the view node.
|
||||
view_path
|
||||
}
|
||||
|
||||
pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode {
|
||||
&self.nodes[node_id.0]
|
||||
}
|
||||
|
||||
fn active_node(&mut self) -> &mut DispatchNode {
|
||||
let active_node_id = self.active_node_id().unwrap();
|
||||
&mut self.nodes[active_node_id.0]
|
||||
}
|
||||
|
||||
pub fn focusable_node_id(&self, target: FocusId) -> Option<DispatchNodeId> {
|
||||
self.focusable_node_ids.get(&target).copied()
|
||||
}
|
||||
|
||||
pub fn root_node_id(&self) -> DispatchNodeId {
|
||||
debug_assert!(!self.nodes.is_empty());
|
||||
DispatchNodeId(0)
|
||||
}
|
||||
|
||||
pub fn active_node_id(&self) -> Option<DispatchNodeId> {
|
||||
self.node_stack.last().copied()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
self as gpui, Element, ElementId, GlobalElementId, InspectorElementId, LayoutId, Style,
|
||||
};
|
||||
use core::panic;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Action, ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler,
|
||||
IntoElement, KeyBinding, KeyContext, Keymap, Pixels, Point, Render, TestAppContext,
|
||||
UTF16Selection, Window,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct TestAction;
|
||||
|
||||
impl Action for TestAction {
|
||||
fn name(&self) -> &'static str {
|
||||
"test::TestAction"
|
||||
}
|
||||
|
||||
fn name_for_type() -> &'static str
|
||||
where
|
||||
Self: ::std::marker::Sized,
|
||||
{
|
||||
"test::TestAction"
|
||||
}
|
||||
|
||||
fn partial_eq(&self, action: &dyn Action) -> bool {
|
||||
action.as_any().downcast_ref::<Self>() == Some(self)
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> std::boxed::Box<dyn Action> {
|
||||
Box::new(TestAction)
|
||||
}
|
||||
|
||||
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Box::new(TestAction))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keybinding_for_action_bounds() {
|
||||
let keymap = Keymap::new(vec![KeyBinding::new(
|
||||
"cmd-n",
|
||||
TestAction,
|
||||
Some("ProjectPanel"),
|
||||
)]);
|
||||
|
||||
let mut registry = ActionRegistry::default();
|
||||
|
||||
registry.load_action::<TestAction>();
|
||||
|
||||
let keymap = Rc::new(RefCell::new(keymap));
|
||||
|
||||
let tree = DispatchTree::new(keymap, Rc::new(registry));
|
||||
|
||||
let contexts = vec![
|
||||
KeyContext::parse("Workspace").unwrap(),
|
||||
KeyContext::parse("ProjectPanel").unwrap(),
|
||||
];
|
||||
|
||||
let keybinding = tree.bindings_for_action(&TestAction, &contexts);
|
||||
|
||||
assert!(keybinding[0].action.partial_eq(&TestAction))
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
fn test_input_handler_pending(cx: &mut TestAppContext) {
|
||||
#[derive(Clone)]
|
||||
struct CustomElement {
|
||||
focus_handle: FocusHandle,
|
||||
text: Rc<RefCell<String>>,
|
||||
}
|
||||
impl CustomElement {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
text: Rc::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Element for CustomElement {
|
||||
type RequestLayoutState = ();
|
||||
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some("custom".into())
|
||||
}
|
||||
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
(window.request_layout(Style::default(), [], cx), ())
|
||||
}
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
window.set_focus_handle(&self.focus_handle, cx);
|
||||
}
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let mut key_context = KeyContext::default();
|
||||
key_context.add("Terminal");
|
||||
window.set_key_context(key_context);
|
||||
window.handle_input(&self.focus_handle, self.clone(), cx);
|
||||
window.on_action(std::any::TypeId::of::<TestAction>(), |_, _, _, _| {});
|
||||
}
|
||||
}
|
||||
impl IntoElement for CustomElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InputHandler for CustomElement {
|
||||
fn selected_text_range(
|
||||
&mut self,
|
||||
_: bool,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<UTF16Selection> {
|
||||
None
|
||||
}
|
||||
|
||||
fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option<Range<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
_: Range<usize>,
|
||||
_: &mut Option<Range<usize>>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
replacement_range: Option<Range<usize>>,
|
||||
text: &str,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) {
|
||||
if replacement_range.is_some() {
|
||||
unimplemented!()
|
||||
}
|
||||
self.text.borrow_mut().push_str(text)
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
replacement_range: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_: Option<Range<usize>>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) {
|
||||
if replacement_range.is_some() {
|
||||
unimplemented!()
|
||||
}
|
||||
self.text.borrow_mut().push_str(new_text)
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _: &mut Window, _: &mut App) {}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
_: Range<usize>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn character_index_for_point(
|
||||
&mut self,
|
||||
_: Point<Pixels>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
impl Render for CustomElement {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]);
|
||||
cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]);
|
||||
});
|
||||
let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx));
|
||||
cx.update(|window, cx| {
|
||||
window.focus(&test.read(cx).focus_handle);
|
||||
window.activate_window();
|
||||
});
|
||||
cx.simulate_keystrokes("ctrl-b [");
|
||||
test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "["))
|
||||
}
|
||||
}
|
||||
Vendored
+713
@@ -0,0 +1,713 @@
|
||||
mod binding;
|
||||
mod context;
|
||||
|
||||
pub use binding::*;
|
||||
pub use context::*;
|
||||
|
||||
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
||||
use collections::{HashMap, HashSet};
|
||||
use smallvec::SmallVec;
|
||||
use std::any::TypeId;
|
||||
|
||||
/// An opaque identifier of which version of the keymap is currently active.
|
||||
/// The keymap's version is changed whenever bindings are added or removed.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub struct KeymapVersion(usize);
|
||||
|
||||
/// A collection of key bindings for the user's application.
|
||||
#[derive(Default)]
|
||||
pub struct Keymap {
|
||||
bindings: Vec<KeyBinding>,
|
||||
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
||||
no_action_binding_indices: Vec<usize>,
|
||||
version: KeymapVersion,
|
||||
}
|
||||
|
||||
/// Index of a binding within a keymap.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct BindingIndex(usize);
|
||||
|
||||
impl Keymap {
|
||||
/// Create a new keymap with the given bindings.
|
||||
pub fn new(bindings: Vec<KeyBinding>) -> Self {
|
||||
let mut this = Self::default();
|
||||
this.add_bindings(bindings);
|
||||
this
|
||||
}
|
||||
|
||||
/// Get the current version of the keymap.
|
||||
pub fn version(&self) -> KeymapVersion {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Add more bindings to the keymap.
|
||||
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
|
||||
for binding in bindings {
|
||||
let action_id = binding.action().as_any().type_id();
|
||||
if is_no_action(&*binding.action) {
|
||||
self.no_action_binding_indices.push(self.bindings.len());
|
||||
} else {
|
||||
self.binding_indices_by_action_id
|
||||
.entry(action_id)
|
||||
.or_default()
|
||||
.push(self.bindings.len());
|
||||
}
|
||||
self.bindings.push(binding);
|
||||
}
|
||||
|
||||
self.version.0 += 1;
|
||||
}
|
||||
|
||||
/// Reset this keymap to its initial state.
|
||||
pub fn clear(&mut self) {
|
||||
self.bindings.clear();
|
||||
self.binding_indices_by_action_id.clear();
|
||||
self.no_action_binding_indices.clear();
|
||||
self.version.0 += 1;
|
||||
}
|
||||
|
||||
/// Iterate over all bindings, in the order they were added.
|
||||
pub fn bindings(&self) -> impl DoubleEndedIterator<Item = &KeyBinding> + ExactSizeIterator {
|
||||
self.bindings.iter()
|
||||
}
|
||||
|
||||
/// Iterate over all bindings for the given action, in the order they were added. For display,
|
||||
/// the last binding should take precedence.
|
||||
pub fn bindings_for_action<'a>(
|
||||
&'a self,
|
||||
action: &'a dyn Action,
|
||||
) -> impl 'a + DoubleEndedIterator<Item = &'a KeyBinding> {
|
||||
let action_id = action.type_id();
|
||||
let binding_indices = self
|
||||
.binding_indices_by_action_id
|
||||
.get(&action_id)
|
||||
.map_or(&[] as _, SmallVec::as_slice)
|
||||
.iter();
|
||||
|
||||
binding_indices.filter_map(|ix| {
|
||||
let binding = &self.bindings[*ix];
|
||||
if !binding.action().partial_eq(action) {
|
||||
return None;
|
||||
}
|
||||
|
||||
for null_ix in &self.no_action_binding_indices {
|
||||
if null_ix > ix {
|
||||
let null_binding = &self.bindings[*null_ix];
|
||||
if null_binding.keystrokes == binding.keystrokes {
|
||||
let null_binding_matches =
|
||||
match (&null_binding.context_predicate, &binding.context_predicate) {
|
||||
(None, _) => true,
|
||||
(Some(_), None) => false,
|
||||
(Some(null_predicate), Some(predicate)) => {
|
||||
null_predicate.is_superset(predicate)
|
||||
}
|
||||
};
|
||||
if null_binding_matches {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(binding)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns all bindings that might match the input without checking context. The bindings
|
||||
/// returned in precedence order (reverse of the order they were added to the keymap).
|
||||
pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
|
||||
self.bindings()
|
||||
.rev()
|
||||
.filter_map(|binding| {
|
||||
binding.match_keystrokes(input).filter(|pending| !pending)?;
|
||||
Some(binding.clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a list of bindings that match the given input, and a boolean indicating whether or
|
||||
/// not more bindings might match if the input was longer. Bindings are returned in precedence
|
||||
/// order (higher precedence first, reverse of the order they were added to the keymap).
|
||||
///
|
||||
/// Precedence is defined by the depth in the tree (matches on the Editor take precedence over
|
||||
/// matches on the Pane, then the Workspace, etc.). Bindings with no context are treated as the
|
||||
/// same as the deepest context.
|
||||
///
|
||||
/// In the case of multiple bindings at the same depth, the ones added to the keymap later take
|
||||
/// precedence. User bindings are added after built-in bindings so that they take precedence.
|
||||
///
|
||||
/// If a user has disabled a binding with `"x": null` it will not be returned. Disabled bindings
|
||||
/// are evaluated with the same precedence rules so you can disable a rule in a given context
|
||||
/// only.
|
||||
pub fn bindings_for_input(
|
||||
&self,
|
||||
input: &[impl AsKeystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
||||
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
||||
let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new();
|
||||
|
||||
for (ix, binding) in self.bindings().enumerate().rev() {
|
||||
let Some(depth) = self.binding_enabled(binding, context_stack) else {
|
||||
continue;
|
||||
};
|
||||
let Some(pending) = binding.match_keystrokes(input) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !pending {
|
||||
matched_bindings.push((depth, BindingIndex(ix), binding));
|
||||
} else {
|
||||
pending_bindings.push((BindingIndex(ix), binding));
|
||||
}
|
||||
}
|
||||
|
||||
matched_bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| {
|
||||
depth_b.cmp(depth_a).then(ix_b.cmp(ix_a))
|
||||
});
|
||||
|
||||
let mut bindings: SmallVec<[_; 1]> = SmallVec::new();
|
||||
let mut first_binding_index = None;
|
||||
|
||||
for (_, ix, binding) in matched_bindings {
|
||||
if is_no_action(&*binding.action) {
|
||||
// Only break if this is a user-defined NoAction binding
|
||||
// This allows user keymaps to override base keymap NoAction bindings
|
||||
if let Some(meta) = binding.meta {
|
||||
if meta.0 == 0 {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// If no meta is set, assume it's a user binding for safety
|
||||
break;
|
||||
}
|
||||
// For non-user NoAction bindings, continue searching for user overrides
|
||||
continue;
|
||||
}
|
||||
bindings.push(binding.clone());
|
||||
first_binding_index.get_or_insert(ix);
|
||||
}
|
||||
|
||||
let mut pending = HashSet::default();
|
||||
for (ix, binding) in pending_bindings.into_iter().rev() {
|
||||
if let Some(binding_ix) = first_binding_index
|
||||
&& binding_ix > ix
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if is_no_action(&*binding.action) {
|
||||
pending.remove(&&binding.keystrokes);
|
||||
continue;
|
||||
}
|
||||
pending.insert(&binding.keystrokes);
|
||||
}
|
||||
|
||||
(bindings, !pending.is_empty())
|
||||
}
|
||||
/// Check if the given binding is enabled, given a certain key context.
|
||||
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||
if let Some(predicate) = &binding.context_predicate {
|
||||
predicate.depth_of(contexts)
|
||||
} else {
|
||||
Some(contexts.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate as gpui;
|
||||
use gpui::NoAction;
|
||||
|
||||
actions!(
|
||||
test_only,
|
||||
[ActionAlpha, ActionBeta, ActionGamma, ActionDelta,]
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn test_keymap() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-a", ActionAlpha {}, None),
|
||||
KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")),
|
||||
KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings.clone());
|
||||
|
||||
// global bindings are enabled in all contexts
|
||||
assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0));
|
||||
assert_eq!(
|
||||
keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
// contextual bindings are enabled in contexts that match their predicate
|
||||
assert_eq!(
|
||||
keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
keymap.binding_enabled(
|
||||
&bindings[2],
|
||||
&[KeyContext::parse("editor mode=full").unwrap()]
|
||||
),
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_depth_precedence() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")),
|
||||
KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let (result, pending) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
||||
&[
|
||||
KeyContext::parse("pane").unwrap(),
|
||||
KeyContext::parse("editor").unwrap(),
|
||||
],
|
||||
);
|
||||
|
||||
assert!(!pending);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result[0].action.partial_eq(&ActionGamma {}));
|
||||
assert!(result[1].action.partial_eq(&ActionBeta {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keymap_disabled() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")),
|
||||
KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")),
|
||||
KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")),
|
||||
KeyBinding::new("ctrl-b", NoAction {}, None),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
// binding is only enabled in a specific context
|
||||
assert!(
|
||||
keymap
|
||||
.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
||||
&[KeyContext::parse("barf").unwrap()],
|
||||
)
|
||||
.0
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
!keymap
|
||||
.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
||||
&[KeyContext::parse("editor").unwrap()],
|
||||
)
|
||||
.0
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
// binding is disabled in a more specific context
|
||||
assert!(
|
||||
keymap
|
||||
.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
||||
&[KeyContext::parse("editor mode=full").unwrap()],
|
||||
)
|
||||
.0
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
// binding is globally disabled
|
||||
assert!(
|
||||
keymap
|
||||
.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-b").unwrap()],
|
||||
&[KeyContext::parse("barf").unwrap()],
|
||||
)
|
||||
.0
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests for https://github.com/zed-industries/zed/issues/30259
|
||||
fn test_multiple_keystroke_binding_disabled() {
|
||||
let bindings = [
|
||||
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
|
||||
KeyBinding::new("space w w", NoAction {}, Some("editor")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let space = || Keystroke::parse("space").unwrap();
|
||||
let w = || Keystroke::parse("w").unwrap();
|
||||
|
||||
let space_w = [space(), w()];
|
||||
let space_w_w = [space(), w(), w()];
|
||||
|
||||
let workspace_context = || [KeyContext::parse("workspace").unwrap()];
|
||||
|
||||
let editor_workspace_context = || {
|
||||
[
|
||||
KeyContext::parse("workspace").unwrap(),
|
||||
KeyContext::parse("editor").unwrap(),
|
||||
]
|
||||
};
|
||||
|
||||
// Ensure `space` results in pending input on the workspace, but not editor
|
||||
let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context());
|
||||
assert!(space_workspace.0.is_empty());
|
||||
assert!(space_workspace.1);
|
||||
|
||||
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
|
||||
assert!(space_editor.0.is_empty());
|
||||
assert!(!space_editor.1);
|
||||
|
||||
// Ensure `space w` results in pending input on the workspace, but not editor
|
||||
let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context());
|
||||
assert!(space_w_workspace.0.is_empty());
|
||||
assert!(space_w_workspace.1);
|
||||
|
||||
let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context());
|
||||
assert!(space_w_editor.0.is_empty());
|
||||
assert!(!space_w_editor.1);
|
||||
|
||||
// Ensure `space w w` results in the binding in the workspace, but not in the editor
|
||||
let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context());
|
||||
assert!(!space_w_w_workspace.0.is_empty());
|
||||
assert!(!space_w_w_workspace.1);
|
||||
|
||||
let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context());
|
||||
assert!(space_w_w_editor.0.is_empty());
|
||||
assert!(!space_w_w_editor.1);
|
||||
|
||||
// Now test what happens if we have another binding defined AFTER the NoAction
|
||||
// that should result in pending
|
||||
let bindings = [
|
||||
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
|
||||
KeyBinding::new("space w w", NoAction {}, Some("editor")),
|
||||
KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
|
||||
];
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
|
||||
assert!(space_editor.0.is_empty());
|
||||
assert!(space_editor.1);
|
||||
|
||||
// Now test what happens if we have another binding defined BEFORE the NoAction
|
||||
// that should result in pending
|
||||
let bindings = [
|
||||
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
|
||||
KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
|
||||
KeyBinding::new("space w w", NoAction {}, Some("editor")),
|
||||
];
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
|
||||
assert!(space_editor.0.is_empty());
|
||||
assert!(space_editor.1);
|
||||
|
||||
// Now test what happens if we have another binding defined at a higher context
|
||||
// that should result in pending
|
||||
let bindings = [
|
||||
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
|
||||
KeyBinding::new("space w x", ActionAlpha {}, Some("workspace")),
|
||||
KeyBinding::new("space w w", NoAction {}, Some("editor")),
|
||||
];
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
|
||||
assert!(space_editor.0.is_empty());
|
||||
assert!(space_editor.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_override_multikey() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
|
||||
KeyBinding::new("ctrl-w", NoAction {}, Some("editor")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
// Ensure `space` results in pending input on the workspace, but not editor
|
||||
let (result, pending) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-w").unwrap()],
|
||||
&[KeyContext::parse("editor").unwrap()],
|
||||
);
|
||||
assert!(result.is_empty());
|
||||
assert!(pending);
|
||||
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
|
||||
KeyBinding::new("ctrl-w", ActionBeta {}, Some("editor")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
// Ensure `space` results in pending input on the workspace, but not editor
|
||||
let (result, pending) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-w").unwrap()],
|
||||
&[KeyContext::parse("editor").unwrap()],
|
||||
);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(!pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_disable() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")),
|
||||
KeyBinding::new("ctrl-x", NoAction {}, Some("editor")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
// Ensure `space` results in pending input on the workspace, but not editor
|
||||
let (result, pending) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-x").unwrap()],
|
||||
&[KeyContext::parse("editor").unwrap()],
|
||||
);
|
||||
assert!(result.is_empty());
|
||||
assert!(!pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_disable() {
|
||||
// disabled at the wrong level
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")),
|
||||
KeyBinding::new("ctrl-x", NoAction {}, Some("workspace")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
// Ensure `space` results in pending input on the workspace, but not editor
|
||||
let (result, pending) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-x").unwrap()],
|
||||
&[
|
||||
KeyContext::parse("workspace").unwrap(),
|
||||
KeyContext::parse("editor").unwrap(),
|
||||
],
|
||||
);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(!pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disable_deeper() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-x", ActionAlpha {}, Some("workspace")),
|
||||
KeyBinding::new("ctrl-x", NoAction {}, Some("editor")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
// Ensure `space` results in pending input on the workspace, but not editor
|
||||
let (result, pending) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-x").unwrap()],
|
||||
&[
|
||||
KeyContext::parse("workspace").unwrap(),
|
||||
KeyContext::parse("editor").unwrap(),
|
||||
],
|
||||
);
|
||||
assert_eq!(result.len(), 0);
|
||||
assert!(!pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pending_match_enabled() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
|
||||
KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")),
|
||||
];
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let matched = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
|
||||
&[
|
||||
KeyContext::parse("Workspace"),
|
||||
KeyContext::parse("Pane"),
|
||||
KeyContext::parse("Editor vim_mode=normal"),
|
||||
]
|
||||
.map(Result::unwrap),
|
||||
);
|
||||
assert_eq!(matched.0.len(), 1);
|
||||
assert!(matched.0[0].action.partial_eq(&ActionBeta));
|
||||
assert!(matched.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pending_match_enabled_extended() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
|
||||
KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")),
|
||||
];
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let matched = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
|
||||
&[
|
||||
KeyContext::parse("Workspace"),
|
||||
KeyContext::parse("Pane"),
|
||||
KeyContext::parse("Editor vim_mode=normal"),
|
||||
]
|
||||
.map(Result::unwrap),
|
||||
);
|
||||
assert_eq!(matched.0.len(), 1);
|
||||
assert!(matched.0[0].action.partial_eq(&ActionBeta));
|
||||
assert!(!matched.1);
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-x", ActionBeta, Some("Workspace")),
|
||||
KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")),
|
||||
];
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let matched = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
|
||||
&[
|
||||
KeyContext::parse("Workspace"),
|
||||
KeyContext::parse("Pane"),
|
||||
KeyContext::parse("Editor vim_mode=normal"),
|
||||
]
|
||||
.map(Result::unwrap),
|
||||
);
|
||||
assert_eq!(matched.0.len(), 1);
|
||||
assert!(matched.0[0].action.partial_eq(&ActionBeta));
|
||||
assert!(!matched.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overriding_prefix() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")),
|
||||
KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
|
||||
];
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
let matched = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
|
||||
&[
|
||||
KeyContext::parse("Workspace"),
|
||||
KeyContext::parse("Pane"),
|
||||
KeyContext::parse("Editor vim_mode=normal"),
|
||||
]
|
||||
.map(Result::unwrap),
|
||||
);
|
||||
assert_eq!(matched.0.len(), 1);
|
||||
assert!(matched.0[0].action.partial_eq(&ActionBeta));
|
||||
assert!(!matched.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_precedence_with_same_source() {
|
||||
// Test case: User has both Workspace and Editor bindings for the same key
|
||||
// Editor binding should take precedence over Workspace binding
|
||||
let bindings = [
|
||||
KeyBinding::new("cmd-r", ActionAlpha {}, Some("Workspace")),
|
||||
KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
// Test with context stack: [Workspace, Editor] (Editor is deeper)
|
||||
let (result, _) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("cmd-r").unwrap()],
|
||||
&[
|
||||
KeyContext::parse("Workspace").unwrap(),
|
||||
KeyContext::parse("Editor").unwrap(),
|
||||
],
|
||||
);
|
||||
|
||||
// Both bindings should be returned, but Editor binding should be first (highest precedence)
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result[0].action.partial_eq(&ActionBeta {})); // Editor binding first
|
||||
assert!(result[1].action.partial_eq(&ActionAlpha {})); // Workspace binding second
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bindings_for_action() {
|
||||
let bindings = [
|
||||
KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")),
|
||||
KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")),
|
||||
KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")),
|
||||
KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")),
|
||||
KeyBinding::new("ctrl-b", NoAction {}, Some("editor")),
|
||||
];
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings);
|
||||
|
||||
assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]);
|
||||
assert_bindings(&keymap, &ActionBeta {}, &[]);
|
||||
assert_bindings(&keymap, &ActionGamma {}, &["ctrl-c"]);
|
||||
|
||||
#[track_caller]
|
||||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||
let actual = keymap
|
||||
.bindings_for_action(action)
|
||||
.map(|binding| binding.keystrokes[0].inner().unparse())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected, "{:?}", action);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_source_precedence_sorting() {
|
||||
// KeybindSource precedence: User (0) > Vim (1) > Base (2) > Default (3)
|
||||
// Test that user keymaps take precedence over default keymaps at the same context depth
|
||||
let mut keymap = Keymap::default();
|
||||
|
||||
// Add a default keymap binding first
|
||||
let mut default_binding = KeyBinding::new("cmd-r", ActionAlpha {}, Some("Editor"));
|
||||
default_binding.set_meta(KeyBindingMetaIndex(3)); // Default source
|
||||
keymap.add_bindings([default_binding]);
|
||||
|
||||
// Add a user keymap binding
|
||||
let mut user_binding = KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor"));
|
||||
user_binding.set_meta(KeyBindingMetaIndex(0)); // User source
|
||||
keymap.add_bindings([user_binding]);
|
||||
|
||||
// Test with Editor context stack
|
||||
let (result, _) = keymap.bindings_for_input(
|
||||
&[Keystroke::parse("cmd-r").unwrap()],
|
||||
&[KeyContext::parse("Editor").unwrap()],
|
||||
);
|
||||
|
||||
// User binding should take precedence over default binding
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result[0].action.partial_eq(&ActionBeta {}));
|
||||
assert!(result[1].action.partial_eq(&ActionAlpha {}));
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
|
||||
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
pub struct KeyBinding {
|
||||
pub(crate) action: Box<dyn Action>,
|
||||
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
|
||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||
/// The json input string used when building the keybinding, if any
|
||||
pub(crate) action_input: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Clone for KeyBinding {
|
||||
fn clone(&self) -> Self {
|
||||
KeyBinding {
|
||||
action: self.action.boxed_clone(),
|
||||
keystrokes: self.keystrokes.clone(),
|
||||
context_predicate: self.context_predicate.clone(),
|
||||
meta: self.meta,
|
||||
action_input: self.action_input.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyBinding {
|
||||
/// Construct a new keybinding from the given data. Panics on parse error.
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
let context_predicate =
|
||||
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
||||
Self::load(
|
||||
keystrokes,
|
||||
Box::new(action),
|
||||
context_predicate,
|
||||
false,
|
||||
None,
|
||||
&DummyKeyboardMapper,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
pub fn load(
|
||||
keystrokes: &str,
|
||||
action: Box<dyn Action>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
use_key_equivalents: bool,
|
||||
action_input: Option<SharedString>,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
.map(|source| {
|
||||
let keystroke = Keystroke::parse(source)?;
|
||||
Ok(KeybindingKeystroke::new_with_mapper(
|
||||
keystroke,
|
||||
use_key_equivalents,
|
||||
keyboard_mapper,
|
||||
))
|
||||
})
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
|
||||
Ok(Self {
|
||||
keystrokes,
|
||||
action,
|
||||
context_predicate,
|
||||
meta: None,
|
||||
action_input,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the metadata for this binding.
|
||||
pub fn with_meta(mut self, meta: KeyBindingMetaIndex) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the metadata for this binding.
|
||||
pub fn set_meta(&mut self, meta: KeyBindingMetaIndex) {
|
||||
self.meta = Some(meta);
|
||||
}
|
||||
|
||||
/// Check if the given keystrokes match this binding.
|
||||
pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
|
||||
if self.keystrokes.len() < typed.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
||||
if !typed.as_keystroke().should_match(target) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(self.keystrokes.len() > typed.len())
|
||||
}
|
||||
|
||||
/// Get the keystrokes associated with this binding
|
||||
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||
self.keystrokes.as_slice()
|
||||
}
|
||||
|
||||
/// Get the action associated with this binding
|
||||
pub fn action(&self) -> &dyn Action {
|
||||
self.action.as_ref()
|
||||
}
|
||||
|
||||
/// Get the predicate used to match this binding
|
||||
pub fn predicate(&self) -> Option<Rc<KeyBindingContextPredicate>> {
|
||||
self.context_predicate.as_ref().map(|rc| rc.clone())
|
||||
}
|
||||
|
||||
/// Get the metadata for this binding
|
||||
pub fn meta(&self) -> Option<KeyBindingMetaIndex> {
|
||||
self.meta
|
||||
}
|
||||
|
||||
/// Get the action input associated with the action for this binding
|
||||
pub fn action_input(&self) -> Option<SharedString> {
|
||||
self.action_input.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for KeyBinding {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("KeyBinding")
|
||||
.field("keystrokes", &self.keystrokes)
|
||||
.field("context_predicate", &self.context_predicate)
|
||||
.field("action", &self.action.name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique identifier for retrieval of metadata associated with a key binding.
|
||||
/// Intended to be used as an index or key into a user-defined store of metadata
|
||||
/// associated with the binding, such as the source of the binding.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct KeyBindingMetaIndex(pub u32);
|
||||
+760
@@ -0,0 +1,760 @@
|
||||
use crate::SharedString;
|
||||
use anyhow::{Context as _, Result};
|
||||
use std::fmt;
|
||||
|
||||
/// A datastructure for resolving whether an action should be dispatched
|
||||
/// at this point in the element tree. Contains a set of identifiers
|
||||
/// and/or key value pairs representing the current context for the
|
||||
/// keymap.
|
||||
#[derive(Clone, Default, Eq, PartialEq, Hash)]
|
||||
pub struct KeyContext(Vec<ContextEntry>);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
/// An entry in a KeyContext
|
||||
pub struct ContextEntry {
|
||||
/// The key (or name if no value)
|
||||
pub key: SharedString,
|
||||
/// The value
|
||||
pub value: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for KeyContext {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &'a str) -> Result<Self> {
|
||||
Self::parse(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyContext {
|
||||
/// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`.
|
||||
pub fn new_with_defaults() -> Self {
|
||||
let mut context = Self::default();
|
||||
#[cfg(target_os = "macos")]
|
||||
context.set("os", "macos");
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
context.set("os", "linux");
|
||||
#[cfg(target_os = "windows")]
|
||||
context.set("os", "windows");
|
||||
#[cfg(not(any(
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "freebsd",
|
||||
target_os = "windows"
|
||||
)))]
|
||||
context.set("os", "unknown");
|
||||
context
|
||||
}
|
||||
|
||||
/// Returns the primary context entry (usually the name of the component)
|
||||
pub fn primary(&self) -> Option<&ContextEntry> {
|
||||
self.0.iter().find(|p| p.value.is_none())
|
||||
}
|
||||
|
||||
/// Returns everything except the primary context entry.
|
||||
pub fn secondary(&self) -> impl Iterator<Item = &ContextEntry> {
|
||||
let primary = self.primary();
|
||||
self.0.iter().filter(move |&p| Some(p) != primary)
|
||||
}
|
||||
|
||||
/// Parse a key context from a string.
|
||||
/// The key context format is very simple:
|
||||
/// - either a single identifier, such as `StatusBar`
|
||||
/// - or a key value pair, such as `mode = visible`
|
||||
/// - separated by whitespace, such as `StatusBar mode = visible`
|
||||
pub fn parse(source: &str) -> Result<Self> {
|
||||
let mut context = Self::default();
|
||||
let source = skip_whitespace(source);
|
||||
Self::parse_expr(source, &mut context)?;
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
|
||||
if source.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let key = source
|
||||
.chars()
|
||||
.take_while(|c| is_identifier_char(*c))
|
||||
.collect::<String>();
|
||||
source = skip_whitespace(&source[key.len()..]);
|
||||
if let Some(suffix) = source.strip_prefix('=') {
|
||||
source = skip_whitespace(suffix);
|
||||
let value = source
|
||||
.chars()
|
||||
.take_while(|c| is_identifier_char(*c))
|
||||
.collect::<String>();
|
||||
source = skip_whitespace(&source[value.len()..]);
|
||||
context.set(key, value);
|
||||
} else {
|
||||
context.add(key);
|
||||
}
|
||||
|
||||
Self::parse_expr(source, context)
|
||||
}
|
||||
|
||||
/// Check if this context is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Clear this context.
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
|
||||
/// Extend this context with another context.
|
||||
pub fn extend(&mut self, other: &Self) {
|
||||
for entry in &other.0 {
|
||||
if !self.contains(&entry.key) {
|
||||
self.0.push(entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an identifier to this context, if it's not already in this context.
|
||||
pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
|
||||
let key = identifier.into();
|
||||
|
||||
if !self.contains(&key) {
|
||||
self.0.push(ContextEntry { key, value: None })
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a key value pair in this context, if it's not already set.
|
||||
pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
|
||||
let key = key.into();
|
||||
if !self.contains(&key) {
|
||||
self.0.push(ContextEntry {
|
||||
key,
|
||||
value: Some(value.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this context contains a given identifier or key.
|
||||
pub fn contains(&self, key: &str) -> bool {
|
||||
self.0.iter().any(|entry| entry.key.as_ref() == key)
|
||||
}
|
||||
|
||||
/// Get the associated value for a given identifier or key.
|
||||
pub fn get(&self, key: &str) -> Option<&SharedString> {
|
||||
self.0
|
||||
.iter()
|
||||
.find(|entry| entry.key.as_ref() == key)?
|
||||
.value
|
||||
.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for KeyContext {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut entries = self.0.iter().peekable();
|
||||
while let Some(entry) = entries.next() {
|
||||
if let Some(ref value) = entry.value {
|
||||
write!(f, "{}={}", entry.key, value)?;
|
||||
} else {
|
||||
write!(f, "{}", entry.key)?;
|
||||
}
|
||||
if entries.peek().is_some() {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A datastructure for resolving whether an action should be dispatched
|
||||
/// Representing a small language for describing which contexts correspond
|
||||
/// to which actions.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum KeyBindingContextPredicate {
|
||||
/// A predicate that will match a given identifier.
|
||||
Identifier(SharedString),
|
||||
/// A predicate that will match a given key-value pair.
|
||||
Equal(SharedString, SharedString),
|
||||
/// A predicate that will match a given key-value pair not being present.
|
||||
NotEqual(SharedString, SharedString),
|
||||
/// A predicate that will match a given predicate appearing below another predicate.
|
||||
/// in the element tree
|
||||
Descendant(
|
||||
Box<KeyBindingContextPredicate>,
|
||||
Box<KeyBindingContextPredicate>,
|
||||
),
|
||||
/// Predicate that will invert another predicate.
|
||||
Not(Box<KeyBindingContextPredicate>),
|
||||
/// A predicate that will match if both of its children match.
|
||||
And(
|
||||
Box<KeyBindingContextPredicate>,
|
||||
Box<KeyBindingContextPredicate>,
|
||||
),
|
||||
/// A predicate that will match if either of its children match.
|
||||
Or(
|
||||
Box<KeyBindingContextPredicate>,
|
||||
Box<KeyBindingContextPredicate>,
|
||||
),
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyBindingContextPredicate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Identifier(name) => write!(f, "{}", name),
|
||||
Self::Equal(left, right) => write!(f, "{} == {}", left, right),
|
||||
Self::NotEqual(left, right) => write!(f, "{} != {}", left, right),
|
||||
Self::Not(pred) => write!(f, "!{}", pred),
|
||||
Self::Descendant(parent, child) => write!(f, "{} > {}", parent, child),
|
||||
Self::And(left, right) => write!(f, "({} && {})", left, right),
|
||||
Self::Or(left, right) => write!(f, "({} || {})", left, right),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyBindingContextPredicate {
|
||||
/// Parse a string in the same format as the keymap's context field.
|
||||
///
|
||||
/// A basic equivalence check against a set of identifiers can performed by
|
||||
/// simply writing a string:
|
||||
///
|
||||
/// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar`
|
||||
///
|
||||
/// You can also specify a key-value pair:
|
||||
///
|
||||
/// `mode == visible` -> A predicate that will match a context with the key `mode`
|
||||
/// with the value `visible`
|
||||
///
|
||||
/// And a logical operations combining these two checks:
|
||||
///
|
||||
/// `StatusBar && mode == visible` -> A predicate that will match a context with the
|
||||
/// identifier `StatusBar` and the key `mode`
|
||||
/// with the value `visible`
|
||||
///
|
||||
///
|
||||
/// There is also a special child `>` operator that will match a predicate that is
|
||||
/// below another predicate:
|
||||
///
|
||||
/// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar`
|
||||
/// and a child context that has the key `mode` with the
|
||||
/// value `visible`
|
||||
///
|
||||
/// This syntax supports `!=`, `||` and `&&` as logical operators.
|
||||
/// You can also preface an operation or check with a `!` to negate it.
|
||||
pub fn parse(source: &str) -> Result<Self> {
|
||||
let source = skip_whitespace(source);
|
||||
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
||||
if let Some(next) = rest.chars().next() {
|
||||
anyhow::bail!("unexpected character '{next:?}'");
|
||||
} else {
|
||||
Ok(predicate)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the deepest depth at which the predicate matches.
|
||||
pub fn depth_of(&self, contexts: &[KeyContext]) -> Option<usize> {
|
||||
for depth in (0..=contexts.len()).rev() {
|
||||
let context_slice = &contexts[0..depth];
|
||||
if self.eval_inner(context_slice, contexts) {
|
||||
return Some(depth);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Eval a predicate against a set of contexts, arranged from lowest to highest.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn eval(&self, contexts: &[KeyContext]) -> bool {
|
||||
self.eval_inner(contexts, contexts)
|
||||
}
|
||||
|
||||
/// Eval a predicate against a set of contexts, arranged from lowest to highest.
|
||||
pub fn eval_inner(&self, contexts: &[KeyContext], all_contexts: &[KeyContext]) -> bool {
|
||||
let Some(context) = contexts.last() else {
|
||||
return false;
|
||||
};
|
||||
match self {
|
||||
Self::Identifier(name) => context.contains(name),
|
||||
Self::Equal(left, right) => context
|
||||
.get(left)
|
||||
.map(|value| value == right)
|
||||
.unwrap_or(false),
|
||||
Self::NotEqual(left, right) => context
|
||||
.get(left)
|
||||
.map(|value| value != right)
|
||||
.unwrap_or(true),
|
||||
Self::Not(pred) => {
|
||||
for i in 0..all_contexts.len() {
|
||||
if pred.eval_inner(&all_contexts[..=i], all_contexts) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
// Workspace > Pane > Editor
|
||||
//
|
||||
// Pane > (Pane > Editor) // should match?
|
||||
// (Pane > Pane) > Editor // should not match?
|
||||
// Pane > !Workspace <-- should match?
|
||||
// !Workspace <-- shouldn't match?
|
||||
Self::Descendant(parent, child) => {
|
||||
for i in 0..contexts.len() - 1 {
|
||||
// [Workspace > Pane], [Editor]
|
||||
if parent.eval_inner(&contexts[..=i], all_contexts) {
|
||||
if !child.eval_inner(&contexts[i + 1..], &contexts[i + 1..]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Self::And(left, right) => {
|
||||
left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts)
|
||||
}
|
||||
Self::Or(left, right) => {
|
||||
left.eval_inner(contexts, all_contexts) || right.eval_inner(contexts, all_contexts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether or not this predicate matches all possible contexts matched by
|
||||
/// the other predicate.
|
||||
pub fn is_superset(&self, other: &Self) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let KeyBindingContextPredicate::Or(left, right) = self {
|
||||
return left.is_superset(other) || right.is_superset(other);
|
||||
}
|
||||
|
||||
match other {
|
||||
KeyBindingContextPredicate::Descendant(_, child) => self.is_superset(child),
|
||||
KeyBindingContextPredicate::And(left, right) => {
|
||||
self.is_superset(left) || self.is_superset(right)
|
||||
}
|
||||
KeyBindingContextPredicate::Identifier(_) => false,
|
||||
KeyBindingContextPredicate::Equal(_, _) => false,
|
||||
KeyBindingContextPredicate::NotEqual(_, _) => false,
|
||||
KeyBindingContextPredicate::Not(_) => false,
|
||||
KeyBindingContextPredicate::Or(_, _) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
|
||||
type Op = fn(
|
||||
KeyBindingContextPredicate,
|
||||
KeyBindingContextPredicate,
|
||||
) -> Result<KeyBindingContextPredicate>;
|
||||
|
||||
let (mut predicate, rest) = Self::parse_primary(source)?;
|
||||
source = rest;
|
||||
|
||||
'parse: loop {
|
||||
for (operator, precedence, constructor) in [
|
||||
(">", PRECEDENCE_CHILD, Self::new_child as Op),
|
||||
("&&", PRECEDENCE_AND, Self::new_and as Op),
|
||||
("||", PRECEDENCE_OR, Self::new_or as Op),
|
||||
("==", PRECEDENCE_EQ, Self::new_eq as Op),
|
||||
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
|
||||
] {
|
||||
if source.starts_with(operator) && precedence >= min_precedence {
|
||||
source = skip_whitespace(&source[operator.len()..]);
|
||||
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
|
||||
predicate = constructor(predicate, right)?;
|
||||
source = rest;
|
||||
continue 'parse;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Ok((predicate, source))
|
||||
}
|
||||
|
||||
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
|
||||
let next = source.chars().next().context("unexpected end")?;
|
||||
match next {
|
||||
'(' => {
|
||||
source = skip_whitespace(&source[1..]);
|
||||
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
||||
let stripped = rest.strip_prefix(')').context("expected a ')'")?;
|
||||
source = skip_whitespace(stripped);
|
||||
Ok((predicate, source))
|
||||
}
|
||||
'!' => {
|
||||
let source = skip_whitespace(&source[1..]);
|
||||
let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?;
|
||||
Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
|
||||
}
|
||||
_ if is_identifier_char(next) => {
|
||||
let len = source
|
||||
.find(|c: char| !is_identifier_char(c) && !is_vim_operator_char(c))
|
||||
.unwrap_or(source.len());
|
||||
let (identifier, rest) = source.split_at(len);
|
||||
source = skip_whitespace(rest);
|
||||
Ok((
|
||||
KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
|
||||
source,
|
||||
))
|
||||
}
|
||||
_ if is_vim_operator_char(next) => {
|
||||
let (operator, rest) = source.split_at(1);
|
||||
source = skip_whitespace(rest);
|
||||
Ok((
|
||||
KeyBindingContextPredicate::Identifier(operator.to_string().into()),
|
||||
source,
|
||||
))
|
||||
}
|
||||
_ => anyhow::bail!("unexpected character '{next:?}'"),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_or(self, other: Self) -> Result<Self> {
|
||||
Ok(Self::Or(Box::new(self), Box::new(other)))
|
||||
}
|
||||
|
||||
fn new_and(self, other: Self) -> Result<Self> {
|
||||
Ok(Self::And(Box::new(self), Box::new(other)))
|
||||
}
|
||||
|
||||
fn new_child(self, other: Self) -> Result<Self> {
|
||||
Ok(Self::Descendant(Box::new(self), Box::new(other)))
|
||||
}
|
||||
|
||||
fn new_eq(self, other: Self) -> Result<Self> {
|
||||
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
||||
Ok(Self::Equal(left, right))
|
||||
} else {
|
||||
anyhow::bail!("operands of == must be identifiers");
|
||||
}
|
||||
}
|
||||
|
||||
fn new_neq(self, other: Self) -> Result<Self> {
|
||||
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
||||
Ok(Self::NotEqual(left, right))
|
||||
} else {
|
||||
anyhow::bail!("operands of != must be identifiers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PRECEDENCE_CHILD: u32 = 1;
|
||||
const PRECEDENCE_OR: u32 = 2;
|
||||
const PRECEDENCE_AND: u32 = 3;
|
||||
const PRECEDENCE_EQ: u32 = 4;
|
||||
const PRECEDENCE_NOT: u32 = 5;
|
||||
|
||||
fn is_identifier_char(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_' || c == '-'
|
||||
}
|
||||
|
||||
fn is_vim_operator_char(c: char) -> bool {
|
||||
c == '>' || c == '<' || c == '~' || c == '"' || c == '?'
|
||||
}
|
||||
|
||||
fn skip_whitespace(source: &str) -> &str {
|
||||
let len = source
|
||||
.find(|c: char| !c.is_whitespace())
|
||||
.unwrap_or(source.len());
|
||||
&source[len..]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::slice;
|
||||
|
||||
use super::*;
|
||||
use crate as gpui;
|
||||
use KeyBindingContextPredicate::*;
|
||||
|
||||
#[test]
|
||||
fn test_actions_definition() {
|
||||
{
|
||||
actions!(test_only, [A, B, C, D, E, F, G]);
|
||||
}
|
||||
|
||||
{
|
||||
actions!(
|
||||
test_only,
|
||||
[
|
||||
H, I, J, K, L, M, N, // Don't wrap, test the trailing comma
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_context() {
|
||||
let mut expected = KeyContext::default();
|
||||
expected.add("baz");
|
||||
expected.set("foo", "bar");
|
||||
assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected);
|
||||
assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected);
|
||||
assert_eq!(
|
||||
KeyContext::parse(" baz foo = bar baz").unwrap(),
|
||||
expected
|
||||
);
|
||||
assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_identifiers() {
|
||||
// Identifiers
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("abc12").unwrap(),
|
||||
Identifier("abc12".into())
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("_1a").unwrap(),
|
||||
Identifier("_1a".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_negations() {
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("!abc").unwrap(),
|
||||
Not(Box::new(Identifier("abc".into())))
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
|
||||
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_equality_operators() {
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("a == b").unwrap(),
|
||||
Equal("a".into(), "b".into())
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("c!=d").unwrap(),
|
||||
NotEqual("c".into(), "d".into())
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("c == !d")
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"operands of == must be identifiers"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_boolean_operators() {
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("a || b").unwrap(),
|
||||
Or(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(Identifier("b".into()))
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
|
||||
Or(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(And(
|
||||
Box::new(Not(Box::new(Identifier("b".into())))),
|
||||
Box::new(Identifier("c".into()))
|
||||
))
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
|
||||
Or(
|
||||
Box::new(And(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(Identifier("b".into()))
|
||||
)),
|
||||
Box::new(And(
|
||||
Box::new(Identifier("c".into())),
|
||||
Box::new(Identifier("d".into()))
|
||||
))
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
|
||||
Or(
|
||||
Box::new(And(
|
||||
Box::new(Equal("a".into(), "b".into())),
|
||||
Box::new(Identifier("c".into()))
|
||||
)),
|
||||
Box::new(And(
|
||||
Box::new(Equal("d".into(), "e".into())),
|
||||
Box::new(Identifier("f".into()))
|
||||
))
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
|
||||
And(
|
||||
Box::new(And(
|
||||
Box::new(And(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(Identifier("b".into()))
|
||||
)),
|
||||
Box::new(Identifier("c".into())),
|
||||
)),
|
||||
Box::new(Identifier("d".into()))
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_parenthesized_expressions() {
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
|
||||
And(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(Or(
|
||||
Box::new(Equal("b".into(), "c".into())),
|
||||
Box::new(NotEqual("d".into(), "e".into())),
|
||||
)),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
|
||||
Or(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(Identifier("b".into())),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_superset() {
|
||||
assert_is_superset("editor", "editor", true);
|
||||
assert_is_superset("editor", "workspace", false);
|
||||
|
||||
assert_is_superset("editor", "editor && vim_mode", true);
|
||||
assert_is_superset("editor", "mode == full && editor", true);
|
||||
assert_is_superset("editor && mode == full", "editor", false);
|
||||
|
||||
assert_is_superset("editor", "something > editor", true);
|
||||
assert_is_superset("editor", "editor > menu", false);
|
||||
|
||||
assert_is_superset("foo || bar || baz", "bar", true);
|
||||
assert_is_superset("foo || bar || baz", "quux", false);
|
||||
|
||||
#[track_caller]
|
||||
fn assert_is_superset(a: &str, b: &str, result: bool) {
|
||||
let a = KeyBindingContextPredicate::parse(a).unwrap();
|
||||
let b = KeyBindingContextPredicate::parse(b).unwrap();
|
||||
assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_child_operator() {
|
||||
let predicate = KeyBindingContextPredicate::parse("parent > child").unwrap();
|
||||
|
||||
let parent_context = KeyContext::try_from("parent").unwrap();
|
||||
let child_context = KeyContext::try_from("child").unwrap();
|
||||
|
||||
let contexts = vec![parent_context.clone(), child_context.clone()];
|
||||
assert!(predicate.eval(&contexts));
|
||||
|
||||
let grandparent_context = KeyContext::try_from("grandparent").unwrap();
|
||||
|
||||
let contexts = vec![
|
||||
grandparent_context,
|
||||
parent_context.clone(),
|
||||
child_context.clone(),
|
||||
];
|
||||
assert!(predicate.eval(&contexts));
|
||||
|
||||
let other_context = KeyContext::try_from("other").unwrap();
|
||||
|
||||
let contexts = vec![other_context.clone(), child_context.clone()];
|
||||
assert!(!predicate.eval(&contexts));
|
||||
|
||||
let contexts = vec![parent_context.clone(), other_context, child_context.clone()];
|
||||
assert!(predicate.eval(&contexts));
|
||||
|
||||
assert!(!predicate.eval(&[]));
|
||||
assert!(!predicate.eval(slice::from_ref(&child_context)));
|
||||
assert!(!predicate.eval(&[parent_context]));
|
||||
|
||||
let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap();
|
||||
assert!(!zany_predicate.eval(slice::from_ref(&child_context)));
|
||||
assert!(zany_predicate.eval(&[child_context.clone(), child_context]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_operator() {
|
||||
let not_predicate = KeyBindingContextPredicate::parse("!editor").unwrap();
|
||||
let editor_context = KeyContext::try_from("editor").unwrap();
|
||||
let workspace_context = KeyContext::try_from("workspace").unwrap();
|
||||
let parent_context = KeyContext::try_from("parent").unwrap();
|
||||
let child_context = KeyContext::try_from("child").unwrap();
|
||||
|
||||
assert!(not_predicate.eval(slice::from_ref(&workspace_context)));
|
||||
assert!(!not_predicate.eval(slice::from_ref(&editor_context)));
|
||||
assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()]));
|
||||
assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()]));
|
||||
|
||||
let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap();
|
||||
assert!(complex_not.eval(slice::from_ref(&workspace_context)));
|
||||
assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()]));
|
||||
|
||||
let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap();
|
||||
let mut mode_context = KeyContext::default();
|
||||
mode_context.set("mode", "full");
|
||||
assert!(!not_mode_predicate.eval(&[mode_context.clone()]));
|
||||
|
||||
let mut other_mode_context = KeyContext::default();
|
||||
other_mode_context.set("mode", "partial");
|
||||
assert!(not_mode_predicate.eval(&[other_mode_context]));
|
||||
|
||||
let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap();
|
||||
assert!(not_descendant.eval(slice::from_ref(&parent_context)));
|
||||
assert!(not_descendant.eval(slice::from_ref(&child_context)));
|
||||
assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
|
||||
|
||||
let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap();
|
||||
assert!(!not_descendant.eval(slice::from_ref(&parent_context)));
|
||||
assert!(!not_descendant.eval(slice::from_ref(&child_context)));
|
||||
assert!(!not_descendant.eval(&[parent_context, child_context]));
|
||||
|
||||
let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap();
|
||||
assert!(double_not.eval(slice::from_ref(&editor_context)));
|
||||
assert!(!double_not.eval(slice::from_ref(&workspace_context)));
|
||||
|
||||
// Test complex descendant cases
|
||||
let workspace_context = KeyContext::try_from("Workspace").unwrap();
|
||||
let pane_context = KeyContext::try_from("Pane").unwrap();
|
||||
let editor_context = KeyContext::try_from("Editor").unwrap();
|
||||
|
||||
// Workspace > Pane > Editor
|
||||
let workspace_pane_editor = vec![
|
||||
workspace_context.clone(),
|
||||
pane_context.clone(),
|
||||
editor_context.clone(),
|
||||
];
|
||||
|
||||
// Pane > (Pane > Editor) - should not match
|
||||
let pane_pane_editor = KeyBindingContextPredicate::parse("Pane > (Pane > Editor)").unwrap();
|
||||
assert!(!pane_pane_editor.eval(&workspace_pane_editor));
|
||||
|
||||
let workspace_pane_editor_predicate =
|
||||
KeyBindingContextPredicate::parse("Workspace > Pane > Editor").unwrap();
|
||||
assert!(workspace_pane_editor_predicate.eval(&workspace_pane_editor));
|
||||
|
||||
// (Pane > Pane) > Editor - should not match
|
||||
let pane_pane_then_editor =
|
||||
KeyBindingContextPredicate::parse("(Pane > Pane) > Editor").unwrap();
|
||||
assert!(!pane_pane_then_editor.eval(&workspace_pane_editor));
|
||||
|
||||
// Pane > !Workspace - should match
|
||||
let pane_not_workspace = KeyBindingContextPredicate::parse("Pane > !Workspace").unwrap();
|
||||
assert!(pane_not_workspace.eval(&[pane_context.clone(), editor_context.clone()]));
|
||||
assert!(!pane_not_workspace.eval(&[pane_context.clone(), workspace_context.clone()]));
|
||||
|
||||
// !Workspace - shouldn't match when Workspace is in the context
|
||||
let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap();
|
||||
assert!(!not_workspace.eval(slice::from_ref(&workspace_context)));
|
||||
assert!(not_workspace.eval(slice::from_ref(&pane_context)));
|
||||
assert!(not_workspace.eval(slice::from_ref(&editor_context)));
|
||||
assert!(!not_workspace.eval(&workspace_pane_editor));
|
||||
}
|
||||
}
|
||||
Vendored
+347
@@ -0,0 +1,347 @@
|
||||
use anyhow::Error;
|
||||
use etagere::euclid::{Point2D, Vector2D};
|
||||
use lyon::geom::Angle;
|
||||
use lyon::math::{Vector, vector};
|
||||
use lyon::path::traits::SvgPathBuilder;
|
||||
use lyon::path::{ArcFlags, Polygon};
|
||||
use lyon::tessellation::{
|
||||
BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers,
|
||||
};
|
||||
|
||||
pub use lyon::math::Transform;
|
||||
pub use lyon::tessellation::{FillOptions, FillRule, StrokeOptions};
|
||||
|
||||
use crate::{Path, Pixels, Point, point, px};
|
||||
|
||||
/// Style of the PathBuilder
|
||||
pub enum PathStyle {
|
||||
/// Stroke style
|
||||
Stroke(StrokeOptions),
|
||||
/// Fill style
|
||||
Fill(FillOptions),
|
||||
}
|
||||
|
||||
/// A [`Path`] builder.
|
||||
pub struct PathBuilder {
|
||||
raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>,
|
||||
transform: Option<lyon::math::Transform>,
|
||||
/// PathStyle of the PathBuilder
|
||||
pub style: PathStyle,
|
||||
dash_array: Option<Vec<Pixels>>,
|
||||
}
|
||||
|
||||
impl From<lyon::path::Builder> for PathBuilder {
|
||||
fn from(builder: lyon::path::Builder) -> Self {
|
||||
Self {
|
||||
raw: builder.with_svg(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lyon::path::builder::WithSvg<lyon::path::BuilderImpl>> for PathBuilder {
|
||||
fn from(raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>) -> Self {
|
||||
Self {
|
||||
raw,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lyon::math::Point> for Point<Pixels> {
|
||||
fn from(p: lyon::math::Point) -> Self {
|
||||
point(px(p.x), px(p.y))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point<Pixels>> for lyon::math::Point {
|
||||
fn from(p: Point<Pixels>) -> Self {
|
||||
lyon::math::point(p.x.0, p.y.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point<Pixels>> for Vector {
|
||||
fn from(p: Point<Pixels>) -> Self {
|
||||
vector(p.x.0, p.y.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point<Pixels>> for Point2D<f32, Pixels> {
|
||||
fn from(p: Point<Pixels>) -> Self {
|
||||
Point2D::new(p.x.0, p.y.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PathBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
raw: lyon::path::Path::builder().with_svg(),
|
||||
style: PathStyle::Fill(FillOptions::default()),
|
||||
transform: None,
|
||||
dash_array: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PathBuilder {
|
||||
/// Creates a new [`PathBuilder`] to build a Stroke path.
|
||||
pub fn stroke(width: Pixels) -> Self {
|
||||
Self {
|
||||
style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`PathBuilder`] to build a Fill path.
|
||||
pub fn fill() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the style of the [`PathBuilder`].
|
||||
pub fn with_style(self, style: PathStyle) -> Self {
|
||||
Self { style, ..self }
|
||||
}
|
||||
|
||||
/// Sets the dash array of the [`PathBuilder`].
|
||||
///
|
||||
/// [MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray)
|
||||
pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self {
|
||||
// If an odd number of values is provided, then the list of values is repeated to yield an even number of values.
|
||||
// Thus, 5,3,2 is equivalent to 5,3,2,5,3,2.
|
||||
let array = if dash_array.len() % 2 == 1 {
|
||||
let mut new_dash_array = dash_array.to_vec();
|
||||
new_dash_array.extend_from_slice(dash_array);
|
||||
new_dash_array
|
||||
} else {
|
||||
dash_array.to_vec()
|
||||
};
|
||||
|
||||
self.dash_array = Some(array);
|
||||
self
|
||||
}
|
||||
|
||||
/// Move the current point to the given point.
|
||||
#[inline]
|
||||
pub fn move_to(&mut self, to: Point<Pixels>) {
|
||||
self.raw.move_to(to.into());
|
||||
}
|
||||
|
||||
/// Draw a straight line from the current point to the given point.
|
||||
#[inline]
|
||||
pub fn line_to(&mut self, to: Point<Pixels>) {
|
||||
self.raw.line_to(to.into());
|
||||
}
|
||||
|
||||
/// Draw a curve from the current point to the given point, using the given control point.
|
||||
#[inline]
|
||||
pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
|
||||
self.raw.quadratic_bezier_to(ctrl.into(), to.into());
|
||||
}
|
||||
|
||||
/// Adds a cubic Bézier to the [`Path`] given its two control points
|
||||
/// and its end point.
|
||||
#[inline]
|
||||
pub fn cubic_bezier_to(
|
||||
&mut self,
|
||||
to: Point<Pixels>,
|
||||
control_a: Point<Pixels>,
|
||||
control_b: Point<Pixels>,
|
||||
) {
|
||||
self.raw
|
||||
.cubic_bezier_to(control_a.into(), control_b.into(), to.into());
|
||||
}
|
||||
|
||||
/// Adds an elliptical arc.
|
||||
pub fn arc_to(
|
||||
&mut self,
|
||||
radii: Point<Pixels>,
|
||||
x_rotation: Pixels,
|
||||
large_arc: bool,
|
||||
sweep: bool,
|
||||
to: Point<Pixels>,
|
||||
) {
|
||||
self.raw.arc_to(
|
||||
radii.into(),
|
||||
Angle::degrees(x_rotation.into()),
|
||||
ArcFlags { large_arc, sweep },
|
||||
to.into(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Equivalent to `arc_to` in relative coordinates.
|
||||
pub fn relative_arc_to(
|
||||
&mut self,
|
||||
radii: Point<Pixels>,
|
||||
x_rotation: Pixels,
|
||||
large_arc: bool,
|
||||
sweep: bool,
|
||||
to: Point<Pixels>,
|
||||
) {
|
||||
self.raw.relative_arc_to(
|
||||
radii.into(),
|
||||
Angle::degrees(x_rotation.into()),
|
||||
ArcFlags { large_arc, sweep },
|
||||
to.into(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a polygon.
|
||||
pub fn add_polygon(&mut self, points: &[Point<Pixels>], closed: bool) {
|
||||
let points = points.iter().copied().map(|p| p.into()).collect::<Vec<_>>();
|
||||
self.raw.add_polygon(Polygon {
|
||||
points: points.as_ref(),
|
||||
closed,
|
||||
});
|
||||
}
|
||||
|
||||
/// Close the current sub-path.
|
||||
#[inline]
|
||||
pub fn close(&mut self) {
|
||||
self.raw.close();
|
||||
}
|
||||
|
||||
/// Applies a transform to the path.
|
||||
#[inline]
|
||||
pub fn transform(&mut self, transform: Transform) {
|
||||
self.transform = Some(transform);
|
||||
}
|
||||
|
||||
/// Applies a translation to the path.
|
||||
#[inline]
|
||||
pub fn translate(&mut self, to: Point<Pixels>) {
|
||||
if let Some(transform) = self.transform {
|
||||
self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0)));
|
||||
} else {
|
||||
self.transform = Some(Transform::translation(to.x.0, to.y.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a scale to the path.
|
||||
#[inline]
|
||||
pub fn scale(&mut self, scale: f32) {
|
||||
if let Some(transform) = self.transform {
|
||||
self.transform = Some(transform.then_scale(scale, scale));
|
||||
} else {
|
||||
self.transform = Some(Transform::scale(scale, scale));
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a rotation to the path.
|
||||
///
|
||||
/// The `angle` is in degrees value in the range 0.0 to 360.0.
|
||||
#[inline]
|
||||
pub fn rotate(&mut self, angle: f32) {
|
||||
let radians = angle.to_radians();
|
||||
if let Some(transform) = self.transform {
|
||||
self.transform = Some(transform.then_rotate(Angle::radians(radians)));
|
||||
} else {
|
||||
self.transform = Some(Transform::rotation(Angle::radians(radians)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds into a [`Path`].
|
||||
#[inline]
|
||||
pub fn build(self) -> Result<Path<Pixels>, Error> {
|
||||
let path = if let Some(transform) = self.transform {
|
||||
self.raw.build().transformed(&transform)
|
||||
} else {
|
||||
self.raw.build()
|
||||
};
|
||||
|
||||
match self.style {
|
||||
PathStyle::Stroke(options) => Self::tessellate_stroke(self.dash_array, &path, &options),
|
||||
PathStyle::Fill(options) => Self::tessellate_fill(&path, &options),
|
||||
}
|
||||
}
|
||||
|
||||
fn tessellate_fill(
|
||||
path: &lyon::path::Path,
|
||||
options: &FillOptions,
|
||||
) -> Result<Path<Pixels>, Error> {
|
||||
// Will contain the result of the tessellation.
|
||||
let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
|
||||
let mut tessellator = FillTessellator::new();
|
||||
|
||||
// Compute the tessellation.
|
||||
tessellator.tessellate_path(
|
||||
path,
|
||||
options,
|
||||
&mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()),
|
||||
)?;
|
||||
|
||||
Ok(Self::build_path(buf))
|
||||
}
|
||||
|
||||
fn tessellate_stroke(
|
||||
dash_array: Option<Vec<Pixels>>,
|
||||
path: &lyon::path::Path,
|
||||
options: &StrokeOptions,
|
||||
) -> Result<Path<Pixels>, Error> {
|
||||
let path = if let Some(dash_array) = dash_array {
|
||||
let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01);
|
||||
let mut sampler = measurements
|
||||
.create_sampler(path, lyon::algorithms::measure::SampleType::Normalized);
|
||||
let mut builder = lyon::path::Path::builder();
|
||||
|
||||
let total_length = sampler.length();
|
||||
let dash_array_len = dash_array.len();
|
||||
let mut pos = 0.;
|
||||
let mut dash_index = 0;
|
||||
while pos < total_length {
|
||||
let dash_length = dash_array[dash_index % dash_array_len].0;
|
||||
let next_pos = (pos + dash_length).min(total_length);
|
||||
if dash_index % 2 == 0 {
|
||||
let start = pos / total_length;
|
||||
let end = next_pos / total_length;
|
||||
sampler.split_range(start..end, &mut builder);
|
||||
}
|
||||
pos = next_pos;
|
||||
dash_index += 1;
|
||||
}
|
||||
|
||||
&builder.build()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
// Will contain the result of the tessellation.
|
||||
let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
|
||||
let mut tessellator = StrokeTessellator::new();
|
||||
|
||||
// Compute the tessellation.
|
||||
tessellator.tessellate_path(
|
||||
path,
|
||||
options,
|
||||
&mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()),
|
||||
)?;
|
||||
|
||||
Ok(Self::build_path(buf))
|
||||
}
|
||||
|
||||
/// Builds a [`Path`] from a [`lyon::tessellation::VertexBuffers`].
|
||||
pub fn build_path(buf: VertexBuffers<lyon::math::Point, u16>) -> Path<Pixels> {
|
||||
if buf.vertices.is_empty() {
|
||||
return Path::new(Point::default());
|
||||
}
|
||||
|
||||
let first_point = buf.vertices[0];
|
||||
|
||||
let mut path = Path::new(first_point.into());
|
||||
for i in 0..buf.indices.len() / 3 {
|
||||
let i0 = buf.indices[i * 3] as usize;
|
||||
let i1 = buf.indices[i * 3 + 1] as usize;
|
||||
let i2 = buf.indices[i * 3 + 2] as usize;
|
||||
|
||||
let v0 = buf.vertices[i0];
|
||||
let v1 = buf.vertices[i1];
|
||||
let v2 = buf.vertices[i2];
|
||||
|
||||
path.push_triangle(
|
||||
(v0.into(), v1.into(), v2.into()),
|
||||
(point(0., 1.), point(0., 1.), point(0., 1.)),
|
||||
);
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
}
|
||||
Vendored
+1862
File diff suppressed because it is too large
Load Diff
+252
@@ -0,0 +1,252 @@
|
||||
use crate::{Action, App, Platform, SharedString};
|
||||
use util::ResultExt;
|
||||
|
||||
/// A menu of the application, either a main menu or a submenu
|
||||
pub struct Menu {
|
||||
/// The name of the menu
|
||||
pub name: SharedString,
|
||||
|
||||
/// The items in the menu
|
||||
pub items: Vec<MenuItem>,
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
/// Create an OwnedMenu from this Menu
|
||||
pub fn owned(self) -> OwnedMenu {
|
||||
OwnedMenu {
|
||||
name: self.name.to_string().into(),
|
||||
items: self.items.into_iter().map(|item| item.owned()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OS menus are menus that are recognized by the operating system
|
||||
/// This allows the operating system to provide specialized items for
|
||||
/// these menus
|
||||
pub struct OsMenu {
|
||||
/// The name of the menu
|
||||
pub name: SharedString,
|
||||
|
||||
/// The type of menu
|
||||
pub menu_type: SystemMenuType,
|
||||
}
|
||||
|
||||
impl OsMenu {
|
||||
/// Create an OwnedOsMenu from this OsMenu
|
||||
pub fn owned(self) -> OwnedOsMenu {
|
||||
OwnedOsMenu {
|
||||
name: self.name.to_string().into(),
|
||||
menu_type: self.menu_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of system menu
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum SystemMenuType {
|
||||
/// The 'Services' menu in the Application menu on macOS
|
||||
Services,
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
pub enum MenuItem {
|
||||
/// A separator between items
|
||||
Separator,
|
||||
|
||||
/// A submenu
|
||||
Submenu(Menu),
|
||||
|
||||
/// A menu, managed by the system (for example, the Services menu on macOS)
|
||||
SystemMenu(OsMenu),
|
||||
|
||||
/// An action that can be performed
|
||||
Action {
|
||||
/// The name of this menu item
|
||||
name: SharedString,
|
||||
|
||||
/// the action to perform when this menu item is selected
|
||||
action: Box<dyn Action>,
|
||||
|
||||
/// The OS Action that corresponds to this action, if any
|
||||
/// See [`OsAction`] for more information
|
||||
os_action: Option<OsAction>,
|
||||
},
|
||||
}
|
||||
|
||||
impl MenuItem {
|
||||
/// Creates a new menu item that is a separator
|
||||
pub fn separator() -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
/// Creates a new menu item that is a submenu
|
||||
pub fn submenu(menu: Menu) -> Self {
|
||||
Self::Submenu(menu)
|
||||
}
|
||||
|
||||
/// Creates a new submenu that is populated by the OS
|
||||
pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
|
||||
Self::SystemMenu(OsMenu {
|
||||
name: name.into(),
|
||||
menu_type,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new menu item that invokes an action
|
||||
pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
|
||||
Self::Action {
|
||||
name: name.into(),
|
||||
action: Box::new(action),
|
||||
os_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new menu item that invokes an action and has an OS action
|
||||
pub fn os_action(
|
||||
name: impl Into<SharedString>,
|
||||
action: impl Action,
|
||||
os_action: OsAction,
|
||||
) -> Self {
|
||||
Self::Action {
|
||||
name: name.into(),
|
||||
action: Box::new(action),
|
||||
os_action: Some(os_action),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an OwnedMenuItem from this MenuItem
|
||||
pub fn owned(self) -> OwnedMenuItem {
|
||||
match self {
|
||||
MenuItem::Separator => OwnedMenuItem::Separator,
|
||||
MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
|
||||
MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.into(),
|
||||
action,
|
||||
os_action,
|
||||
},
|
||||
MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OS menus are menus that are recognized by the operating system
|
||||
/// This allows the operating system to provide specialized items for
|
||||
/// these menus
|
||||
#[derive(Clone)]
|
||||
pub struct OwnedOsMenu {
|
||||
/// The name of the menu
|
||||
pub name: SharedString,
|
||||
|
||||
/// The type of menu
|
||||
pub menu_type: SystemMenuType,
|
||||
}
|
||||
|
||||
/// A menu of the application, either a main menu or a submenu
|
||||
#[derive(Clone)]
|
||||
pub struct OwnedMenu {
|
||||
/// The name of the menu
|
||||
pub name: SharedString,
|
||||
|
||||
/// The items in the menu
|
||||
pub items: Vec<OwnedMenuItem>,
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
pub enum OwnedMenuItem {
|
||||
/// A separator between items
|
||||
Separator,
|
||||
|
||||
/// A submenu
|
||||
Submenu(OwnedMenu),
|
||||
|
||||
/// A menu, managed by the system (for example, the Services menu on macOS)
|
||||
SystemMenu(OwnedOsMenu),
|
||||
|
||||
/// An action that can be performed
|
||||
Action {
|
||||
/// The name of this menu item
|
||||
name: String,
|
||||
|
||||
/// the action to perform when this menu item is selected
|
||||
action: Box<dyn Action>,
|
||||
|
||||
/// The OS Action that corresponds to this action, if any
|
||||
/// See [`OsAction`] for more information
|
||||
os_action: Option<OsAction>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clone for OwnedMenuItem {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
OwnedMenuItem::Separator => OwnedMenuItem::Separator,
|
||||
OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
|
||||
OwnedMenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.clone(),
|
||||
action: action.boxed_clone(),
|
||||
os_action: *os_action,
|
||||
},
|
||||
OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: As part of the global selections refactor, these should
|
||||
// be moved to GPUI-provided actions that make this association
|
||||
// without leaking the platform details to GPUI users
|
||||
|
||||
/// OS actions are actions that are recognized by the operating system
|
||||
/// This allows the operating system to provide specialized behavior for
|
||||
/// these actions
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum OsAction {
|
||||
/// The 'cut' action
|
||||
Cut,
|
||||
|
||||
/// The 'copy' action
|
||||
Copy,
|
||||
|
||||
/// The 'paste' action
|
||||
Paste,
|
||||
|
||||
/// The 'select all' action
|
||||
SelectAll,
|
||||
|
||||
/// The 'undo' action
|
||||
Undo,
|
||||
|
||||
/// The 'redo' action
|
||||
Redo,
|
||||
}
|
||||
|
||||
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
|
||||
platform.on_will_open_app_menu(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move || {
|
||||
cx.update(|cx| cx.clear_pending_keystrokes()).ok();
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_validate_app_menu_command(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.update(|cx| cx.is_action_available(action))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_app_menu_action(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.update(|cx| cx.dispatch_action(action)).log_err();
|
||||
}
|
||||
}));
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
mod apple_compat;
|
||||
mod blade_atlas;
|
||||
mod blade_context;
|
||||
mod blade_renderer;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use apple_compat::*;
|
||||
pub(crate) use blade_atlas::*;
|
||||
pub(crate) use blade_context::*;
|
||||
pub(crate) use blade_renderer::*;
|
||||
@@ -0,0 +1,60 @@
|
||||
use super::{BladeContext, BladeRenderer, BladeSurfaceConfig};
|
||||
use blade_graphics as gpu;
|
||||
use std::{ffi::c_void, ptr::NonNull};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
inner: BladeContext,
|
||||
}
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: BladeContext::new().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Renderer = BladeRenderer;
|
||||
|
||||
pub unsafe fn new_renderer(
|
||||
context: Context,
|
||||
_native_window: *mut c_void,
|
||||
native_view: *mut c_void,
|
||||
bounds: crate::Size<f32>,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
use raw_window_handle as rwh;
|
||||
struct RawWindow {
|
||||
view: *mut c_void,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||
let view = NonNull::new(self.view).unwrap();
|
||||
let handle = rwh::AppKitWindowHandle::new(view);
|
||||
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
impl rwh::HasDisplayHandle for RawWindow {
|
||||
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
|
||||
let handle = rwh::AppKitDisplayHandle::new();
|
||||
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
|
||||
BladeRenderer::new(
|
||||
&context.inner,
|
||||
&RawWindow {
|
||||
view: native_view as *mut _,
|
||||
},
|
||||
BladeSurfaceConfig {
|
||||
size: gpu::Extent {
|
||||
width: bounds.width as u32,
|
||||
height: bounds.height as u32,
|
||||
depth: 1,
|
||||
},
|
||||
transparent,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
use crate::{
|
||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
|
||||
Point, Size, platform::AtlasTextureList,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use blade_graphics as gpu;
|
||||
use blade_util::{BufferBelt, BufferBeltDescriptor};
|
||||
use collections::FxHashMap;
|
||||
use etagere::BucketedAtlasAllocator;
|
||||
use parking_lot::Mutex;
|
||||
use std::{borrow::Cow, ops, sync::Arc};
|
||||
|
||||
pub(crate) struct BladeAtlas(Mutex<BladeAtlasState>);
|
||||
|
||||
struct PendingUpload {
|
||||
id: AtlasTextureId,
|
||||
bounds: Bounds<DevicePixels>,
|
||||
data: gpu::BufferPiece,
|
||||
}
|
||||
|
||||
struct BladeAtlasState {
|
||||
gpu: Arc<gpu::Context>,
|
||||
upload_belt: BufferBelt,
|
||||
storage: BladeAtlasStorage,
|
||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
||||
initializations: Vec<AtlasTextureId>,
|
||||
uploads: Vec<PendingUpload>,
|
||||
}
|
||||
|
||||
#[cfg(gles)]
|
||||
unsafe impl Send for BladeAtlasState {}
|
||||
|
||||
impl BladeAtlasState {
|
||||
fn destroy(&mut self) {
|
||||
self.storage.destroy(&self.gpu);
|
||||
self.upload_belt.destroy(&self.gpu);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BladeTextureInfo {
|
||||
pub raw_view: gpu::TextureView,
|
||||
}
|
||||
|
||||
impl BladeAtlas {
|
||||
pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self {
|
||||
BladeAtlas(Mutex::new(BladeAtlasState {
|
||||
gpu: Arc::clone(gpu),
|
||||
upload_belt: BufferBelt::new(BufferBeltDescriptor {
|
||||
memory: gpu::Memory::Upload,
|
||||
min_chunk_size: 0x10000,
|
||||
alignment: 64, // Vulkan `optimalBufferCopyOffsetAlignment` on Intel XE
|
||||
}),
|
||||
storage: BladeAtlasStorage::default(),
|
||||
tiles_by_key: Default::default(),
|
||||
initializations: Vec::new(),
|
||||
uploads: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn destroy(&self) {
|
||||
self.0.lock().destroy();
|
||||
}
|
||||
|
||||
pub fn before_frame(&self, gpu_encoder: &mut gpu::CommandEncoder) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.flush(gpu_encoder);
|
||||
}
|
||||
|
||||
pub fn after_frame(&self, sync_point: &gpu::SyncPoint) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.upload_belt.flush(sync_point);
|
||||
}
|
||||
|
||||
pub fn get_texture_info(&self, id: AtlasTextureId) -> BladeTextureInfo {
|
||||
let lock = self.0.lock();
|
||||
let texture = &lock.storage[id];
|
||||
BladeTextureInfo {
|
||||
raw_view: texture.raw_view,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformAtlas for BladeAtlas {
|
||||
fn get_or_insert_with<'a>(
|
||||
&self,
|
||||
key: &AtlasKey,
|
||||
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
||||
) -> Result<Option<AtlasTile>> {
|
||||
let mut lock = self.0.lock();
|
||||
if let Some(tile) = lock.tiles_by_key.get(key) {
|
||||
Ok(Some(tile.clone()))
|
||||
} else {
|
||||
profiling::scope!("new tile");
|
||||
let Some((size, bytes)) = build()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let tile = lock.allocate(size, key.texture_kind());
|
||||
lock.upload_texture(tile.texture_id, tile.bounds, &bytes);
|
||||
lock.tiles_by_key.insert(key.clone(), tile.clone());
|
||||
Ok(Some(tile))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self, key: &AtlasKey) {
|
||||
let mut lock = self.0.lock();
|
||||
|
||||
let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mut texture) = texture_slot.take() {
|
||||
texture.decrement_ref_count();
|
||||
if texture.is_unreferenced() {
|
||||
lock.storage[id.kind]
|
||||
.free_list
|
||||
.push(texture.id.index as usize);
|
||||
texture.destroy(&lock.gpu);
|
||||
} else {
|
||||
*texture_slot = Some(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BladeAtlasState {
|
||||
fn allocate(&mut self, size: Size<DevicePixels>, texture_kind: AtlasTextureKind) -> AtlasTile {
|
||||
{
|
||||
let textures = &mut self.storage[texture_kind];
|
||||
|
||||
if let Some(tile) = textures
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find_map(|texture| texture.allocate(size))
|
||||
{
|
||||
return tile;
|
||||
}
|
||||
}
|
||||
|
||||
let texture = self.push_texture(size, texture_kind);
|
||||
texture.allocate(size).unwrap()
|
||||
}
|
||||
|
||||
fn push_texture(
|
||||
&mut self,
|
||||
min_size: Size<DevicePixels>,
|
||||
kind: AtlasTextureKind,
|
||||
) -> &mut BladeAtlasTexture {
|
||||
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
|
||||
width: DevicePixels(1024),
|
||||
height: DevicePixels(1024),
|
||||
};
|
||||
|
||||
let size = min_size.max(&DEFAULT_ATLAS_SIZE);
|
||||
let format;
|
||||
let usage;
|
||||
match kind {
|
||||
AtlasTextureKind::Monochrome => {
|
||||
format = gpu::TextureFormat::R8Unorm;
|
||||
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||
}
|
||||
AtlasTextureKind::Polychrome => {
|
||||
format = gpu::TextureFormat::Bgra8Unorm;
|
||||
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||
}
|
||||
}
|
||||
|
||||
let raw = self.gpu.create_texture(gpu::TextureDesc {
|
||||
name: "atlas",
|
||||
format,
|
||||
size: gpu::Extent {
|
||||
width: size.width.into(),
|
||||
height: size.height.into(),
|
||||
depth: 1,
|
||||
},
|
||||
array_layer_count: 1,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: gpu::TextureDimension::D2,
|
||||
usage,
|
||||
external: None,
|
||||
});
|
||||
let raw_view = self.gpu.create_texture_view(
|
||||
raw,
|
||||
gpu::TextureViewDesc {
|
||||
name: "",
|
||||
format,
|
||||
dimension: gpu::ViewDimension::D2,
|
||||
subresources: &Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let texture_list = &mut self.storage[kind];
|
||||
let index = texture_list.free_list.pop();
|
||||
|
||||
let atlas_texture = BladeAtlasTexture {
|
||||
id: AtlasTextureId {
|
||||
index: index.unwrap_or(texture_list.textures.len()) as u32,
|
||||
kind,
|
||||
},
|
||||
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
|
||||
format,
|
||||
raw,
|
||||
raw_view,
|
||||
live_atlas_keys: 0,
|
||||
};
|
||||
|
||||
self.initializations.push(atlas_texture.id);
|
||||
|
||||
if let Some(ix) = index {
|
||||
texture_list.textures[ix] = Some(atlas_texture);
|
||||
texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap()
|
||||
} else {
|
||||
texture_list.textures.push(Some(atlas_texture));
|
||||
texture_list.textures.last_mut().unwrap().as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
|
||||
let data = self.upload_belt.alloc_bytes(bytes, &self.gpu);
|
||||
self.uploads.push(PendingUpload { id, bounds, data });
|
||||
}
|
||||
|
||||
fn flush_initializations(&mut self, encoder: &mut gpu::CommandEncoder) {
|
||||
for id in self.initializations.drain(..) {
|
||||
let texture = &self.storage[id];
|
||||
encoder.init_texture(texture.raw);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, encoder: &mut gpu::CommandEncoder) {
|
||||
self.flush_initializations(encoder);
|
||||
|
||||
let mut transfers = encoder.transfer("atlas");
|
||||
for upload in self.uploads.drain(..) {
|
||||
let texture = &self.storage[upload.id];
|
||||
transfers.copy_buffer_to_texture(
|
||||
upload.data,
|
||||
upload.bounds.size.width.to_bytes(texture.bytes_per_pixel()),
|
||||
gpu::TexturePiece {
|
||||
texture: texture.raw,
|
||||
mip_level: 0,
|
||||
array_layer: 0,
|
||||
origin: [
|
||||
upload.bounds.origin.x.into(),
|
||||
upload.bounds.origin.y.into(),
|
||||
0,
|
||||
],
|
||||
},
|
||||
gpu::Extent {
|
||||
width: upload.bounds.size.width.into(),
|
||||
height: upload.bounds.size.height.into(),
|
||||
depth: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BladeAtlasStorage {
|
||||
monochrome_textures: AtlasTextureList<BladeAtlasTexture>,
|
||||
polychrome_textures: AtlasTextureList<BladeAtlasTexture>,
|
||||
}
|
||||
|
||||
impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
|
||||
type Output = AtlasTextureList<BladeAtlasTexture>;
|
||||
fn index(&self, kind: AtlasTextureKind) -> &Self::Output {
|
||||
match kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::IndexMut<AtlasTextureKind> for BladeAtlasStorage {
|
||||
fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output {
|
||||
match kind {
|
||||
crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Index<AtlasTextureId> for BladeAtlasStorage {
|
||||
type Output = BladeAtlasTexture;
|
||||
fn index(&self, id: AtlasTextureId) -> &Self::Output {
|
||||
let textures = match id.kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
};
|
||||
textures[id.index as usize].as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl BladeAtlasStorage {
|
||||
fn destroy(&mut self, gpu: &gpu::Context) {
|
||||
for mut texture in self.monochrome_textures.drain().flatten() {
|
||||
texture.destroy(gpu);
|
||||
}
|
||||
for mut texture in self.polychrome_textures.drain().flatten() {
|
||||
texture.destroy(gpu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BladeAtlasTexture {
|
||||
id: AtlasTextureId,
|
||||
allocator: BucketedAtlasAllocator,
|
||||
raw: gpu::Texture,
|
||||
raw_view: gpu::TextureView,
|
||||
format: gpu::TextureFormat,
|
||||
live_atlas_keys: u32,
|
||||
}
|
||||
|
||||
impl BladeAtlasTexture {
|
||||
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
|
||||
let allocation = self.allocator.allocate(size.into())?;
|
||||
let tile = AtlasTile {
|
||||
texture_id: self.id,
|
||||
tile_id: allocation.id.into(),
|
||||
padding: 0,
|
||||
bounds: Bounds {
|
||||
origin: allocation.rectangle.min.into(),
|
||||
size,
|
||||
},
|
||||
};
|
||||
self.live_atlas_keys += 1;
|
||||
Some(tile)
|
||||
}
|
||||
|
||||
fn destroy(&mut self, gpu: &gpu::Context) {
|
||||
gpu.destroy_texture(self.raw);
|
||||
gpu.destroy_texture_view(self.raw_view);
|
||||
}
|
||||
|
||||
fn bytes_per_pixel(&self) -> u8 {
|
||||
self.format.block_info().size
|
||||
}
|
||||
|
||||
fn decrement_ref_count(&mut self) {
|
||||
self.live_atlas_keys -= 1;
|
||||
}
|
||||
|
||||
fn is_unreferenced(&mut self) -> bool {
|
||||
self.live_atlas_keys == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size<DevicePixels>> for etagere::Size {
|
||||
fn from(size: Size<DevicePixels>) -> Self {
|
||||
etagere::Size::new(size.width.into(), size.height.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Point> for Point<DevicePixels> {
|
||||
fn from(value: etagere::Point) -> Self {
|
||||
Point {
|
||||
x: DevicePixels::from(value.x),
|
||||
y: DevicePixels::from(value.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Size> for Size<DevicePixels> {
|
||||
fn from(size: etagere::Size) -> Self {
|
||||
Size {
|
||||
width: DevicePixels::from(size.width),
|
||||
height: DevicePixels::from(size.height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Rectangle> for Bounds<DevicePixels> {
|
||||
fn from(rectangle: etagere::Rectangle) -> Self {
|
||||
Bounds {
|
||||
origin: rectangle.min.into(),
|
||||
size: rectangle.size().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use anyhow::Context as _;
|
||||
use blade_graphics as gpu;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg_attr(target_os = "macos", derive(Clone))]
|
||||
pub struct BladeContext {
|
||||
pub(super) gpu: Arc<gpu::Context>,
|
||||
}
|
||||
|
||||
impl BladeContext {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let device_id_forced = match std::env::var("ZED_DEVICE_ID") {
|
||||
Ok(val) => parse_pci_id(&val)
|
||||
.context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
|
||||
.log_err(),
|
||||
Err(std::env::VarError::NotPresent) => None,
|
||||
err => {
|
||||
err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
|
||||
.log_err();
|
||||
None
|
||||
}
|
||||
};
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init(gpu::ContextDesc {
|
||||
presentation: true,
|
||||
validation: false,
|
||||
device_id: device_id_forced.unwrap_or(0),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("{e:?}"))?,
|
||||
);
|
||||
Ok(Self { gpu })
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
|
||||
let mut id = id.trim();
|
||||
|
||||
if id.starts_with("0x") || id.starts_with("0X") {
|
||||
id = &id[2..];
|
||||
}
|
||||
let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
|
||||
let is_4_chars = id.len() == 4;
|
||||
anyhow::ensure!(
|
||||
is_4_chars && is_hex_string,
|
||||
"Expected a 4 digit PCI ID in hexadecimal format"
|
||||
);
|
||||
|
||||
u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_pci_id;
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_id() {
|
||||
assert!(parse_pci_id("0xABCD").is_ok());
|
||||
assert!(parse_pci_id("ABCD").is_ok());
|
||||
assert!(parse_pci_id("abcd").is_ok());
|
||||
assert!(parse_pci_id("1234").is_ok());
|
||||
assert!(parse_pci_id("123").is_err());
|
||||
assert_eq!(
|
||||
parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
|
||||
parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
|
||||
parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
|
||||
parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1072
File diff suppressed because it is too large
Load Diff
+1296
File diff suppressed because it is too large
Load Diff
+41
@@ -0,0 +1,41 @@
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{KeybindingKeystroke, Keystroke};
|
||||
|
||||
/// A trait for platform-specific keyboard layouts
|
||||
pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout ID, which should be unique to the layout
|
||||
fn id(&self) -> &str;
|
||||
/// Get the keyboard layout display name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// A trait for platform-specific keyboard mappings
|
||||
pub trait PlatformKeyboardMapper {
|
||||
/// Map a key equivalent to its platform-specific representation
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke;
|
||||
/// Get the key equivalents for the current keyboard layout,
|
||||
/// only used on macOS
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
|
||||
}
|
||||
|
||||
/// A dummy implementation of the platform keyboard mapper
|
||||
pub struct DummyKeyboardMapper;
|
||||
|
||||
impl PlatformKeyboardMapper for DummyKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
_use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke::from_keystroke(keystroke)
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
+767
@@ -0,0 +1,767 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Write},
|
||||
};
|
||||
|
||||
use crate::PlatformKeyboardMapper;
|
||||
|
||||
/// This is a helper trait so that we can simplify the implementation of some functions
|
||||
pub trait AsKeystroke {
|
||||
/// Returns the GPUI representation of the keystroke.
|
||||
fn as_keystroke(&self) -> &Keystroke;
|
||||
}
|
||||
|
||||
/// A keystroke and associated metadata generated by the platform
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
pub struct Keystroke {
|
||||
/// the state of the modifier keys at the time the keystroke was generated
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
/// key is the character printed on the key that was pressed
|
||||
/// e.g. for option-s, key is "s"
|
||||
/// On layouts that do not have ascii keys (e.g. Thai)
|
||||
/// this will be the ASCII-equivalent character (q instead of ๆ),
|
||||
/// and the typed character will be present in key_char.
|
||||
pub key: String,
|
||||
|
||||
/// key_char is the character that could have been typed when
|
||||
/// this binding was pressed.
|
||||
/// e.g. for s this is "s", for option-s "ß", and cmd-s None
|
||||
pub key_char: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a keystroke that can be used in keybindings and displayed to the user.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct KeybindingKeystroke {
|
||||
/// The GPUI representation of the keystroke.
|
||||
inner: Keystroke,
|
||||
/// The modifiers to display.
|
||||
#[cfg(target_os = "windows")]
|
||||
display_modifiers: Modifiers,
|
||||
/// The key to display.
|
||||
#[cfg(target_os = "windows")]
|
||||
display_key: String,
|
||||
}
|
||||
|
||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidKeystrokeError {
|
||||
/// The invalid keystroke.
|
||||
pub keystroke: String,
|
||||
}
|
||||
|
||||
impl Error for InvalidKeystrokeError {}
|
||||
|
||||
impl Display for InvalidKeystrokeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Invalid keystroke \"{}\". {}",
|
||||
self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
|
||||
pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
|
||||
(`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
|
||||
followed by a key, separated by `-`.";
|
||||
|
||||
impl Keystroke {
|
||||
/// When matching a key we cannot know whether the user intended to type
|
||||
/// the key_char or the key itself. On some non-US keyboards keys we use in our
|
||||
/// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
|
||||
/// and on some keyboards the IME handler converts a sequence of keys into a
|
||||
/// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
|
||||
///
|
||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||
/// both possibilities for self against the target.
|
||||
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
.as_ref()
|
||||
.filter(|key_char| key_char != &&self.key)
|
||||
{
|
||||
let ime_modifiers = Modifiers {
|
||||
control: self.modifiers.control,
|
||||
platform: self.modifiers.platform,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
.as_ref()
|
||||
.filter(|key_char| key_char != &&self.key)
|
||||
{
|
||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
/// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
|
||||
/// key_char syntax is only used for generating test events,
|
||||
/// secondary means "cmd" on macOS and "ctrl" on other platforms
|
||||
/// when matching a key with an key_char set will be matched without it.
|
||||
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut modifiers = Modifiers::none();
|
||||
let mut key = None;
|
||||
let mut key_char = None;
|
||||
|
||||
let mut components = source.split('-').peekable();
|
||||
while let Some(component) = components.next() {
|
||||
if component.eq_ignore_ascii_case("ctrl") {
|
||||
modifiers.control = true;
|
||||
continue;
|
||||
}
|
||||
if component.eq_ignore_ascii_case("alt") {
|
||||
modifiers.alt = true;
|
||||
continue;
|
||||
}
|
||||
if component.eq_ignore_ascii_case("shift") {
|
||||
modifiers.shift = true;
|
||||
continue;
|
||||
}
|
||||
if component.eq_ignore_ascii_case("fn") {
|
||||
modifiers.function = true;
|
||||
continue;
|
||||
}
|
||||
if component.eq_ignore_ascii_case("secondary") {
|
||||
if cfg!(target_os = "macos") {
|
||||
modifiers.platform = true;
|
||||
} else {
|
||||
modifiers.control = true;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_platform = component.eq_ignore_ascii_case("cmd")
|
||||
|| component.eq_ignore_ascii_case("super")
|
||||
|| component.eq_ignore_ascii_case("win");
|
||||
|
||||
if is_platform {
|
||||
modifiers.platform = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut key_str = component.to_string();
|
||||
|
||||
if let Some(next) = components.peek() {
|
||||
if next.is_empty() && source.ends_with('-') {
|
||||
key = Some(String::from("-"));
|
||||
break;
|
||||
} else if next.len() > 1 && next.starts_with('>') {
|
||||
key = Some(key_str);
|
||||
key_char = Some(String::from(&next[1..]));
|
||||
components.next();
|
||||
} else {
|
||||
return Err(InvalidKeystrokeError {
|
||||
keystroke: source.to_owned(),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
|
||||
// Convert to shift + lowercase char
|
||||
modifiers.shift = true;
|
||||
key_str.make_ascii_lowercase();
|
||||
} else {
|
||||
// convert ascii chars to lowercase so that named keys like "tab" and "enter"
|
||||
// are accepted case insensitively and stored how we expect so they are matched properly
|
||||
key_str.make_ascii_lowercase()
|
||||
}
|
||||
key = Some(key_str);
|
||||
}
|
||||
|
||||
// Allow for the user to specify a keystroke modifier as the key itself
|
||||
// This sets the `key` to the modifier, and disables the modifier
|
||||
key = key.or_else(|| {
|
||||
use std::mem;
|
||||
// std::mem::take clears bool incase its true
|
||||
if mem::take(&mut modifiers.shift) {
|
||||
Some("shift".to_string())
|
||||
} else if mem::take(&mut modifiers.control) {
|
||||
Some("control".to_string())
|
||||
} else if mem::take(&mut modifiers.alt) {
|
||||
Some("alt".to_string())
|
||||
} else if mem::take(&mut modifiers.platform) {
|
||||
Some("platform".to_string())
|
||||
} else if mem::take(&mut modifiers.function) {
|
||||
Some("function".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let key = key.ok_or_else(|| InvalidKeystrokeError {
|
||||
keystroke: source.to_owned(),
|
||||
})?;
|
||||
|
||||
Ok(Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char,
|
||||
})
|
||||
}
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
unparse(&self.modifiers, &self.key)
|
||||
}
|
||||
|
||||
/// Returns true if this keystroke left
|
||||
/// the ime system in an incomplete state.
|
||||
pub fn is_ime_in_progress(&self) -> bool {
|
||||
self.key_char.is_none()
|
||||
&& (is_printable_key(&self.key) || self.key.is_empty())
|
||||
&& !(self.modifiers.platform
|
||||
|| self.modifiers.control
|
||||
|| self.modifiers.function
|
||||
|| self.modifiers.alt)
|
||||
}
|
||||
|
||||
/// Returns a new keystroke with the key_char filled.
|
||||
/// This is used for dispatch_keystroke where we want users to
|
||||
/// be able to simulate typing "space", etc.
|
||||
pub fn with_simulated_ime(mut self) -> Self {
|
||||
if self.key_char.is_none()
|
||||
&& !self.modifiers.platform
|
||||
&& !self.modifiers.control
|
||||
&& !self.modifiers.function
|
||||
&& !self.modifiers.alt
|
||||
{
|
||||
self.key_char = match self.key.as_str() {
|
||||
"space" => Some(" ".into()),
|
||||
"tab" => Some("\t".into()),
|
||||
"enter" => Some("\n".into()),
|
||||
key if !is_printable_key(key) || key.is_empty() => None,
|
||||
key => {
|
||||
if self.modifiers.shift {
|
||||
Some(key.to_uppercase())
|
||||
} else {
|
||||
Some(key.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl KeybindingKeystroke {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
|
||||
KeybindingKeystroke {
|
||||
inner,
|
||||
display_modifiers,
|
||||
display_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper.
|
||||
pub fn new_with_mapper(
|
||||
inner: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> Self {
|
||||
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
|
||||
}
|
||||
|
||||
/// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping.
|
||||
pub fn from_keystroke(keystroke: Keystroke) -> Self {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let key = keystroke.key.clone();
|
||||
let modifiers = keystroke.modifiers;
|
||||
KeybindingKeystroke {
|
||||
inner: keystroke,
|
||||
display_modifiers: modifiers,
|
||||
display_key: key,
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
KeybindingKeystroke { inner: keystroke }
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the GPUI representation of the keystroke.
|
||||
pub fn inner(&self) -> &Keystroke {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Returns the modifiers.
|
||||
///
|
||||
/// Platform-specific behavior:
|
||||
/// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
|
||||
/// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
|
||||
/// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
|
||||
pub fn modifiers(&self) -> &Modifiers {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
&self.display_modifiers
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
&self.inner.modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the key.
|
||||
///
|
||||
/// Platform-specific behavior:
|
||||
/// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
|
||||
/// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
|
||||
pub fn key(&self) -> &str {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
&self.display_key
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
&self.inner.key
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
|
||||
pub fn set_modifiers(&mut self, modifiers: Modifiers) {
|
||||
self.inner.modifiers = modifiers;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.display_modifiers = modifiers;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
|
||||
pub fn set_key(&mut self, key: String) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.display_key = key.clone();
|
||||
}
|
||||
self.inner.key = key;
|
||||
}
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
unparse(&self.display_modifiers, &self.display_key)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
unparse(&self.inner.modifiers, &self.inner.key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the key_char
|
||||
pub fn remove_key_char(&mut self) {
|
||||
self.inner.key_char = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_printable_key(key: &str) -> bool {
|
||||
!matches!(
|
||||
key,
|
||||
"f1" | "f2"
|
||||
| "f3"
|
||||
| "f4"
|
||||
| "f5"
|
||||
| "f6"
|
||||
| "f7"
|
||||
| "f8"
|
||||
| "f9"
|
||||
| "f10"
|
||||
| "f11"
|
||||
| "f12"
|
||||
| "f13"
|
||||
| "f14"
|
||||
| "f15"
|
||||
| "f16"
|
||||
| "f17"
|
||||
| "f18"
|
||||
| "f19"
|
||||
| "f20"
|
||||
| "f21"
|
||||
| "f22"
|
||||
| "f23"
|
||||
| "f24"
|
||||
| "f25"
|
||||
| "f26"
|
||||
| "f27"
|
||||
| "f28"
|
||||
| "f29"
|
||||
| "f30"
|
||||
| "f31"
|
||||
| "f32"
|
||||
| "f33"
|
||||
| "f34"
|
||||
| "f35"
|
||||
| "backspace"
|
||||
| "delete"
|
||||
| "left"
|
||||
| "right"
|
||||
| "up"
|
||||
| "down"
|
||||
| "pageup"
|
||||
| "pagedown"
|
||||
| "insert"
|
||||
| "home"
|
||||
| "end"
|
||||
| "back"
|
||||
| "forward"
|
||||
| "escape"
|
||||
)
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Keystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(&self.modifiers, f)?;
|
||||
display_key(&self.key, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeybindingKeystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(self.modifiers(), f)?;
|
||||
display_key(self.key(), f)
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of the modifier keys at some point in time
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
|
||||
pub struct Modifiers {
|
||||
/// The control key
|
||||
#[serde(default)]
|
||||
pub control: bool,
|
||||
|
||||
/// The alt key
|
||||
/// Sometimes also known as the 'meta' key
|
||||
#[serde(default)]
|
||||
pub alt: bool,
|
||||
|
||||
/// The shift key
|
||||
#[serde(default)]
|
||||
pub shift: bool,
|
||||
|
||||
/// The command key, on macos
|
||||
/// the windows key, on windows
|
||||
/// the super key, on linux
|
||||
#[serde(default)]
|
||||
pub platform: bool,
|
||||
|
||||
/// The function key
|
||||
#[serde(default)]
|
||||
pub function: bool,
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
/// Returns whether any modifier key is pressed.
|
||||
pub fn modified(&self) -> bool {
|
||||
self.control || self.alt || self.shift || self.platform || self.function
|
||||
}
|
||||
|
||||
/// Whether the semantically 'secondary' modifier key is pressed.
|
||||
///
|
||||
/// On macOS, this is the command key.
|
||||
/// On Linux and Windows, this is the control key.
|
||||
pub fn secondary(&self) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
self.platform
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
self.control
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns how many modifier keys are pressed.
|
||||
pub fn number_of_modifiers(&self) -> u8 {
|
||||
self.control as u8
|
||||
+ self.alt as u8
|
||||
+ self.shift as u8
|
||||
+ self.platform as u8
|
||||
+ self.function as u8
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with no modifiers.
|
||||
pub fn none() -> Modifiers {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just the command key.
|
||||
pub fn command() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Returns [`Modifiers`] with just the secondary key pressed.
|
||||
pub fn secondary_key() -> Modifiers {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Modifiers {
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just the windows key.
|
||||
pub fn windows() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just the super key.
|
||||
pub fn super_key() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just control.
|
||||
pub fn control() -> Modifiers {
|
||||
Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just alt.
|
||||
pub fn alt() -> Modifiers {
|
||||
Modifiers {
|
||||
alt: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just shift.
|
||||
pub fn shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with command + shift.
|
||||
pub fn command_shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with command + shift.
|
||||
pub fn control_shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
control: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
|
||||
pub fn is_subset_of(&self, other: &Modifiers) -> bool {
|
||||
(*other & *self) == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for Modifiers {
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(mut self, other: Self) -> Self::Output {
|
||||
self |= other;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOrAssign for Modifiers {
|
||||
fn bitor_assign(&mut self, other: Self) {
|
||||
self.control |= other.control;
|
||||
self.alt |= other.alt;
|
||||
self.shift |= other.shift;
|
||||
self.platform |= other.platform;
|
||||
self.function |= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitXor for Modifiers {
|
||||
type Output = Self;
|
||||
fn bitxor(mut self, rhs: Self) -> Self::Output {
|
||||
self ^= rhs;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitXorAssign for Modifiers {
|
||||
fn bitxor_assign(&mut self, other: Self) {
|
||||
self.control ^= other.control;
|
||||
self.alt ^= other.alt;
|
||||
self.shift ^= other.shift;
|
||||
self.platform ^= other.platform;
|
||||
self.function ^= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAnd for Modifiers {
|
||||
type Output = Self;
|
||||
fn bitand(mut self, rhs: Self) -> Self::Output {
|
||||
self &= rhs;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAndAssign for Modifiers {
|
||||
fn bitand_assign(&mut self, other: Self) {
|
||||
self.control &= other.control;
|
||||
self.alt &= other.alt;
|
||||
self.shift &= other.shift;
|
||||
self.platform &= other.platform;
|
||||
self.function &= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of the capslock key at some point in time
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
|
||||
pub struct Capslock {
|
||||
/// The capslock key is on
|
||||
#[serde(default)]
|
||||
pub on: bool,
|
||||
}
|
||||
|
||||
impl AsKeystroke for Keystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsKeystroke for KeybindingKeystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let key = match key {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
||||
let mut result = String::new();
|
||||
if modifiers.function {
|
||||
result.push_str("fn-");
|
||||
}
|
||||
if modifiers.control {
|
||||
result.push_str("ctrl-");
|
||||
}
|
||||
if modifiers.alt {
|
||||
result.push_str("alt-");
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
result.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
result.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
result.push_str("win-");
|
||||
}
|
||||
if modifiers.shift {
|
||||
result.push_str("shift-");
|
||||
}
|
||||
result.push_str(&key);
|
||||
result
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
mod dispatcher;
|
||||
mod headless;
|
||||
mod keyboard;
|
||||
mod platform;
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
mod text_system;
|
||||
#[cfg(feature = "wayland")]
|
||||
mod wayland;
|
||||
#[cfg(feature = "x11")]
|
||||
mod x11;
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
mod xdg_desktop_portal;
|
||||
|
||||
pub(crate) use dispatcher::*;
|
||||
pub(crate) use headless::*;
|
||||
pub(crate) use keyboard::*;
|
||||
pub(crate) use platform::*;
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
pub(crate) use text_system::*;
|
||||
#[cfg(feature = "wayland")]
|
||||
pub(crate) use wayland::*;
|
||||
#[cfg(feature = "x11")]
|
||||
pub(crate) use x11::*;
|
||||
|
||||
#[cfg(all(feature = "screen-capture", any(feature = "wayland", feature = "x11")))]
|
||||
pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
|
||||
#[cfg(not(all(feature = "screen-capture", any(feature = "wayland", feature = "x11"))))]
|
||||
pub(crate) type PlatformScreenCaptureFrame = ();
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
use async_task::Runnable;
|
||||
use calloop::{
|
||||
EventLoop,
|
||||
channel::{self, Sender},
|
||||
timer::TimeoutAction,
|
||||
};
|
||||
use std::{
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
struct TimerAfter {
|
||||
duration: Duration,
|
||||
runnable: Runnable,
|
||||
}
|
||||
|
||||
pub(crate) struct LinuxDispatcher {
|
||||
main_sender: Sender<Runnable>,
|
||||
timer_sender: Sender<TimerAfter>,
|
||||
background_sender: flume::Sender<Runnable>,
|
||||
_background_threads: Vec<thread::JoinHandle<()>>,
|
||||
main_thread_id: thread::ThreadId,
|
||||
}
|
||||
|
||||
impl LinuxDispatcher {
|
||||
pub fn new(main_sender: Sender<Runnable>) -> Self {
|
||||
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
|
||||
let thread_count = std::thread::available_parallelism()
|
||||
.map(|i| i.get())
|
||||
.unwrap_or(1);
|
||||
|
||||
let mut background_threads = (0..thread_count)
|
||||
.map(|i| {
|
||||
let receiver = background_receiver.clone();
|
||||
std::thread::Builder::new()
|
||||
.name(format!("Worker-{i}"))
|
||||
.spawn(move || {
|
||||
for runnable in receiver {
|
||||
let start = Instant::now();
|
||||
|
||||
runnable.run();
|
||||
|
||||
log::trace!(
|
||||
"background thread {}: ran runnable. took: {:?}",
|
||||
i,
|
||||
start.elapsed()
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
|
||||
let timer_thread = std::thread::Builder::new()
|
||||
.name("Timer".to_owned())
|
||||
.spawn(|| {
|
||||
let mut event_loop: EventLoop<()> =
|
||||
EventLoop::try_new().expect("Failed to initialize timer loop!");
|
||||
|
||||
let handle = event_loop.handle();
|
||||
let timer_handle = event_loop.handle();
|
||||
handle
|
||||
.insert_source(timer_channel, move |e, _, _| {
|
||||
if let channel::Event::Msg(timer) = e {
|
||||
// This has to be in an option to satisfy the borrow checker. The callback below should only be scheduled once.
|
||||
let mut runnable = Some(timer.runnable);
|
||||
timer_handle
|
||||
.insert_source(
|
||||
calloop::timer::Timer::from_duration(timer.duration),
|
||||
move |_, _, _| {
|
||||
if let Some(runnable) = runnable.take() {
|
||||
runnable.run();
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
},
|
||||
)
|
||||
.expect("Failed to start timer");
|
||||
}
|
||||
})
|
||||
.expect("Failed to start timer thread");
|
||||
|
||||
event_loop.run(None, &mut (), |_| {}).log_err();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
background_threads.push(timer_thread);
|
||||
|
||||
Self {
|
||||
main_sender,
|
||||
timer_sender,
|
||||
background_sender,
|
||||
_background_threads: background_threads,
|
||||
main_thread_id: thread::current().id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDispatcher for LinuxDispatcher {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
thread::current().id() == self.main_thread_id
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
|
||||
self.background_sender.send(runnable).unwrap();
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||
self.main_sender.send(runnable).unwrap_or_else(|runnable| {
|
||||
// NOTE: Runnable may wrap a Future that is !Send.
|
||||
//
|
||||
// This is usually safe because we only poll it on the main thread.
|
||||
// However if the send fails, we know that:
|
||||
// 1. main_receiver has been dropped (which implies the app is shutting down)
|
||||
// 2. we are on a background thread.
|
||||
// It is not safe to drop something !Send on the wrong thread, and
|
||||
// the app will exit soon anyway, so we must forget the runnable.
|
||||
std::mem::forget(runnable);
|
||||
});
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
|
||||
self.timer_sender
|
||||
.send(TimerAfter { duration, runnable })
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod client;
|
||||
|
||||
pub(crate) use client::*;
|
||||
@@ -0,0 +1,134 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
AnyWindowHandle, CursorStyle, DisplayId, LinuxKeyboardLayout, PlatformDisplay,
|
||||
PlatformKeyboardLayout, WindowParams,
|
||||
};
|
||||
|
||||
pub struct HeadlessClientState {
|
||||
pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
|
||||
pub(crate) common: LinuxCommon,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct HeadlessClient(Rc<RefCell<HeadlessClientState>>);
|
||||
|
||||
impl HeadlessClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle
|
||||
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
|
||||
event_loop: Some(event_loop),
|
||||
_loop_handle: handle,
|
||||
common,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl LinuxClient for HeadlessClient {
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
|
||||
f(&mut self.0.borrow_mut().common)
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(LinuxKeyboardLayout::new("unknown".into()))
|
||||
}
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn display(&self, _id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
{
|
||||
let (mut tx, rx) = futures::channel::oneshot::channel();
|
||||
tx.send(Err(anyhow::anyhow!(
|
||||
"Headless mode does not support screen capture."
|
||||
)))
|
||||
.ok();
|
||||
rx
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
None
|
||||
}
|
||||
|
||||
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_handle: AnyWindowHandle,
|
||||
_params: WindowParams,
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||
anyhow::bail!("neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode");
|
||||
}
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
"headless"
|
||||
}
|
||||
|
||||
fn set_cursor_style(&self, _style: CursorStyle) {}
|
||||
|
||||
fn open_uri(&self, _uri: &str) {}
|
||||
|
||||
fn reveal_path(&self, _path: std::path::PathBuf) {}
|
||||
|
||||
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn run(&self) {
|
||||
let mut event_loop = self
|
||||
.0
|
||||
.borrow_mut()
|
||||
.event_loop
|
||||
.take()
|
||||
.expect("App is already running");
|
||||
|
||||
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
use crate::{PlatformKeyboardLayout, SharedString};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct LinuxKeyboardLayout {
|
||||
name: SharedString,
|
||||
}
|
||||
|
||||
impl PlatformKeyboardLayout for LinuxKeyboardLayout {
|
||||
fn id(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl LinuxKeyboardLayout {
|
||||
pub(crate) fn new(name: SharedString) -> Self {
|
||||
Self { name }
|
||||
}
|
||||
}
|
||||
+1039
File diff suppressed because it is too large
Load Diff
+581
@@ -0,0 +1,581 @@
|
||||
use crate::{
|
||||
Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight,
|
||||
GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, SUBPIXEL_VARIANTS_X,
|
||||
SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, point, size,
|
||||
};
|
||||
use anyhow::{Context as _, Ok, Result};
|
||||
use collections::HashMap;
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
|
||||
FontSystem, ShapeBuffer, ShapeLine, SwashCache,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use parking_lot::RwLock;
|
||||
use pathfinder_geometry::{
|
||||
rect::{RectF, RectI},
|
||||
vector::{Vector2F, Vector2I},
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct FontKey {
|
||||
family: SharedString,
|
||||
features: FontFeatures,
|
||||
}
|
||||
|
||||
impl FontKey {
|
||||
fn new(family: SharedString, features: FontFeatures) -> Self {
|
||||
Self { family, features }
|
||||
}
|
||||
}
|
||||
|
||||
struct CosmicTextSystemState {
|
||||
swash_cache: SwashCache,
|
||||
font_system: FontSystem,
|
||||
scratch: ShapeBuffer,
|
||||
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
|
||||
loaded_fonts: Vec<LoadedFont>,
|
||||
/// Caches the `FontId`s associated with a specific family to avoid iterating the font database
|
||||
/// for every font face in a family.
|
||||
font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
|
||||
}
|
||||
|
||||
struct LoadedFont {
|
||||
font: Arc<CosmicTextFont>,
|
||||
features: CosmicFontFeatures,
|
||||
is_known_emoji_font: bool,
|
||||
}
|
||||
|
||||
impl CosmicTextSystem {
|
||||
pub(crate) fn new() -> Self {
|
||||
// todo(linux) make font loading non-blocking
|
||||
let mut font_system = FontSystem::new();
|
||||
|
||||
Self(RwLock::new(CosmicTextSystemState {
|
||||
font_system,
|
||||
swash_cache: SwashCache::new(),
|
||||
scratch: ShapeBuffer::default(),
|
||||
loaded_fonts: Vec::new(),
|
||||
font_ids_by_family_cache: HashMap::default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CosmicTextSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformTextSystem for CosmicTextSystem {
|
||||
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||
self.0.write().add_fonts(fonts)
|
||||
}
|
||||
|
||||
fn all_font_names(&self) -> Vec<String> {
|
||||
let mut result = self
|
||||
.0
|
||||
.read()
|
||||
.font_system
|
||||
.db()
|
||||
.faces()
|
||||
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
|
||||
.collect_vec();
|
||||
result.sort();
|
||||
result.dedup();
|
||||
result
|
||||
}
|
||||
|
||||
fn font_id(&self, font: &Font) -> Result<FontId> {
|
||||
// todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit?
|
||||
let mut state = self.0.write();
|
||||
let key = FontKey::new(font.family.clone(), font.features.clone());
|
||||
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = state.load_family(&font.family, &font.features)?;
|
||||
state.font_ids_by_family_cache.insert(key.clone(), font_ids);
|
||||
state.font_ids_by_family_cache[&key].as_ref()
|
||||
};
|
||||
|
||||
// todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here
|
||||
let candidate_properties = candidates
|
||||
.iter()
|
||||
.map(|font_id| {
|
||||
let database_id = state.loaded_font(*font_id).font.id();
|
||||
let face_info = state.font_system.db().face(database_id).expect("");
|
||||
face_info_into_properties(face_info)
|
||||
})
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
|
||||
let ix =
|
||||
font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
|
||||
.context("requested font family contains no font matching the other parameters")?;
|
||||
|
||||
Ok(candidates[ix])
|
||||
}
|
||||
|
||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
|
||||
let metrics = self
|
||||
.0
|
||||
.read()
|
||||
.loaded_font(font_id)
|
||||
.font
|
||||
.as_swash()
|
||||
.metrics(&[]);
|
||||
|
||||
FontMetrics {
|
||||
units_per_em: metrics.units_per_em as u32,
|
||||
ascent: metrics.ascent,
|
||||
descent: -metrics.descent, // todo(linux) confirm this is correct
|
||||
line_gap: metrics.leading,
|
||||
underline_position: metrics.underline_offset,
|
||||
underline_thickness: metrics.stroke_size,
|
||||
cap_height: metrics.cap_height,
|
||||
x_height: metrics.x_height,
|
||||
// todo(linux): Compute this correctly
|
||||
bounding_box: Bounds {
|
||||
origin: point(0.0, 0.0),
|
||||
size: size(metrics.max_width, metrics.ascent + metrics.descent),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
|
||||
let lock = self.0.read();
|
||||
let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
|
||||
let glyph_id = glyph_id.0 as u16;
|
||||
// todo(linux): Compute this correctly
|
||||
// see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620
|
||||
Ok(Bounds {
|
||||
origin: point(0.0, 0.0),
|
||||
size: size(
|
||||
glyph_metrics.advance_width(glyph_id),
|
||||
glyph_metrics.advance_height(glyph_id),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||
self.0.read().advance(font_id, glyph_id)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.0.read().glyph_for_char(font_id, ch)
|
||||
}
|
||||
|
||||
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
self.0.write().raster_bounds(params)
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
self.0.write().rasterize_glyph(params, raster_bounds)
|
||||
}
|
||||
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
|
||||
self.0.write().layout_line(text, font_size, runs)
|
||||
}
|
||||
}
|
||||
|
||||
impl CosmicTextSystemState {
|
||||
fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
|
||||
&self.loaded_fonts[font_id.0]
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||
let db = self.font_system.db_mut();
|
||||
for bytes in fonts {
|
||||
match bytes {
|
||||
Cow::Borrowed(embedded_font) => {
|
||||
db.load_font_data(embedded_font.to_vec());
|
||||
}
|
||||
Cow::Owned(bytes) => {
|
||||
db.load_font_data(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn load_family(
|
||||
&mut self,
|
||||
name: &str,
|
||||
features: &FontFeatures,
|
||||
) -> Result<SmallVec<[FontId; 4]>> {
|
||||
// TODO: Determine the proper system UI font.
|
||||
let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans");
|
||||
|
||||
let families = self
|
||||
.font_system
|
||||
.db()
|
||||
.faces()
|
||||
.filter(|face| face.families.iter().any(|family| *name == family.0))
|
||||
.map(|face| (face.id, face.post_script_name.clone()))
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
|
||||
let mut loaded_font_ids = SmallVec::new();
|
||||
for (font_id, postscript_name) in families {
|
||||
let font = self
|
||||
.font_system
|
||||
.get_font(font_id)
|
||||
.context("Could not load font")?;
|
||||
|
||||
// HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
|
||||
let allowed_bad_font_names = [
|
||||
"SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
|
||||
"Segoe Fluent Icons",
|
||||
];
|
||||
|
||||
if font.as_swash().charmap().map('m') == 0
|
||||
&& !allowed_bad_font_names.contains(&postscript_name.as_str())
|
||||
{
|
||||
self.font_system.db_mut().remove_face(font.id());
|
||||
continue;
|
||||
};
|
||||
|
||||
let font_id = FontId(self.loaded_fonts.len());
|
||||
loaded_font_ids.push(font_id);
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: features.try_into()?,
|
||||
is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(loaded_font_ids)
|
||||
}
|
||||
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||
let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
|
||||
Ok(Size {
|
||||
width: glyph_metrics.advance_width(glyph_id.0 as u16),
|
||||
height: glyph_metrics.advance_height(glyph_id.0 as u16),
|
||||
})
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
|
||||
if glyph_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(GlyphId(glyph_id.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let font = &self.loaded_fonts[params.font_id.0].font;
|
||||
let subpixel_shift = point(
|
||||
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
|
||||
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
|
||||
);
|
||||
let image = self
|
||||
.swash_cache
|
||||
.get_image(
|
||||
&mut self.font_system,
|
||||
CacheKey::new(
|
||||
font.id(),
|
||||
params.glyph_id.0 as u16,
|
||||
(params.font_size * params.scale_factor).into(),
|
||||
(subpixel_shift.x, subpixel_shift.y.trunc()),
|
||||
cosmic_text::CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||
Ok(Bounds {
|
||||
origin: point(image.placement.left.into(), (-image.placement.top).into()),
|
||||
size: size(image.placement.width.into(), image.placement.height.into()),
|
||||
})
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn rasterize_glyph(
|
||||
&mut self,
|
||||
params: &RenderGlyphParams,
|
||||
glyph_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
|
||||
anyhow::bail!("glyph bounds are empty");
|
||||
} else {
|
||||
let bitmap_size = glyph_bounds.size;
|
||||
let font = &self.loaded_fonts[params.font_id.0].font;
|
||||
let subpixel_shift = point(
|
||||
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
|
||||
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
|
||||
);
|
||||
let mut image = self
|
||||
.swash_cache
|
||||
.get_image(
|
||||
&mut self.font_system,
|
||||
CacheKey::new(
|
||||
font.id(),
|
||||
params.glyph_id.0 as u16,
|
||||
(params.font_size * params.scale_factor).into(),
|
||||
(subpixel_shift.x, subpixel_shift.y.trunc()),
|
||||
cosmic_text::CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||
|
||||
if params.is_emoji {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in image.data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((bitmap_size, image.data))
|
||||
}
|
||||
}
|
||||
|
||||
/// This is used when cosmic_text has chosen a fallback font instead of using the requested
|
||||
/// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
|
||||
/// yet have an entry for this fallback font, and so one is added.
|
||||
///
|
||||
/// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
|
||||
/// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
|
||||
/// current use of this field is for the *input* of `layout_line`, and so it's fine to use
|
||||
/// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
|
||||
fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
|
||||
if let Some(ix) = self
|
||||
.loaded_fonts
|
||||
.iter()
|
||||
.position(|loaded_font| loaded_font.font.id() == id)
|
||||
{
|
||||
FontId(ix)
|
||||
} else {
|
||||
let font = self.font_system.get_font(id).unwrap();
|
||||
let face = self.font_system.db().face(id).unwrap();
|
||||
|
||||
let font_id = FontId(self.loaded_fonts.len());
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: CosmicFontFeatures::new(),
|
||||
is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
|
||||
});
|
||||
|
||||
font_id
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
|
||||
let mut attrs_list = AttrsList::new(&Attrs::new());
|
||||
let mut offs = 0;
|
||||
for run in font_runs {
|
||||
let loaded_font = self.loaded_font(run.font_id);
|
||||
let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
|
||||
|
||||
attrs_list.add_span(
|
||||
offs..(offs + run.len),
|
||||
&Attrs::new()
|
||||
.metadata(run.font_id.0)
|
||||
.family(Family::Name(&font.families.first().unwrap().0))
|
||||
.stretch(font.stretch)
|
||||
.style(font.style)
|
||||
.weight(font.weight)
|
||||
.font_features(loaded_font.features.clone()),
|
||||
);
|
||||
offs += run.len;
|
||||
}
|
||||
|
||||
let line = ShapeLine::new(
|
||||
&mut self.font_system,
|
||||
text,
|
||||
&attrs_list,
|
||||
cosmic_text::Shaping::Advanced,
|
||||
4,
|
||||
);
|
||||
let mut layout_lines = Vec::with_capacity(1);
|
||||
line.layout_to_buffer(
|
||||
&mut self.scratch,
|
||||
font_size.0,
|
||||
None, // We do our own wrapping
|
||||
cosmic_text::Wrap::None,
|
||||
None,
|
||||
&mut layout_lines,
|
||||
None,
|
||||
);
|
||||
let layout = layout_lines.first().unwrap();
|
||||
|
||||
let mut runs: Vec<ShapedRun> = Vec::new();
|
||||
for glyph in &layout.glyphs {
|
||||
let mut font_id = FontId(glyph.metadata);
|
||||
let mut loaded_font = self.loaded_font(font_id);
|
||||
if loaded_font.font.id() != glyph.font_id {
|
||||
font_id = self.font_id_for_cosmic_id(glyph.font_id);
|
||||
loaded_font = self.loaded_font(font_id);
|
||||
}
|
||||
let is_emoji = loaded_font.is_known_emoji_font;
|
||||
|
||||
// HACK: Prevent crash caused by variation selectors.
|
||||
if glyph.glyph_id == 3 && is_emoji {
|
||||
continue;
|
||||
}
|
||||
|
||||
let shaped_glyph = ShapedGlyph {
|
||||
id: GlyphId(glyph.glyph_id as u32),
|
||||
position: point(glyph.x.into(), glyph.y.into()),
|
||||
index: glyph.start,
|
||||
is_emoji,
|
||||
};
|
||||
|
||||
if let Some(last_run) = runs
|
||||
.last_mut()
|
||||
.filter(|last_run| last_run.font_id == font_id)
|
||||
{
|
||||
last_run.glyphs.push(shaped_glyph);
|
||||
} else {
|
||||
runs.push(ShapedRun {
|
||||
font_id,
|
||||
glyphs: vec![shaped_glyph],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LineLayout {
|
||||
font_size,
|
||||
width: layout.w.into(),
|
||||
ascent: layout.max_ascent.into(),
|
||||
descent: layout.max_descent.into(),
|
||||
runs,
|
||||
len: text.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&FontFeatures> for CosmicFontFeatures {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(features: &FontFeatures) -> Result<Self> {
|
||||
let mut result = CosmicFontFeatures::new();
|
||||
for feature in features.0.iter() {
|
||||
let name_bytes: [u8; 4] = feature
|
||||
.0
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.context("Incorrect feature flag format")?;
|
||||
|
||||
let tag = cosmic_text::FeatureTag::new(&name_bytes);
|
||||
|
||||
result.set(tag, feature.1);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectF> for Bounds<f32> {
|
||||
fn from(rect: RectF) -> Self {
|
||||
Bounds {
|
||||
origin: point(rect.origin_x(), rect.origin_y()),
|
||||
size: size(rect.width(), rect.height()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectI> for Bounds<DevicePixels> {
|
||||
fn from(rect: RectI) -> Self {
|
||||
Bounds {
|
||||
origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
|
||||
size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector2I> for Size<DevicePixels> {
|
||||
fn from(value: Vector2I) -> Self {
|
||||
size(value.x().into(), value.y().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectI> for Bounds<i32> {
|
||||
fn from(rect: RectI) -> Self {
|
||||
Bounds {
|
||||
origin: point(rect.origin_x(), rect.origin_y()),
|
||||
size: size(rect.width(), rect.height()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point<u32>> for Vector2I {
|
||||
fn from(size: Point<u32>) -> Self {
|
||||
Vector2I::new(size.x as i32, size.y as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector2F> for Size<f32> {
|
||||
fn from(vec: Vector2F) -> Self {
|
||||
size(vec.x(), vec.y())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontWeight> for cosmic_text::Weight {
|
||||
fn from(value: FontWeight) -> Self {
|
||||
cosmic_text::Weight(value.0 as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontStyle> for cosmic_text::Style {
|
||||
fn from(style: FontStyle) -> Self {
|
||||
match style {
|
||||
FontStyle::Normal => cosmic_text::Style::Normal,
|
||||
FontStyle::Italic => cosmic_text::Style::Italic,
|
||||
FontStyle::Oblique => cosmic_text::Style::Oblique,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties {
|
||||
font_kit::properties::Properties {
|
||||
style: match font.style {
|
||||
crate::FontStyle::Normal => font_kit::properties::Style::Normal,
|
||||
crate::FontStyle::Italic => font_kit::properties::Style::Italic,
|
||||
crate::FontStyle::Oblique => font_kit::properties::Style::Oblique,
|
||||
},
|
||||
weight: font_kit::properties::Weight(font.weight.0),
|
||||
stretch: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn face_info_into_properties(
|
||||
face_info: &cosmic_text::fontdb::FaceInfo,
|
||||
) -> font_kit::properties::Properties {
|
||||
font_kit::properties::Properties {
|
||||
style: match face_info.style {
|
||||
cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
|
||||
cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
|
||||
cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
|
||||
},
|
||||
// both libs use the same values for weight
|
||||
weight: font_kit::properties::Weight(face_info.weight.0.into()),
|
||||
stretch: match face_info.stretch {
|
||||
cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
|
||||
cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
|
||||
cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
|
||||
cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
|
||||
cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
|
||||
cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
|
||||
cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
|
||||
cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
|
||||
cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn check_is_known_emoji_font(postscript_name: &str) -> bool {
|
||||
// TODO: Include other common emoji fonts
|
||||
postscript_name == "NotoColorEmoji"
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
mod client;
|
||||
mod clipboard;
|
||||
mod cursor;
|
||||
mod display;
|
||||
mod serial;
|
||||
mod window;
|
||||
|
||||
pub(crate) use client::*;
|
||||
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||
|
||||
use crate::CursorStyle;
|
||||
|
||||
impl CursorStyle {
|
||||
pub(super) fn to_shape(self) -> Shape {
|
||||
match self {
|
||||
CursorStyle::Arrow => Shape::Default,
|
||||
CursorStyle::IBeam => Shape::Text,
|
||||
CursorStyle::Crosshair => Shape::Crosshair,
|
||||
CursorStyle::ClosedHand => Shape::Grabbing,
|
||||
CursorStyle::OpenHand => Shape::Grab,
|
||||
CursorStyle::PointingHand => Shape::Pointer,
|
||||
CursorStyle::ResizeLeft => Shape::WResize,
|
||||
CursorStyle::ResizeRight => Shape::EResize,
|
||||
CursorStyle::ResizeLeftRight => Shape::EwResize,
|
||||
CursorStyle::ResizeUp => Shape::NResize,
|
||||
CursorStyle::ResizeDown => Shape::SResize,
|
||||
CursorStyle::ResizeUpDown => Shape::NsResize,
|
||||
CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
|
||||
CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
|
||||
CursorStyle::ResizeColumn => Shape::ColResize,
|
||||
CursorStyle::ResizeRow => Shape::RowResize,
|
||||
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
|
||||
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
|
||||
CursorStyle::DragLink => Shape::Alias,
|
||||
CursorStyle::DragCopy => Shape::Copy,
|
||||
CursorStyle::ContextualMenu => Shape::ContextMenu,
|
||||
CursorStyle::None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("CursorStyle::None should be handled separately in the client");
|
||||
#[cfg(not(debug_assertions))]
|
||||
Shape::Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2159
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,262 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{ErrorKind, Write},
|
||||
os::fd::{AsRawFd, BorrowedFd, OwnedFd},
|
||||
};
|
||||
|
||||
use calloop::{LoopHandle, PostAction};
|
||||
use filedescriptor::Pipe;
|
||||
use strum::IntoEnumIterator;
|
||||
use wayland_client::{Connection, protocol::wl_data_offer::WlDataOffer};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
|
||||
|
||||
use crate::{
|
||||
ClipboardEntry, ClipboardItem, Image, ImageFormat, WaylandClientStatePtr, hash,
|
||||
platform::linux::platform::read_fd,
|
||||
};
|
||||
|
||||
/// Text mime types that we'll offer to other programs.
|
||||
pub(crate) const TEXT_MIME_TYPES: [&str; 3] =
|
||||
["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"];
|
||||
pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
|
||||
|
||||
/// Text mime types that we'll accept from other programs.
|
||||
pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"];
|
||||
|
||||
pub(crate) struct Clipboard {
|
||||
connection: Connection,
|
||||
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
|
||||
self_mime: String,
|
||||
|
||||
// Internal clipboard
|
||||
contents: Option<ClipboardItem>,
|
||||
primary_contents: Option<ClipboardItem>,
|
||||
|
||||
// External clipboard
|
||||
cached_read: Option<ClipboardItem>,
|
||||
current_offer: Option<DataOffer<WlDataOffer>>,
|
||||
cached_primary_read: Option<ClipboardItem>,
|
||||
current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
}
|
||||
|
||||
pub(crate) trait ReceiveData {
|
||||
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>);
|
||||
}
|
||||
|
||||
impl ReceiveData for WlDataOffer {
|
||||
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
|
||||
self.receive(mime_type, fd);
|
||||
}
|
||||
}
|
||||
|
||||
impl ReceiveData for ZwpPrimarySelectionOfferV1 {
|
||||
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
|
||||
self.receive(mime_type, fd);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types.
|
||||
pub(crate) struct DataOffer<T: ReceiveData> {
|
||||
pub inner: T,
|
||||
mime_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl<T: ReceiveData> DataOffer<T> {
|
||||
pub fn new(offer: T) -> Self {
|
||||
Self {
|
||||
inner: offer,
|
||||
mime_types: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_mime_type(&mut self, mime_type: String) {
|
||||
self.mime_types.push(mime_type)
|
||||
}
|
||||
|
||||
fn has_mime_type(&self, mime_type: &str) -> bool {
|
||||
self.mime_types.iter().any(|t| t == mime_type)
|
||||
}
|
||||
|
||||
fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option<Vec<u8>> {
|
||||
let pipe = Pipe::new().unwrap();
|
||||
self.inner.receive_data(mime_type.to_string(), unsafe {
|
||||
BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
|
||||
});
|
||||
let fd = pipe.read;
|
||||
drop(pipe.write);
|
||||
|
||||
connection.flush().unwrap();
|
||||
|
||||
match unsafe { read_fd(fd) } {
|
||||
Ok(bytes) => Some(bytes),
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_text(&self, connection: &Connection) -> Option<ClipboardItem> {
|
||||
let mime_type = self.mime_types.iter().find(|&mime_type| {
|
||||
ALLOWED_TEXT_MIME_TYPES
|
||||
.iter()
|
||||
.any(|&allowed| allowed == mime_type)
|
||||
})?;
|
||||
let bytes = self.read_bytes(connection, mime_type)?;
|
||||
let text_content = match String::from_utf8(bytes) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
log::error!("Failed to convert clipboard content to UTF-8: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize the text to unix line endings, otherwise
|
||||
// copying from eg: firefox inserts a lot of blank
|
||||
// lines, and that is super annoying.
|
||||
let result = text_content.replace("\r\n", "\n");
|
||||
Some(ClipboardItem::new_string(result))
|
||||
}
|
||||
|
||||
fn read_image(&self, connection: &Connection) -> Option<ClipboardItem> {
|
||||
for format in ImageFormat::iter() {
|
||||
let mime_type = format.mime_type();
|
||||
if !self.has_mime_type(mime_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(bytes) = self.read_bytes(connection, mime_type) {
|
||||
let id = hash(&bytes);
|
||||
return Some(ClipboardItem {
|
||||
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
pub fn new(
|
||||
connection: Connection,
|
||||
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
|
||||
) -> Self {
|
||||
Self {
|
||||
connection,
|
||||
loop_handle,
|
||||
self_mime: format!("pid/{}", std::process::id()),
|
||||
|
||||
contents: None,
|
||||
primary_contents: None,
|
||||
|
||||
cached_read: None,
|
||||
current_offer: None,
|
||||
cached_primary_read: None,
|
||||
current_primary_offer: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, item: ClipboardItem) {
|
||||
self.contents = Some(item);
|
||||
}
|
||||
|
||||
pub fn set_primary(&mut self, item: ClipboardItem) {
|
||||
self.primary_contents = Some(item);
|
||||
}
|
||||
|
||||
pub fn set_offer(&mut self, data_offer: Option<DataOffer<WlDataOffer>>) {
|
||||
self.cached_read = None;
|
||||
self.current_offer = data_offer;
|
||||
}
|
||||
|
||||
pub fn set_primary_offer(&mut self, data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>) {
|
||||
self.cached_primary_read = None;
|
||||
self.current_primary_offer = data_offer;
|
||||
}
|
||||
|
||||
pub fn self_mime(&self) -> String {
|
||||
self.self_mime.clone()
|
||||
}
|
||||
|
||||
pub fn send(&self, _mime_type: String, fd: OwnedFd) {
|
||||
if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) {
|
||||
self.send_internal(fd, text.as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) {
|
||||
if let Some(text) = self
|
||||
.primary_contents
|
||||
.as_ref()
|
||||
.and_then(|contents| contents.text())
|
||||
{
|
||||
self.send_internal(fd, text.as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Option<ClipboardItem> {
|
||||
let offer = self.current_offer.as_ref()?;
|
||||
if let Some(cached) = self.cached_read.clone() {
|
||||
return Some(cached);
|
||||
}
|
||||
|
||||
if offer.has_mime_type(&self.self_mime) {
|
||||
return self.contents.clone();
|
||||
}
|
||||
|
||||
let item = offer
|
||||
.read_text(&self.connection)
|
||||
.or_else(|| offer.read_image(&self.connection))?;
|
||||
|
||||
self.cached_read = Some(item.clone());
|
||||
Some(item)
|
||||
}
|
||||
|
||||
pub fn read_primary(&mut self) -> Option<ClipboardItem> {
|
||||
let offer = self.current_primary_offer.as_ref()?;
|
||||
if let Some(cached) = self.cached_primary_read.clone() {
|
||||
return Some(cached);
|
||||
}
|
||||
|
||||
if offer.has_mime_type(&self.self_mime) {
|
||||
return self.primary_contents.clone();
|
||||
}
|
||||
|
||||
let item = offer
|
||||
.read_text(&self.connection)
|
||||
.or_else(|| offer.read_image(&self.connection))?;
|
||||
|
||||
self.cached_primary_read = Some(item.clone());
|
||||
Some(item)
|
||||
}
|
||||
|
||||
fn send_internal(&self, fd: OwnedFd, bytes: Vec<u8>) {
|
||||
let mut written = 0;
|
||||
self.loop_handle
|
||||
.insert_source(
|
||||
calloop::generic::Generic::new(
|
||||
File::from(fd),
|
||||
calloop::Interest::WRITE,
|
||||
calloop::Mode::Level,
|
||||
),
|
||||
move |_, file, _| {
|
||||
let mut file = unsafe { file.get_mut() };
|
||||
loop {
|
||||
match file.write(&bytes[written..]) {
|
||||
Ok(n) if written + n == bytes.len() => {
|
||||
written += n;
|
||||
break Ok(PostAction::Remove);
|
||||
}
|
||||
Ok(n) => written += n,
|
||||
Err(err) if err.kind() == ErrorKind::WouldBlock => {
|
||||
break Ok(PostAction::Continue);
|
||||
}
|
||||
Err(_) => break Ok(PostAction::Remove),
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
use crate::Globals;
|
||||
use crate::platform::linux::{DEFAULT_CURSOR_ICON_NAME, log_cursor_icon_warning};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use util::ResultExt;
|
||||
|
||||
use wayland_client::Connection;
|
||||
use wayland_client::protocol::wl_surface::WlSurface;
|
||||
use wayland_client::protocol::{wl_pointer::WlPointer, wl_shm::WlShm};
|
||||
use wayland_cursor::{CursorImageBuffer, CursorTheme};
|
||||
|
||||
pub(crate) struct Cursor {
|
||||
loaded_theme: Option<LoadedTheme>,
|
||||
size: u32,
|
||||
scaled_size: u32,
|
||||
surface: WlSurface,
|
||||
shm: WlShm,
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
pub(crate) struct LoadedTheme {
|
||||
theme: CursorTheme,
|
||||
name: Option<String>,
|
||||
scaled_size: u32,
|
||||
}
|
||||
|
||||
impl Drop for Cursor {
|
||||
fn drop(&mut self) {
|
||||
self.loaded_theme.take();
|
||||
self.surface.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
|
||||
let mut this = Self {
|
||||
loaded_theme: None,
|
||||
size,
|
||||
scaled_size: size,
|
||||
surface: globals.compositor.create_surface(&globals.qh, ()),
|
||||
shm: globals.shm.clone(),
|
||||
connection: connection.clone(),
|
||||
};
|
||||
this.set_theme_internal(None);
|
||||
this
|
||||
}
|
||||
|
||||
fn set_theme_internal(&mut self, theme_name: Option<String>) {
|
||||
if let Some(loaded_theme) = self.loaded_theme.as_ref()
|
||||
&& loaded_theme.name == theme_name
|
||||
&& loaded_theme.scaled_size == self.scaled_size
|
||||
{
|
||||
return;
|
||||
}
|
||||
let result = if let Some(theme_name) = theme_name.as_ref() {
|
||||
CursorTheme::load_from_name(
|
||||
&self.connection,
|
||||
self.shm.clone(),
|
||||
theme_name,
|
||||
self.scaled_size,
|
||||
)
|
||||
} else {
|
||||
CursorTheme::load(&self.connection, self.shm.clone(), self.scaled_size)
|
||||
};
|
||||
if let Some(theme) = result
|
||||
.context("Wayland: Failed to load cursor theme")
|
||||
.log_err()
|
||||
{
|
||||
self.loaded_theme = Some(LoadedTheme {
|
||||
theme,
|
||||
name: theme_name,
|
||||
scaled_size: self.scaled_size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_theme(&mut self, theme_name: String) {
|
||||
self.set_theme_internal(Some(theme_name));
|
||||
}
|
||||
|
||||
fn set_scaled_size(&mut self, scaled_size: u32) {
|
||||
self.scaled_size = scaled_size;
|
||||
let theme_name = self
|
||||
.loaded_theme
|
||||
.as_ref()
|
||||
.and_then(|loaded_theme| loaded_theme.name.clone());
|
||||
self.set_theme_internal(theme_name);
|
||||
}
|
||||
|
||||
pub fn set_size(&mut self, size: u32) {
|
||||
self.size = size;
|
||||
self.set_scaled_size(size);
|
||||
}
|
||||
|
||||
pub fn set_icon(
|
||||
&mut self,
|
||||
wl_pointer: &WlPointer,
|
||||
serial_id: u32,
|
||||
mut cursor_icon_names: &[&str],
|
||||
scale: i32,
|
||||
) {
|
||||
self.set_scaled_size(self.size * scale as u32);
|
||||
|
||||
let Some(loaded_theme) = &mut self.loaded_theme else {
|
||||
log::warn!("Wayland: Unable to load cursor themes");
|
||||
return;
|
||||
};
|
||||
let mut theme = &mut loaded_theme.theme;
|
||||
|
||||
let mut buffer: &CursorImageBuffer;
|
||||
'outer: {
|
||||
for cursor_icon_name in cursor_icon_names {
|
||||
if let Some(cursor) = theme.get_cursor(cursor_icon_name) {
|
||||
buffer = &cursor[0];
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cursor) = theme.get_cursor(DEFAULT_CURSOR_ICON_NAME) {
|
||||
buffer = &cursor[0];
|
||||
log_cursor_icon_warning(anyhow!(
|
||||
"wayland: Unable to get cursor icon {:?}. \
|
||||
Using default cursor icon: '{}'",
|
||||
cursor_icon_names,
|
||||
DEFAULT_CURSOR_ICON_NAME
|
||||
));
|
||||
} else {
|
||||
log_cursor_icon_warning(anyhow!(
|
||||
"wayland: Unable to fallback on default cursor icon '{}' for theme '{}'",
|
||||
DEFAULT_CURSOR_ICON_NAME,
|
||||
loaded_theme.name.as_deref().unwrap_or("default")
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let (width, height) = buffer.dimensions();
|
||||
let (hot_x, hot_y) = buffer.hotspot();
|
||||
|
||||
self.surface.set_buffer_scale(scale);
|
||||
|
||||
wl_pointer.set_cursor(
|
||||
serial_id,
|
||||
Some(&self.surface),
|
||||
hot_x as i32 / scale,
|
||||
hot_y as i32 / scale,
|
||||
);
|
||||
|
||||
self.surface.attach(Some(buffer), 0, 0);
|
||||
self.surface.damage(0, 0, width as i32, height as i32);
|
||||
self.surface.commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use uuid::Uuid;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
||||
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct WaylandDisplay {
|
||||
/// The ID of the wl_output object
|
||||
pub id: ObjectId,
|
||||
pub name: Option<String>,
|
||||
pub bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
impl Hash for WaylandDisplay {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDisplay for WaylandDisplay {
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId(self.id.protocol_id())
|
||||
}
|
||||
|
||||
fn uuid(&self) -> anyhow::Result<Uuid> {
|
||||
let name = self
|
||||
.name
|
||||
.as_ref()
|
||||
.context("Wayland display does not have a name")?;
|
||||
Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes()))
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use collections::HashMap;
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum SerialKind {
|
||||
DataDevice,
|
||||
InputMethod,
|
||||
MouseEnter,
|
||||
MousePress,
|
||||
KeyPress,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SerialData {
|
||||
serial: u32,
|
||||
}
|
||||
|
||||
impl SerialData {
|
||||
fn new(value: u32) -> Self {
|
||||
Self { serial: value }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper for tracking of different serial kinds.
|
||||
pub(crate) struct SerialTracker {
|
||||
serials: HashMap<SerialKind, SerialData>,
|
||||
}
|
||||
|
||||
impl SerialTracker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
serials: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, kind: SerialKind, value: u32) {
|
||||
self.serials.insert(kind, SerialData::new(value));
|
||||
}
|
||||
|
||||
/// Returns the latest tracked serial of the provided [`SerialKind`]
|
||||
///
|
||||
/// Will return 0 if not tracked.
|
||||
pub fn get(&self, kind: SerialKind) -> u32 {
|
||||
self.serials
|
||||
.get(&kind)
|
||||
.map(|serial_data| serial_data.serial)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
+1219
File diff suppressed because it is too large
Load Diff
+12
@@ -0,0 +1,12 @@
|
||||
mod client;
|
||||
mod clipboard;
|
||||
mod display;
|
||||
mod event;
|
||||
mod window;
|
||||
mod xim_handler;
|
||||
|
||||
pub(crate) use client::*;
|
||||
pub(crate) use display::*;
|
||||
pub(crate) use event::*;
|
||||
pub(crate) use window::*;
|
||||
pub(crate) use xim_handler::*;
|
||||
+2491
File diff suppressed because it is too large
Load Diff
+1270
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
use anyhow::Context as _;
|
||||
use uuid::Uuid;
|
||||
use x11rb::{connection::Connection as _, xcb_ffi::XCBConnection};
|
||||
|
||||
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, Size, px};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct X11Display {
|
||||
x_screen_index: usize,
|
||||
bounds: Bounds<Pixels>,
|
||||
uuid: Uuid,
|
||||
}
|
||||
|
||||
impl X11Display {
|
||||
pub(crate) fn new(
|
||||
xcb: &XCBConnection,
|
||||
scale_factor: f32,
|
||||
x_screen_index: usize,
|
||||
) -> anyhow::Result<Self> {
|
||||
let screen = xcb
|
||||
.setup()
|
||||
.roots
|
||||
.get(x_screen_index)
|
||||
.with_context(|| format!("No screen found with index {x_screen_index}"))?;
|
||||
Ok(Self {
|
||||
x_screen_index,
|
||||
bounds: Bounds {
|
||||
origin: Default::default(),
|
||||
size: Size {
|
||||
width: px(screen.width_in_pixels as f32 / scale_factor),
|
||||
height: px(screen.height_in_pixels as f32 / scale_factor),
|
||||
},
|
||||
},
|
||||
uuid: Uuid::from_bytes([0; 16]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDisplay for X11Display {
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId(self.x_screen_index as u32)
|
||||
}
|
||||
|
||||
fn uuid(&self) -> anyhow::Result<Uuid> {
|
||||
Ok(self.uuid)
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
use x11rb::protocol::{
|
||||
xinput,
|
||||
xproto::{self, ModMask},
|
||||
};
|
||||
|
||||
use crate::{Modifiers, MouseButton, NavigationDirection};
|
||||
|
||||
pub(crate) enum ButtonOrScroll {
|
||||
Button(MouseButton),
|
||||
Scroll(ScrollDirection),
|
||||
}
|
||||
|
||||
pub(crate) enum ScrollDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub(crate) fn button_or_scroll_from_event_detail(detail: u32) -> Option<ButtonOrScroll> {
|
||||
Some(match detail {
|
||||
1 => ButtonOrScroll::Button(MouseButton::Left),
|
||||
2 => ButtonOrScroll::Button(MouseButton::Middle),
|
||||
3 => ButtonOrScroll::Button(MouseButton::Right),
|
||||
4 => ButtonOrScroll::Scroll(ScrollDirection::Up),
|
||||
5 => ButtonOrScroll::Scroll(ScrollDirection::Down),
|
||||
6 => ButtonOrScroll::Scroll(ScrollDirection::Left),
|
||||
7 => ButtonOrScroll::Scroll(ScrollDirection::Right),
|
||||
8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)),
|
||||
9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers {
|
||||
Modifiers {
|
||||
control: state.contains(xproto::KeyButMask::CONTROL),
|
||||
alt: state.contains(xproto::KeyButMask::MOD1),
|
||||
shift: state.contains(xproto::KeyButMask::SHIFT),
|
||||
platform: state.contains(xproto::KeyButMask::MOD4),
|
||||
function: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers {
|
||||
Modifiers {
|
||||
control: modifier_info.effective as u16 & ModMask::CONTROL.bits()
|
||||
== ModMask::CONTROL.bits(),
|
||||
alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(),
|
||||
shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(),
|
||||
platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(),
|
||||
function: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option<MouseButton> {
|
||||
Some(if button_mask & 2 == 2 {
|
||||
MouseButton::Left
|
||||
} else if button_mask & 4 == 4 {
|
||||
MouseButton::Middle
|
||||
} else if button_mask & 8 == 8 {
|
||||
MouseButton::Right
|
||||
} else {
|
||||
return None;
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get_valuator_axis_index(
|
||||
valuator_mask: &Vec<u32>,
|
||||
valuator_number: u16,
|
||||
) -> Option<usize> {
|
||||
// XInput valuator masks have a 1 at the bit indexes corresponding to each
|
||||
// valuator present in this event's axisvalues. Axisvalues is ordered from
|
||||
// lowest valuator number to highest, so counting bits before the 1 bit for
|
||||
// this valuator yields the index in axisvalues.
|
||||
if bit_is_set_in_vec(valuator_mask, valuator_number) {
|
||||
Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of 1 bits in `bit_vec` for all bits where `i < bit_index`.
|
||||
fn popcount_upto_bit_index(bit_vec: &Vec<u32>, bit_index: u16) -> u32 {
|
||||
let array_index = bit_index as usize / 32;
|
||||
let popcount: u32 = bit_vec
|
||||
.get(array_index)
|
||||
.map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones());
|
||||
if array_index == 0 {
|
||||
popcount
|
||||
} else {
|
||||
// Valuator numbers over 32 probably never occur for scroll position, but may as well
|
||||
// support it.
|
||||
let leading_popcount: u32 = bit_vec
|
||||
.iter()
|
||||
.take(array_index)
|
||||
.map(|bits| bits.count_ones())
|
||||
.sum();
|
||||
popcount + leading_popcount
|
||||
}
|
||||
}
|
||||
|
||||
fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool {
|
||||
let array_index = bit_index as usize / 32;
|
||||
bit_vec
|
||||
.get(array_index)
|
||||
.is_some_and(|bits| bit_is_set(*bits, bit_index % 32))
|
||||
}
|
||||
|
||||
fn bit_is_set(bits: u32, bit_index: u16) -> bool {
|
||||
bits & (1 << bit_index) != 0
|
||||
}
|
||||
|
||||
/// Sets every bit with `i >= bit_index` to 0.
|
||||
fn keep_bits_upto(bits: u32, bit_index: u16) -> u32 {
|
||||
if bit_index == 0 {
|
||||
0
|
||||
} else if bit_index >= 32 {
|
||||
u32::MAX
|
||||
} else {
|
||||
bits & ((1 << bit_index) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_valuator_axis_index() {
|
||||
assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0));
|
||||
assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1));
|
||||
assert!(get_valuator_axis_index(&vec![0b11], 2) == None);
|
||||
|
||||
assert!(get_valuator_axis_index(&vec![0b100], 0) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b100], 1) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0));
|
||||
assert!(get_valuator_axis_index(&vec![0b100], 3) == None);
|
||||
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0));
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1));
|
||||
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0));
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1));
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2));
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None);
|
||||
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3));
|
||||
}
|
||||
}
|
||||
+1670
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
||||
use std::default::Default;
|
||||
|
||||
use x11rb::protocol::{Event, xproto};
|
||||
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle};
|
||||
|
||||
pub enum XimCallbackEvent {
|
||||
XimXEvent(x11rb::protocol::Event),
|
||||
XimPreeditEvent(xproto::Window, String),
|
||||
XimCommitEvent(xproto::Window, String),
|
||||
}
|
||||
|
||||
pub struct XimHandler {
|
||||
pub im_id: u16,
|
||||
pub ic_id: u16,
|
||||
pub connected: bool,
|
||||
pub window: xproto::Window,
|
||||
pub last_callback_event: Option<XimCallbackEvent>,
|
||||
}
|
||||
|
||||
impl XimHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
im_id: Default::default(),
|
||||
ic_id: Default::default(),
|
||||
connected: false,
|
||||
window: Default::default(),
|
||||
last_callback_event: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler {
|
||||
fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> {
|
||||
client.open("C")
|
||||
}
|
||||
|
||||
fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> {
|
||||
self.im_id = input_method_id;
|
||||
|
||||
client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle])
|
||||
}
|
||||
|
||||
fn handle_get_im_values(
|
||||
&mut self,
|
||||
client: &mut C,
|
||||
input_method_id: u16,
|
||||
_attributes: AHashMap<AttributeName, Vec<u8>>,
|
||||
) -> Result<(), ClientError> {
|
||||
let ic_attributes = client
|
||||
.build_ic_attributes()
|
||||
.push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS)
|
||||
.push(AttributeName::ClientWindow, self.window)
|
||||
.push(AttributeName::FocusWindow, self.window)
|
||||
.build();
|
||||
client.create_ic(input_method_id, ic_attributes)
|
||||
}
|
||||
|
||||
fn handle_create_ic(
|
||||
&mut self,
|
||||
_client: &mut C,
|
||||
_input_method_id: u16,
|
||||
input_context_id: u16,
|
||||
) -> Result<(), ClientError> {
|
||||
self.connected = true;
|
||||
self.ic_id = input_context_id;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_commit(
|
||||
&mut self,
|
||||
_client: &mut C,
|
||||
_input_method_id: u16,
|
||||
_input_context_id: u16,
|
||||
text: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
self.last_callback_event = Some(XimCallbackEvent::XimCommitEvent(
|
||||
self.window,
|
||||
String::from(text),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_forward_event(
|
||||
&mut self,
|
||||
_client: &mut C,
|
||||
_input_method_id: u16,
|
||||
_input_context_id: u16,
|
||||
_flag: xim::ForwardEventFlag,
|
||||
xev: C::XEvent,
|
||||
) -> Result<(), ClientError> {
|
||||
match xev.response_type {
|
||||
x11rb::protocol::xproto::KEY_PRESS_EVENT => {
|
||||
self.last_callback_event = Some(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
|
||||
}
|
||||
x11rb::protocol::xproto::KEY_RELEASE_EVENT => {
|
||||
self.last_callback_event =
|
||||
Some(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> {
|
||||
client.disconnect()
|
||||
}
|
||||
|
||||
fn handle_preedit_draw(
|
||||
&mut self,
|
||||
_client: &mut C,
|
||||
_input_method_id: u16,
|
||||
_input_context_id: u16,
|
||||
_caret: i32,
|
||||
_chg_first: i32,
|
||||
_chg_len: i32,
|
||||
_status: xim::PreeditDrawStatus,
|
||||
preedit_string: &str,
|
||||
_feedbacks: Vec<xim::Feedback>,
|
||||
) -> Result<(), ClientError> {
|
||||
// XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text
|
||||
// XIMUnderline: 2, XIMSecondary: 16: underlined text
|
||||
// XIMHighlight: 4: normal text
|
||||
// XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position
|
||||
// XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified,
|
||||
// but interchangeable as above
|
||||
// Currently there's no way to support these.
|
||||
self.last_callback_event = Some(XimCallbackEvent::XimPreeditEvent(
|
||||
self.window,
|
||||
String::from(preedit_string),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//! Provides a [calloop] event source from [XDG Desktop Portal] events
|
||||
//!
|
||||
//! This module uses the [ashpd] crate
|
||||
|
||||
use ashpd::desktop::settings::{ColorScheme, Settings};
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use crate::{BackgroundExecutor, WindowAppearance};
|
||||
|
||||
pub enum Event {
|
||||
WindowAppearance(WindowAppearance),
|
||||
#[cfg_attr(feature = "x11", allow(dead_code))]
|
||||
CursorTheme(String),
|
||||
#[cfg_attr(feature = "x11", allow(dead_code))]
|
||||
CursorSize(u32),
|
||||
}
|
||||
|
||||
pub struct XDPEventSource {
|
||||
channel: Channel<Event>,
|
||||
}
|
||||
|
||||
impl XDPEventSource {
|
||||
pub fn new(executor: &BackgroundExecutor) -> Self {
|
||||
let (sender, channel) = calloop::channel::channel();
|
||||
|
||||
let background = executor.clone();
|
||||
|
||||
executor
|
||||
.spawn(async move {
|
||||
let settings = Settings::new().await?;
|
||||
|
||||
if let Ok(initial_appearance) = settings.color_scheme().await {
|
||||
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
|
||||
initial_appearance,
|
||||
)))?;
|
||||
}
|
||||
if let Ok(initial_theme) = settings
|
||||
.read::<String>("org.gnome.desktop.interface", "cursor-theme")
|
||||
.await
|
||||
{
|
||||
sender.send(Event::CursorTheme(initial_theme))?;
|
||||
}
|
||||
|
||||
// If u32 is used here, it throws invalid type error
|
||||
if let Ok(initial_size) = settings
|
||||
.read::<i32>("org.gnome.desktop.interface", "cursor-size")
|
||||
.await
|
||||
{
|
||||
sender.send(Event::CursorSize(initial_size as u32))?;
|
||||
}
|
||||
|
||||
if let Ok(mut cursor_theme_changed) = settings
|
||||
.receive_setting_changed_with_args(
|
||||
"org.gnome.desktop.interface",
|
||||
"cursor-theme",
|
||||
)
|
||||
.await
|
||||
{
|
||||
let sender = sender.clone();
|
||||
background
|
||||
.spawn(async move {
|
||||
while let Some(theme) = cursor_theme_changed.next().await {
|
||||
let theme = theme?;
|
||||
sender.send(Event::CursorTheme(theme))?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
if let Ok(mut cursor_size_changed) = settings
|
||||
.receive_setting_changed_with_args::<i32>(
|
||||
"org.gnome.desktop.interface",
|
||||
"cursor-size",
|
||||
)
|
||||
.await
|
||||
{
|
||||
let sender = sender.clone();
|
||||
background
|
||||
.spawn(async move {
|
||||
while let Some(size) = cursor_size_changed.next().await {
|
||||
let size = size?;
|
||||
sender.send(Event::CursorSize(size as u32))?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
let mut appearance_changed = settings.receive_color_scheme_changed().await?;
|
||||
while let Some(scheme) = appearance_changed.next().await {
|
||||
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
|
||||
scheme,
|
||||
)))?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self { channel }
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for XDPEventSource {
|
||||
type Event = Event;
|
||||
type Metadata = ();
|
||||
type Ret = ();
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn process_events<F>(
|
||||
&mut self,
|
||||
readiness: Readiness,
|
||||
token: Token,
|
||||
mut callback: F,
|
||||
) -> Result<PostAction, Self::Error>
|
||||
where
|
||||
F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret,
|
||||
{
|
||||
self.channel.process_events(readiness, token, |evt, _| {
|
||||
if let calloop::channel::Event::Msg(msg) = evt {
|
||||
(callback)(msg, &mut ())
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(PostAction::Continue)
|
||||
}
|
||||
|
||||
fn register(
|
||||
&mut self,
|
||||
poll: &mut Poll,
|
||||
token_factory: &mut TokenFactory,
|
||||
) -> calloop::Result<()> {
|
||||
self.channel.register(poll, token_factory)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reregister(
|
||||
&mut self,
|
||||
poll: &mut Poll,
|
||||
token_factory: &mut TokenFactory,
|
||||
) -> calloop::Result<()> {
|
||||
self.channel.reregister(poll, token_factory)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> {
|
||||
self.channel.unregister(poll)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowAppearance {
|
||||
fn from_native(cs: ColorScheme) -> WindowAppearance {
|
||||
match cs {
|
||||
ColorScheme::PreferDark => WindowAppearance::Dark,
|
||||
ColorScheme::PreferLight => WindowAppearance::Light,
|
||||
ColorScheme::NoPreference => WindowAppearance::Light,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
|
||||
fn set_native(&mut self, cs: ColorScheme) {
|
||||
*self = Self::from_native(cs);
|
||||
}
|
||||
}
|
||||
Vendored
+163
@@ -0,0 +1,163 @@
|
||||
//! Macos screen have a y axis that goings up from the bottom of the screen and
|
||||
//! an origin at the bottom left of the main display.
|
||||
mod dispatcher;
|
||||
mod display;
|
||||
mod display_link;
|
||||
mod events;
|
||||
mod keyboard;
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
mod screen_capture;
|
||||
|
||||
#[cfg(not(feature = "macos-blade"))]
|
||||
mod metal_atlas;
|
||||
#[cfg(not(feature = "macos-blade"))]
|
||||
pub mod metal_renderer;
|
||||
|
||||
use core_video::image_buffer::CVImageBuffer;
|
||||
#[cfg(not(feature = "macos-blade"))]
|
||||
use metal_renderer as renderer;
|
||||
|
||||
#[cfg(feature = "macos-blade")]
|
||||
use crate::platform::blade as renderer;
|
||||
|
||||
mod attributed_string;
|
||||
|
||||
#[cfg(feature = "font-kit")]
|
||||
mod open_type;
|
||||
|
||||
#[cfg(feature = "font-kit")]
|
||||
mod text_system;
|
||||
|
||||
mod platform;
|
||||
mod window;
|
||||
mod window_appearance;
|
||||
|
||||
use crate::{DevicePixels, Pixels, Size, px, size};
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::{NSAutoreleasePool, NSNotFound, NSRect, NSSize, NSString, NSUInteger},
|
||||
};
|
||||
|
||||
use objc::runtime::{BOOL, NO, YES};
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
pub(crate) use dispatcher::*;
|
||||
pub(crate) use display::*;
|
||||
pub(crate) use display_link::*;
|
||||
pub(crate) use keyboard::*;
|
||||
pub(crate) use platform::*;
|
||||
pub(crate) use window::*;
|
||||
|
||||
#[cfg(feature = "font-kit")]
|
||||
pub(crate) use text_system::*;
|
||||
|
||||
/// A frame of video captured from a screen.
|
||||
pub(crate) type PlatformScreenCaptureFrame = CVImageBuffer;
|
||||
|
||||
trait BoolExt {
|
||||
fn to_objc(self) -> BOOL;
|
||||
}
|
||||
|
||||
impl BoolExt for bool {
|
||||
fn to_objc(self) -> BOOL {
|
||||
if self { YES } else { NO }
|
||||
}
|
||||
}
|
||||
|
||||
trait NSStringExt {
|
||||
unsafe fn to_str(&self) -> &str;
|
||||
}
|
||||
|
||||
impl NSStringExt for id {
|
||||
unsafe fn to_str(&self) -> &str {
|
||||
unsafe {
|
||||
let cstr = self.UTF8String();
|
||||
if cstr.is_null() {
|
||||
""
|
||||
} else {
|
||||
CStr::from_ptr(cstr as *mut c_char).to_str().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct NSRange {
|
||||
pub location: NSUInteger,
|
||||
pub length: NSUInteger,
|
||||
}
|
||||
|
||||
impl NSRange {
|
||||
fn invalid() -> Self {
|
||||
Self {
|
||||
location: NSNotFound as NSUInteger,
|
||||
length: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
self.location != NSNotFound as NSUInteger
|
||||
}
|
||||
|
||||
fn to_range(self) -> Option<Range<usize>> {
|
||||
if self.is_valid() {
|
||||
let start = self.location as usize;
|
||||
let end = start + self.length as usize;
|
||||
Some(start..end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Range<usize>> for NSRange {
|
||||
fn from(range: Range<usize>) -> Self {
|
||||
NSRange {
|
||||
location: range.start as NSUInteger,
|
||||
length: range.len() as NSUInteger,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl objc::Encode for NSRange {
|
||||
fn encode() -> objc::Encoding {
|
||||
let encoding = format!(
|
||||
"{{NSRange={}{}}}",
|
||||
NSUInteger::encode().as_str(),
|
||||
NSUInteger::encode().as_str()
|
||||
);
|
||||
unsafe { objc::Encoding::from_str(&encoding) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn ns_string(string: &str) -> id {
|
||||
unsafe { NSString::alloc(nil).init_str(string).autorelease() }
|
||||
}
|
||||
|
||||
impl From<NSSize> for Size<Pixels> {
|
||||
fn from(value: NSSize) -> Self {
|
||||
Size {
|
||||
width: px(value.width as f32),
|
||||
height: px(value.height as f32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NSRect> for Size<Pixels> {
|
||||
fn from(rect: NSRect) -> Self {
|
||||
let NSSize { width, height } = rect.size;
|
||||
size(width.into(), height.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NSRect> for Size<DevicePixels> {
|
||||
fn from(rect: NSRect) -> Self {
|
||||
let NSSize { width, height } = rect.size;
|
||||
size(DevicePixels(width as i32), DevicePixels(height as i32))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use cocoa::base::id;
|
||||
use cocoa::foundation::NSRange;
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
|
||||
/// The `cocoa` crate does not define NSAttributedString (and related Cocoa classes),
|
||||
/// which are needed for copying rich text (that is, text intermingled with images)
|
||||
/// to the clipboard. This adds access to those APIs.
|
||||
#[allow(non_snake_case)]
|
||||
pub trait NSAttributedString: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSAttributedString), alloc]
|
||||
}
|
||||
|
||||
unsafe fn init_attributed_string(self, string: id) -> id;
|
||||
unsafe fn appendAttributedString_(self, attr_string: id);
|
||||
unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
|
||||
unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
|
||||
unsafe fn string(self) -> id;
|
||||
}
|
||||
|
||||
impl NSAttributedString for id {
|
||||
unsafe fn init_attributed_string(self, string: id) -> id {
|
||||
msg_send![self, initWithString: string]
|
||||
}
|
||||
|
||||
unsafe fn appendAttributedString_(self, attr_string: id) {
|
||||
let _: () = msg_send![self, appendAttributedString: attr_string];
|
||||
}
|
||||
|
||||
unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
|
||||
msg_send![self, RTFDFromRange: range documentAttributes: attrs]
|
||||
}
|
||||
|
||||
unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
|
||||
msg_send![self, RTFFromRange: range documentAttributes: attrs]
|
||||
}
|
||||
|
||||
unsafe fn string(self) -> id {
|
||||
msg_send![self, string]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NSMutableAttributedString: NSAttributedString {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSMutableAttributedString), alloc]
|
||||
}
|
||||
}
|
||||
|
||||
impl NSMutableAttributedString for id {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cocoa::appkit::NSImage;
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSString;
|
||||
#[test]
|
||||
#[ignore] // This was SIGSEGV-ing on CI but not locally; need to investigate https://github.com/zed-industries/zed/actions/runs/10362363230/job/28684225486?pr=15782#step:4:1348
|
||||
fn test_nsattributed_string() {
|
||||
// TODO move these to parent module once it's actually ready to be used
|
||||
#[allow(non_snake_case)]
|
||||
pub trait NSTextAttachment: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSTextAttachment), alloc]
|
||||
}
|
||||
}
|
||||
|
||||
impl NSTextAttachment for id {}
|
||||
|
||||
unsafe {
|
||||
let image: id = msg_send![class!(NSImage), alloc];
|
||||
image.initWithContentsOfFile_(NSString::alloc(nil).init_str("test.jpeg"));
|
||||
let _size = image.size();
|
||||
|
||||
let string = NSString::alloc(nil).init_str("Test String");
|
||||
let attr_string = NSMutableAttributedString::alloc(nil).init_attributed_string(string);
|
||||
let hello_string = NSString::alloc(nil).init_str("Hello World");
|
||||
let hello_attr_string =
|
||||
NSAttributedString::alloc(nil).init_attributed_string(hello_string);
|
||||
attr_string.appendAttributedString_(hello_attr_string);
|
||||
|
||||
let attachment = NSTextAttachment::alloc(nil);
|
||||
let _: () = msg_send![attachment, setImage: image];
|
||||
let image_attr_string =
|
||||
msg_send![class!(NSAttributedString), attributedStringWithAttachment: attachment];
|
||||
attr_string.appendAttributedString_(image_attr_string);
|
||||
|
||||
let another_string = NSString::alloc(nil).init_str("Another String");
|
||||
let another_attr_string =
|
||||
NSAttributedString::alloc(nil).init_attributed_string(another_string);
|
||||
attr_string.appendAttributedString_(another_attr_string);
|
||||
|
||||
let _len: cocoa::foundation::NSUInteger = msg_send![attr_string, length];
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// pasteboard.clearContents();
|
||||
|
||||
let rtfd_data = attr_string.RTFDFromRange_documentAttributes_(
|
||||
NSRange::new(0, msg_send![attr_string, length]),
|
||||
nil,
|
||||
);
|
||||
assert_ne!(rtfd_data, nil);
|
||||
// if rtfd_data != nil {
|
||||
// pasteboard.setData_forType(rtfd_data, NSPasteboardTypeRTFD);
|
||||
// }
|
||||
|
||||
// let rtf_data = attributed_string.RTFFromRange_documentAttributes_(
|
||||
// NSRange::new(0, attributed_string.length()),
|
||||
// nil,
|
||||
// );
|
||||
// if rtf_data != nil {
|
||||
// pasteboard.setData_forType(rtf_data, NSPasteboardTypeRTF);
|
||||
// }
|
||||
|
||||
// let plain_text = attributed_string.string();
|
||||
// pasteboard.setString_forType(plain_text, NSPasteboardTypeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <dispatch/source.h>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
use async_task::Runnable;
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
runtime::{BOOL, YES},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
ptr::{NonNull, addr_of},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent
|
||||
/// these pub items from leaking into public API.
|
||||
pub(crate) mod dispatch_sys {
|
||||
include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
|
||||
}
|
||||
|
||||
use dispatch_sys::*;
|
||||
pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t {
|
||||
addr_of!(_dispatch_main_q) as *const _ as dispatch_queue_t
|
||||
}
|
||||
|
||||
pub(crate) struct MacDispatcher;
|
||||
|
||||
impl PlatformDispatcher for MacDispatcher {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
|
||||
is_main_thread == YES
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
|
||||
unsafe {
|
||||
dispatch_async_f(
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0),
|
||||
runnable.into_raw().as_ptr() as *mut c_void,
|
||||
Some(trampoline),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||
unsafe {
|
||||
dispatch_async_f(
|
||||
dispatch_get_main_queue(),
|
||||
runnable.into_raw().as_ptr() as *mut c_void,
|
||||
Some(trampoline),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
|
||||
unsafe {
|
||||
let queue =
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0);
|
||||
let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64);
|
||||
dispatch_after_f(
|
||||
when,
|
||||
queue,
|
||||
runnable.into_raw().as_ptr() as *mut c_void,
|
||||
Some(trampoline),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn trampoline(runnable: *mut c_void) {
|
||||
let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) };
|
||||
task.run();
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, px, size};
|
||||
use anyhow::Result;
|
||||
use cocoa::{
|
||||
appkit::NSScreen,
|
||||
base::{id, nil},
|
||||
foundation::{NSDictionary, NSString},
|
||||
};
|
||||
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
|
||||
use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID);
|
||||
|
||||
unsafe impl Send for MacDisplay {}
|
||||
|
||||
impl MacDisplay {
|
||||
/// Get the screen with the given [`DisplayId`].
|
||||
pub fn find_by_id(id: DisplayId) -> Option<Self> {
|
||||
Self::all().find(|screen| screen.id() == id)
|
||||
}
|
||||
|
||||
/// Get the primary screen - the one with the menu bar, and whose bottom left
|
||||
/// corner is at the origin of the AppKit coordinate system.
|
||||
pub fn primary() -> Self {
|
||||
// Instead of iterating through all active systems displays via `all()` we use the first
|
||||
// NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList`
|
||||
// will always return a list of active displays (machine might be sleeping).
|
||||
//
|
||||
// The following is what Chromium does too:
|
||||
//
|
||||
// https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56
|
||||
unsafe {
|
||||
let screens = NSScreen::screens(nil);
|
||||
let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0);
|
||||
let device_description = NSScreen::deviceDescription(screen);
|
||||
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
|
||||
let screen_number = device_description.objectForKey_(screen_number_key);
|
||||
let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue];
|
||||
Self(screen_number)
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtains an iterator over all currently active system displays.
|
||||
pub fn all() -> impl Iterator<Item = Self> {
|
||||
unsafe {
|
||||
// We're assuming there aren't more than 32 displays connected to the system.
|
||||
let mut displays = Vec::with_capacity(32);
|
||||
let mut display_count = 0;
|
||||
let result = CGGetActiveDisplayList(
|
||||
displays.capacity() as u32,
|
||||
displays.as_mut_ptr(),
|
||||
&mut display_count,
|
||||
);
|
||||
|
||||
if result == 0 {
|
||||
displays.set_len(display_count as usize);
|
||||
displays.into_iter().map(MacDisplay)
|
||||
} else {
|
||||
panic!("Failed to get active display list. Result: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "ApplicationServices", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
|
||||
}
|
||||
|
||||
impl PlatformDisplay for MacDisplay {
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId(self.0)
|
||||
}
|
||||
|
||||
fn uuid(&self) -> Result<Uuid> {
|
||||
let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) };
|
||||
anyhow::ensure!(
|
||||
!cfuuid.is_null(),
|
||||
"AppKit returned a null from CGDisplayCreateUUIDFromDisplayID"
|
||||
);
|
||||
|
||||
let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) };
|
||||
Ok(Uuid::from_bytes([
|
||||
bytes.byte0,
|
||||
bytes.byte1,
|
||||
bytes.byte2,
|
||||
bytes.byte3,
|
||||
bytes.byte4,
|
||||
bytes.byte5,
|
||||
bytes.byte6,
|
||||
bytes.byte7,
|
||||
bytes.byte8,
|
||||
bytes.byte9,
|
||||
bytes.byte10,
|
||||
bytes.byte11,
|
||||
bytes.byte12,
|
||||
bytes.byte13,
|
||||
bytes.byte14,
|
||||
bytes.byte15,
|
||||
]))
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
unsafe {
|
||||
// CGDisplayBounds is in "global display" coordinates, where 0 is
|
||||
// the top left of the primary display.
|
||||
let bounds = CGDisplayBounds(self.0);
|
||||
|
||||
Bounds {
|
||||
origin: Default::default(),
|
||||
size: size(px(bounds.size.width as f32), px(bounds.size.height as f32)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
use crate::{
|
||||
dispatch_get_main_queue,
|
||||
dispatch_sys::{
|
||||
_dispatch_source_type_data_add, dispatch_resume, dispatch_set_context,
|
||||
dispatch_source_cancel, dispatch_source_create, dispatch_source_merge_data,
|
||||
dispatch_source_set_event_handler_f, dispatch_source_t, dispatch_suspend,
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use core_graphics::display::CGDirectDisplayID;
|
||||
use std::ffi::c_void;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct DisplayLink {
|
||||
display_link: Option<sys::DisplayLink>,
|
||||
frame_requests: dispatch_source_t,
|
||||
}
|
||||
|
||||
impl DisplayLink {
|
||||
pub fn new(
|
||||
display_id: CGDirectDisplayID,
|
||||
data: *mut c_void,
|
||||
callback: unsafe extern "C" fn(*mut c_void),
|
||||
) -> Result<DisplayLink> {
|
||||
unsafe extern "C" fn display_link_callback(
|
||||
_display_link_out: *mut sys::CVDisplayLink,
|
||||
_current_time: *const sys::CVTimeStamp,
|
||||
_output_time: *const sys::CVTimeStamp,
|
||||
_flags_in: i64,
|
||||
_flags_out: *mut i64,
|
||||
frame_requests: *mut c_void,
|
||||
) -> i32 {
|
||||
unsafe {
|
||||
let frame_requests = frame_requests as dispatch_source_t;
|
||||
dispatch_source_merge_data(frame_requests, 1);
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let frame_requests = dispatch_source_create(
|
||||
&_dispatch_source_type_data_add,
|
||||
0,
|
||||
0,
|
||||
dispatch_get_main_queue(),
|
||||
);
|
||||
dispatch_set_context(
|
||||
crate::dispatch_sys::dispatch_object_t {
|
||||
_ds: frame_requests,
|
||||
},
|
||||
data,
|
||||
);
|
||||
dispatch_source_set_event_handler_f(frame_requests, Some(callback));
|
||||
|
||||
let display_link = sys::DisplayLink::new(
|
||||
display_id,
|
||||
display_link_callback,
|
||||
frame_requests as *mut c_void,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
display_link: Some(display_link),
|
||||
frame_requests,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
dispatch_resume(crate::dispatch_sys::dispatch_object_t {
|
||||
_ds: self.frame_requests,
|
||||
});
|
||||
self.display_link.as_mut().unwrap().start()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
dispatch_suspend(crate::dispatch_sys::dispatch_object_t {
|
||||
_ds: self.frame_requests,
|
||||
});
|
||||
self.display_link.as_mut().unwrap().stop()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DisplayLink {
|
||||
fn drop(&mut self) {
|
||||
self.stop().log_err();
|
||||
// We see occasional segfaults on the CVDisplayLink thread.
|
||||
//
|
||||
// It seems possible that this happens because CVDisplayLinkRelease releases the CVDisplayLink
|
||||
// on the main thread immediately, but the background thread that CVDisplayLink uses for timers
|
||||
// is still accessing it.
|
||||
//
|
||||
// We might also want to upgrade to CADisplayLink, but that requires dropping old macOS support.
|
||||
std::mem::forget(self.display_link.take());
|
||||
unsafe {
|
||||
dispatch_source_cancel(self.frame_requests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod sys {
|
||||
//! Derived from display-link crate under the following license:
|
||||
//! <https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT>
|
||||
//! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc)
|
||||
#![allow(dead_code, non_upper_case_globals)]
|
||||
|
||||
use anyhow::Result;
|
||||
use core_graphics::display::CGDirectDisplayID;
|
||||
use foreign_types::{ForeignType, foreign_type};
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
fmt::{self, Debug, Formatter},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CVDisplayLink {}
|
||||
|
||||
foreign_type! {
|
||||
pub unsafe type DisplayLink {
|
||||
type CType = CVDisplayLink;
|
||||
fn drop = CVDisplayLinkRelease;
|
||||
fn clone = CVDisplayLinkRetain;
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for DisplayLink {
|
||||
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
|
||||
formatter
|
||||
.debug_tuple("DisplayLink")
|
||||
.field(&self.as_ptr())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct CVTimeStamp {
|
||||
pub version: u32,
|
||||
pub video_time_scale: i32,
|
||||
pub video_time: i64,
|
||||
pub host_time: u64,
|
||||
pub rate_scalar: f64,
|
||||
pub video_refresh_period: i64,
|
||||
pub smpte_time: CVSMPTETime,
|
||||
pub flags: u64,
|
||||
pub reserved: u64,
|
||||
}
|
||||
|
||||
pub type CVTimeStampFlags = u64;
|
||||
|
||||
pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0;
|
||||
pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1;
|
||||
pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2;
|
||||
pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3;
|
||||
pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4;
|
||||
pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16;
|
||||
pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17;
|
||||
pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags =
|
||||
kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid;
|
||||
pub const kCVTimeStampIsInterlaced: CVTimeStampFlags =
|
||||
kCVTimeStampTopField | kCVTimeStampBottomField;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct CVSMPTETime {
|
||||
pub subframes: i16,
|
||||
pub subframe_divisor: i16,
|
||||
pub counter: u32,
|
||||
pub time_type: u32,
|
||||
pub flags: u32,
|
||||
pub hours: i16,
|
||||
pub minutes: i16,
|
||||
pub seconds: i16,
|
||||
pub frames: i16,
|
||||
}
|
||||
|
||||
pub type CVSMPTETimeType = u32;
|
||||
|
||||
pub const kCVSMPTETimeType24: CVSMPTETimeType = 0;
|
||||
pub const kCVSMPTETimeType25: CVSMPTETimeType = 1;
|
||||
pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2;
|
||||
pub const kCVSMPTETimeType30: CVSMPTETimeType = 3;
|
||||
pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4;
|
||||
pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5;
|
||||
pub const kCVSMPTETimeType60: CVSMPTETimeType = 6;
|
||||
pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7;
|
||||
|
||||
pub type CVSMPTETimeFlags = u32;
|
||||
|
||||
pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0;
|
||||
pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1;
|
||||
|
||||
pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn(
|
||||
display_link_out: *mut CVDisplayLink,
|
||||
// A pointer to the current timestamp. This represents the timestamp when the callback is called.
|
||||
current_time: *const CVTimeStamp,
|
||||
// A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed.
|
||||
output_time: *const CVTimeStamp,
|
||||
// Unused
|
||||
flags_in: i64,
|
||||
// Unused
|
||||
flags_out: *mut i64,
|
||||
// A pointer to app-defined data.
|
||||
display_link_context: *mut c_void,
|
||||
) -> i32;
|
||||
|
||||
#[link(name = "CoreFoundation", kind = "framework")]
|
||||
#[link(name = "CoreVideo", kind = "framework")]
|
||||
#[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)]
|
||||
unsafe extern "C" {
|
||||
pub fn CVDisplayLinkCreateWithActiveCGDisplays(
|
||||
display_link_out: *mut *mut CVDisplayLink,
|
||||
) -> i32;
|
||||
pub fn CVDisplayLinkSetCurrentCGDisplay(
|
||||
display_link: &mut DisplayLinkRef,
|
||||
display_id: u32,
|
||||
) -> i32;
|
||||
pub fn CVDisplayLinkSetOutputCallback(
|
||||
display_link: &mut DisplayLinkRef,
|
||||
callback: CVDisplayLinkOutputCallback,
|
||||
user_info: *mut c_void,
|
||||
) -> i32;
|
||||
pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32;
|
||||
pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32;
|
||||
pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink);
|
||||
pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink;
|
||||
}
|
||||
|
||||
impl DisplayLink {
|
||||
/// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc)
|
||||
pub unsafe fn new(
|
||||
display_id: CGDirectDisplayID,
|
||||
callback: CVDisplayLinkOutputCallback,
|
||||
user_info: *mut c_void,
|
||||
) -> Result<Self> {
|
||||
unsafe {
|
||||
let mut display_link: *mut CVDisplayLink = 0 as _;
|
||||
|
||||
let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link);
|
||||
anyhow::ensure!(code == 0, "could not create display link, code: {}", code);
|
||||
|
||||
let mut display_link = DisplayLink::from_ptr(display_link);
|
||||
|
||||
let code = CVDisplayLinkSetOutputCallback(&mut display_link, callback, user_info);
|
||||
anyhow::ensure!(code == 0, "could not set output callback, code: {}", code);
|
||||
|
||||
let code = CVDisplayLinkSetCurrentCGDisplay(&mut display_link, display_id);
|
||||
anyhow::ensure!(
|
||||
code == 0,
|
||||
"could not assign display to display link, code: {}",
|
||||
code
|
||||
);
|
||||
|
||||
Ok(display_link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayLinkRef {
|
||||
/// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc)
|
||||
pub unsafe fn start(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
let code = CVDisplayLinkStart(self);
|
||||
anyhow::ensure!(code == 0, "could not start display link, code: {}", code);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc)
|
||||
pub unsafe fn stop(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
let code = CVDisplayLinkStop(self);
|
||||
anyhow::ensure!(code == 0, "could not stop display link, code: {}", code);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
use crate::{
|
||||
Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
||||
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
platform::mac::{
|
||||
LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource,
|
||||
TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
|
||||
},
|
||||
point, px,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||
base::{YES, id},
|
||||
};
|
||||
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
|
||||
use core_graphics::event::CGKeyCode;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use std::{borrow::Cow, ffi::c_void};
|
||||
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const SPACE_KEY: u16 = b' ' as u16;
|
||||
const ENTER_KEY: u16 = 0x0d;
|
||||
const NUMPAD_ENTER_KEY: u16 = 0x03;
|
||||
pub(crate) const ESCAPE_KEY: u16 = 0x1b;
|
||||
const TAB_KEY: u16 = 0x09;
|
||||
const SHIFT_TAB_KEY: u16 = 0x19;
|
||||
|
||||
pub fn key_to_native(key: &str) -> Cow<'_, str> {
|
||||
use cocoa::appkit::*;
|
||||
let code = match key {
|
||||
"space" => SPACE_KEY,
|
||||
"backspace" => BACKSPACE_KEY,
|
||||
"escape" => ESCAPE_KEY,
|
||||
"up" => NSUpArrowFunctionKey,
|
||||
"down" => NSDownArrowFunctionKey,
|
||||
"left" => NSLeftArrowFunctionKey,
|
||||
"right" => NSRightArrowFunctionKey,
|
||||
"pageup" => NSPageUpFunctionKey,
|
||||
"pagedown" => NSPageDownFunctionKey,
|
||||
"home" => NSHomeFunctionKey,
|
||||
"end" => NSEndFunctionKey,
|
||||
"delete" => NSDeleteFunctionKey,
|
||||
"insert" => NSHelpFunctionKey,
|
||||
"f1" => NSF1FunctionKey,
|
||||
"f2" => NSF2FunctionKey,
|
||||
"f3" => NSF3FunctionKey,
|
||||
"f4" => NSF4FunctionKey,
|
||||
"f5" => NSF5FunctionKey,
|
||||
"f6" => NSF6FunctionKey,
|
||||
"f7" => NSF7FunctionKey,
|
||||
"f8" => NSF8FunctionKey,
|
||||
"f9" => NSF9FunctionKey,
|
||||
"f10" => NSF10FunctionKey,
|
||||
"f11" => NSF11FunctionKey,
|
||||
"f12" => NSF12FunctionKey,
|
||||
"f13" => NSF13FunctionKey,
|
||||
"f14" => NSF14FunctionKey,
|
||||
"f15" => NSF15FunctionKey,
|
||||
"f16" => NSF16FunctionKey,
|
||||
"f17" => NSF17FunctionKey,
|
||||
"f18" => NSF18FunctionKey,
|
||||
"f19" => NSF19FunctionKey,
|
||||
"f20" => NSF20FunctionKey,
|
||||
"f21" => NSF21FunctionKey,
|
||||
"f22" => NSF22FunctionKey,
|
||||
"f23" => NSF23FunctionKey,
|
||||
"f24" => NSF24FunctionKey,
|
||||
"f25" => NSF25FunctionKey,
|
||||
"f26" => NSF26FunctionKey,
|
||||
"f27" => NSF27FunctionKey,
|
||||
"f28" => NSF28FunctionKey,
|
||||
"f29" => NSF29FunctionKey,
|
||||
"f30" => NSF30FunctionKey,
|
||||
"f31" => NSF31FunctionKey,
|
||||
"f32" => NSF32FunctionKey,
|
||||
"f33" => NSF33FunctionKey,
|
||||
"f34" => NSF34FunctionKey,
|
||||
"f35" => NSF35FunctionKey,
|
||||
_ => return Cow::Borrowed(key),
|
||||
};
|
||||
Cow::Owned(String::from_utf16(&[code]).unwrap())
|
||||
}
|
||||
|
||||
unsafe fn read_modifiers(native_event: id) -> Modifiers {
|
||||
unsafe {
|
||||
let modifiers = native_event.modifierFlags();
|
||||
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
|
||||
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
|
||||
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
|
||||
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
|
||||
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
|
||||
|
||||
Modifiers {
|
||||
control,
|
||||
alt,
|
||||
shift,
|
||||
platform: command,
|
||||
function,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformInput {
|
||||
pub(crate) unsafe fn from_native(
|
||||
native_event: id,
|
||||
window_height: Option<Pixels>,
|
||||
) -> Option<Self> {
|
||||
unsafe {
|
||||
let event_type = native_event.eventType();
|
||||
|
||||
// Filter out event types that aren't in the NSEventType enum.
|
||||
// See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
|
||||
match event_type as u64 {
|
||||
0 | 21 | 32 | 33 | 35 | 36 | 37 => {
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match event_type {
|
||||
NSEventType::NSFlagsChanged => {
|
||||
Some(Self::ModifiersChanged(ModifiersChangedEvent {
|
||||
modifiers: read_modifiers(native_event),
|
||||
capslock: Capslock {
|
||||
on: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSAlphaShiftKeyMask),
|
||||
},
|
||||
}))
|
||||
}
|
||||
NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
|
||||
keystroke: parse_keystroke(native_event),
|
||||
is_held: native_event.isARepeat() == YES,
|
||||
})),
|
||||
NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent {
|
||||
keystroke: parse_keystroke(native_event),
|
||||
})),
|
||||
NSEventType::NSLeftMouseDown
|
||||
| NSEventType::NSRightMouseDown
|
||||
| NSEventType::NSOtherMouseDown => {
|
||||
let button = match native_event.buttonNumber() {
|
||||
0 => MouseButton::Left,
|
||||
1 => MouseButton::Right,
|
||||
2 => MouseButton::Middle,
|
||||
3 => MouseButton::Navigate(NavigationDirection::Back),
|
||||
4 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||
// Other mouse buttons aren't tracked currently
|
||||
_ => return None,
|
||||
};
|
||||
window_height.map(|window_height| {
|
||||
Self::MouseDown(MouseDownEvent {
|
||||
button,
|
||||
position: point(
|
||||
px(native_event.locationInWindow().x as f32),
|
||||
// MacOS screen coordinates are relative to bottom left
|
||||
window_height - px(native_event.locationInWindow().y as f32),
|
||||
),
|
||||
modifiers: read_modifiers(native_event),
|
||||
click_count: native_event.clickCount() as usize,
|
||||
first_mouse: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseUp
|
||||
| NSEventType::NSRightMouseUp
|
||||
| NSEventType::NSOtherMouseUp => {
|
||||
let button = match native_event.buttonNumber() {
|
||||
0 => MouseButton::Left,
|
||||
1 => MouseButton::Right,
|
||||
2 => MouseButton::Middle,
|
||||
3 => MouseButton::Navigate(NavigationDirection::Back),
|
||||
4 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||
// Other mouse buttons aren't tracked currently
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
window_height.map(|window_height| {
|
||||
Self::MouseUp(MouseUpEvent {
|
||||
button,
|
||||
position: point(
|
||||
px(native_event.locationInWindow().x as f32),
|
||||
window_height - px(native_event.locationInWindow().y as f32),
|
||||
),
|
||||
modifiers: read_modifiers(native_event),
|
||||
click_count: native_event.clickCount() as usize,
|
||||
})
|
||||
})
|
||||
}
|
||||
// Some mice (like Logitech MX Master) send navigation buttons as swipe events
|
||||
NSEventType::NSEventTypeSwipe => {
|
||||
let navigation_direction = match native_event.phase() {
|
||||
NSEventPhase::NSEventPhaseEnded => match native_event.deltaX() {
|
||||
x if x > 0.0 => Some(NavigationDirection::Back),
|
||||
x if x < 0.0 => Some(NavigationDirection::Forward),
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
match navigation_direction {
|
||||
Some(direction) => window_height.map(|window_height| {
|
||||
Self::MouseDown(MouseDownEvent {
|
||||
button: MouseButton::Navigate(direction),
|
||||
position: point(
|
||||
px(native_event.locationInWindow().x as f32),
|
||||
window_height - px(native_event.locationInWindow().y as f32),
|
||||
),
|
||||
modifiers: read_modifiers(native_event),
|
||||
click_count: 1,
|
||||
first_mouse: false,
|
||||
})
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
NSEventType::NSScrollWheel => window_height.map(|window_height| {
|
||||
let phase = match native_event.phase() {
|
||||
NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
|
||||
TouchPhase::Started
|
||||
}
|
||||
NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended,
|
||||
_ => TouchPhase::Moved,
|
||||
};
|
||||
|
||||
let raw_data = point(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
);
|
||||
|
||||
let delta = if native_event.hasPreciseScrollingDeltas() == YES {
|
||||
ScrollDelta::Pixels(raw_data.map(px))
|
||||
} else {
|
||||
ScrollDelta::Lines(raw_data)
|
||||
};
|
||||
|
||||
Self::ScrollWheel(ScrollWheelEvent {
|
||||
position: point(
|
||||
px(native_event.locationInWindow().x as f32),
|
||||
window_height - px(native_event.locationInWindow().y as f32),
|
||||
),
|
||||
delta,
|
||||
touch_phase: phase,
|
||||
modifiers: read_modifiers(native_event),
|
||||
})
|
||||
}),
|
||||
NSEventType::NSLeftMouseDragged
|
||||
| NSEventType::NSRightMouseDragged
|
||||
| NSEventType::NSOtherMouseDragged => {
|
||||
let pressed_button = match native_event.buttonNumber() {
|
||||
0 => MouseButton::Left,
|
||||
1 => MouseButton::Right,
|
||||
2 => MouseButton::Middle,
|
||||
3 => MouseButton::Navigate(NavigationDirection::Back),
|
||||
4 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||
// Other mouse buttons aren't tracked currently
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
window_height.map(|window_height| {
|
||||
Self::MouseMove(MouseMoveEvent {
|
||||
pressed_button: Some(pressed_button),
|
||||
position: point(
|
||||
px(native_event.locationInWindow().x as f32),
|
||||
window_height - px(native_event.locationInWindow().y as f32),
|
||||
),
|
||||
modifiers: read_modifiers(native_event),
|
||||
})
|
||||
})
|
||||
}
|
||||
NSEventType::NSMouseMoved => window_height.map(|window_height| {
|
||||
Self::MouseMove(MouseMoveEvent {
|
||||
position: point(
|
||||
px(native_event.locationInWindow().x as f32),
|
||||
window_height - px(native_event.locationInWindow().y as f32),
|
||||
),
|
||||
pressed_button: None,
|
||||
modifiers: read_modifiers(native_event),
|
||||
})
|
||||
}),
|
||||
NSEventType::NSMouseExited => window_height.map(|window_height| {
|
||||
Self::MouseExited(MouseExitEvent {
|
||||
position: point(
|
||||
px(native_event.locationInWindow().x as f32),
|
||||
window_height - px(native_event.locationInWindow().y as f32),
|
||||
),
|
||||
|
||||
pressed_button: None,
|
||||
modifiers: read_modifiers(native_event),
|
||||
})
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
unsafe {
|
||||
use cocoa::appkit::*;
|
||||
|
||||
let mut characters = native_event
|
||||
.charactersIgnoringModifiers()
|
||||
.to_str()
|
||||
.to_string();
|
||||
let mut key_char = None;
|
||||
let first_char = characters.chars().next().map(|ch| ch as u16);
|
||||
let modifiers = native_event.modifierFlags();
|
||||
|
||||
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
|
||||
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
|
||||
let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
|
||||
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
|
||||
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
|
||||
&& first_char
|
||||
.is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch));
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
let key = match first_char {
|
||||
Some(SPACE_KEY) => {
|
||||
key_char = Some(" ".to_string());
|
||||
"space".to_string()
|
||||
}
|
||||
Some(TAB_KEY) => {
|
||||
key_char = Some("\t".to_string());
|
||||
"tab".to_string()
|
||||
}
|
||||
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => {
|
||||
key_char = Some("\n".to_string());
|
||||
"enter".to_string()
|
||||
}
|
||||
Some(BACKSPACE_KEY) => "backspace".to_string(),
|
||||
Some(ESCAPE_KEY) => "escape".to_string(),
|
||||
Some(SHIFT_TAB_KEY) => "tab".to_string(),
|
||||
Some(NSUpArrowFunctionKey) => "up".to_string(),
|
||||
Some(NSDownArrowFunctionKey) => "down".to_string(),
|
||||
Some(NSLeftArrowFunctionKey) => "left".to_string(),
|
||||
Some(NSRightArrowFunctionKey) => "right".to_string(),
|
||||
Some(NSPageUpFunctionKey) => "pageup".to_string(),
|
||||
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
|
||||
Some(NSHomeFunctionKey) => "home".to_string(),
|
||||
Some(NSEndFunctionKey) => "end".to_string(),
|
||||
Some(NSDeleteFunctionKey) => "delete".to_string(),
|
||||
// Observed Insert==NSHelpFunctionKey not NSInsertFunctionKey.
|
||||
Some(NSHelpFunctionKey) => "insert".to_string(),
|
||||
Some(NSF1FunctionKey) => "f1".to_string(),
|
||||
Some(NSF2FunctionKey) => "f2".to_string(),
|
||||
Some(NSF3FunctionKey) => "f3".to_string(),
|
||||
Some(NSF4FunctionKey) => "f4".to_string(),
|
||||
Some(NSF5FunctionKey) => "f5".to_string(),
|
||||
Some(NSF6FunctionKey) => "f6".to_string(),
|
||||
Some(NSF7FunctionKey) => "f7".to_string(),
|
||||
Some(NSF8FunctionKey) => "f8".to_string(),
|
||||
Some(NSF9FunctionKey) => "f9".to_string(),
|
||||
Some(NSF10FunctionKey) => "f10".to_string(),
|
||||
Some(NSF11FunctionKey) => "f11".to_string(),
|
||||
Some(NSF12FunctionKey) => "f12".to_string(),
|
||||
Some(NSF13FunctionKey) => "f13".to_string(),
|
||||
Some(NSF14FunctionKey) => "f14".to_string(),
|
||||
Some(NSF15FunctionKey) => "f15".to_string(),
|
||||
Some(NSF16FunctionKey) => "f16".to_string(),
|
||||
Some(NSF17FunctionKey) => "f17".to_string(),
|
||||
Some(NSF18FunctionKey) => "f18".to_string(),
|
||||
Some(NSF19FunctionKey) => "f19".to_string(),
|
||||
Some(NSF20FunctionKey) => "f20".to_string(),
|
||||
Some(NSF21FunctionKey) => "f21".to_string(),
|
||||
Some(NSF22FunctionKey) => "f22".to_string(),
|
||||
Some(NSF23FunctionKey) => "f23".to_string(),
|
||||
Some(NSF24FunctionKey) => "f24".to_string(),
|
||||
Some(NSF25FunctionKey) => "f25".to_string(),
|
||||
Some(NSF26FunctionKey) => "f26".to_string(),
|
||||
Some(NSF27FunctionKey) => "f27".to_string(),
|
||||
Some(NSF28FunctionKey) => "f28".to_string(),
|
||||
Some(NSF29FunctionKey) => "f29".to_string(),
|
||||
Some(NSF30FunctionKey) => "f30".to_string(),
|
||||
Some(NSF31FunctionKey) => "f31".to_string(),
|
||||
Some(NSF32FunctionKey) => "f32".to_string(),
|
||||
Some(NSF33FunctionKey) => "f33".to_string(),
|
||||
Some(NSF34FunctionKey) => "f34".to_string(),
|
||||
Some(NSF35FunctionKey) => "f35".to_string(),
|
||||
_ => {
|
||||
// Cases to test when modifying this:
|
||||
//
|
||||
// qwerty key | none | cmd | cmd-shift
|
||||
// * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout)
|
||||
// * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd)
|
||||
// * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S)
|
||||
// * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers)
|
||||
// * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/)
|
||||
// * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout)
|
||||
// * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z)
|
||||
//
|
||||
let mut chars_ignoring_modifiers =
|
||||
chars_for_modified_key(native_event.keyCode(), NO_MOD);
|
||||
let mut chars_with_shift =
|
||||
chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
|
||||
let always_use_cmd_layout = always_use_command_layout();
|
||||
|
||||
// Handle Dvorak+QWERTY / Russian / Armenian
|
||||
if command || always_use_cmd_layout {
|
||||
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
|
||||
let chars_with_both =
|
||||
chars_for_modified_key(native_event.keyCode(), CMD_MOD | SHIFT_MOD);
|
||||
|
||||
// We don't do this in the case that the shifted command key generates
|
||||
// the same character as the unshifted command key (Norwegian, e.g.)
|
||||
if chars_with_both != chars_with_cmd {
|
||||
chars_with_shift = chars_with_both;
|
||||
|
||||
// Handle edge-case where cmd-shift-s reports cmd-s instead of
|
||||
// cmd-shift-s (Ukrainian, etc.)
|
||||
} else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
|
||||
chars_with_shift = chars_with_cmd.to_ascii_uppercase();
|
||||
}
|
||||
chars_ignoring_modifiers = chars_with_cmd;
|
||||
}
|
||||
|
||||
if !control && !command && !function {
|
||||
let mut mods = NO_MOD;
|
||||
if shift {
|
||||
mods |= SHIFT_MOD;
|
||||
}
|
||||
if alt {
|
||||
mods |= OPTION_MOD;
|
||||
}
|
||||
|
||||
key_char = Some(chars_for_modified_key(native_event.keyCode(), mods));
|
||||
}
|
||||
|
||||
if shift
|
||||
&& chars_ignoring_modifiers
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase())
|
||||
{
|
||||
chars_ignoring_modifiers
|
||||
} else if shift {
|
||||
shift = false;
|
||||
chars_with_shift
|
||||
} else {
|
||||
chars_ignoring_modifiers
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Keystroke {
|
||||
modifiers: Modifiers {
|
||||
control,
|
||||
alt,
|
||||
shift,
|
||||
platform: command,
|
||||
function,
|
||||
},
|
||||
key,
|
||||
key_char,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn always_use_command_layout() -> bool {
|
||||
if chars_for_modified_key(0, NO_MOD).is_ascii() {
|
||||
return false;
|
||||
}
|
||||
|
||||
chars_for_modified_key(0, CMD_MOD).is_ascii()
|
||||
}
|
||||
|
||||
const NO_MOD: u32 = 0;
|
||||
const CMD_MOD: u32 = 1;
|
||||
const SHIFT_MOD: u32 = 2;
|
||||
const OPTION_MOD: u32 = 8;
|
||||
|
||||
fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
|
||||
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
|
||||
// shifted >> 8 for UCKeyTranslate
|
||||
const CG_SPACE_KEY: u16 = 49;
|
||||
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kUCKeyActionDown: u16 = 0;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
|
||||
|
||||
let keyboard_type = unsafe { LMGetKbdType() as u32 };
|
||||
const BUFFER_SIZE: usize = 4;
|
||||
let mut dead_key_state = 0;
|
||||
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
|
||||
let mut buffer_size: usize = 0;
|
||||
|
||||
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
|
||||
if keyboard.is_null() {
|
||||
return "".to_string();
|
||||
}
|
||||
let layout_data = unsafe {
|
||||
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
|
||||
as CFDataRef
|
||||
};
|
||||
if layout_data.is_null() {
|
||||
unsafe {
|
||||
let _: () = msg_send![keyboard, release];
|
||||
}
|
||||
return "".to_string();
|
||||
}
|
||||
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
|
||||
|
||||
unsafe {
|
||||
UCKeyTranslate(
|
||||
keyboard_layout as *const c_void,
|
||||
code,
|
||||
kUCKeyActionDown,
|
||||
modifiers,
|
||||
keyboard_type,
|
||||
kUCKeyTranslateNoDeadKeysMask,
|
||||
&mut dead_key_state,
|
||||
BUFFER_SIZE,
|
||||
&mut buffer_size as *mut usize,
|
||||
&mut buffer as *mut u16,
|
||||
);
|
||||
if dead_key_state != 0 {
|
||||
UCKeyTranslate(
|
||||
keyboard_layout as *const c_void,
|
||||
CG_SPACE_KEY,
|
||||
kUCKeyActionDown,
|
||||
modifiers,
|
||||
keyboard_type,
|
||||
kUCKeyTranslateNoDeadKeysMask,
|
||||
&mut dead_key_state,
|
||||
BUFFER_SIZE,
|
||||
&mut buffer_size as *mut usize,
|
||||
&mut buffer as *mut u16,
|
||||
);
|
||||
}
|
||||
let _: () = msg_send![keyboard, release];
|
||||
}
|
||||
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
|
||||
}
|
||||
+1500
File diff suppressed because it is too large
Load Diff
+281
@@ -0,0 +1,281 @@
|
||||
use crate::{
|
||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
|
||||
Point, Size, platform::AtlasTextureList,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::FxHashMap;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use etagere::BucketedAtlasAllocator;
|
||||
use metal::Device;
|
||||
use parking_lot::Mutex;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
|
||||
|
||||
impl MetalAtlas {
|
||||
pub(crate) fn new(device: Device) -> Self {
|
||||
MetalAtlas(Mutex::new(MetalAtlasState {
|
||||
device: AssertSend(device),
|
||||
monochrome_textures: Default::default(),
|
||||
polychrome_textures: Default::default(),
|
||||
tiles_by_key: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture {
|
||||
self.0.lock().texture(id).metal_texture.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct MetalAtlasState {
|
||||
device: AssertSend<Device>,
|
||||
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
|
||||
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
|
||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
||||
}
|
||||
|
||||
impl PlatformAtlas for MetalAtlas {
|
||||
fn get_or_insert_with<'a>(
|
||||
&self,
|
||||
key: &AtlasKey,
|
||||
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
||||
) -> Result<Option<AtlasTile>> {
|
||||
let mut lock = self.0.lock();
|
||||
if let Some(tile) = lock.tiles_by_key.get(key) {
|
||||
Ok(Some(tile.clone()))
|
||||
} else {
|
||||
let Some((size, bytes)) = build()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let tile = lock
|
||||
.allocate(size, key.texture_kind())
|
||||
.context("failed to allocate")?;
|
||||
let texture = lock.texture(tile.texture_id);
|
||||
texture.upload(tile.bounds, &bytes);
|
||||
lock.tiles_by_key.insert(key.clone(), tile.clone());
|
||||
Ok(Some(tile))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self, key: &AtlasKey) {
|
||||
let mut lock = self.0.lock();
|
||||
let Some(id) = lock.tiles_by_key.get(key).map(|v| v.texture_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let textures = match id.kind {
|
||||
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
|
||||
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
|
||||
};
|
||||
|
||||
let Some(texture_slot) = textures
|
||||
.textures
|
||||
.iter_mut()
|
||||
.find(|texture| texture.as_ref().is_some_and(|v| v.id == id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mut texture) = texture_slot.take() {
|
||||
texture.decrement_ref_count();
|
||||
|
||||
if texture.is_unreferenced() {
|
||||
textures.free_list.push(id.index as usize);
|
||||
lock.tiles_by_key.remove(key);
|
||||
} else {
|
||||
*texture_slot = Some(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MetalAtlasState {
|
||||
fn allocate(
|
||||
&mut self,
|
||||
size: Size<DevicePixels>,
|
||||
texture_kind: AtlasTextureKind,
|
||||
) -> Option<AtlasTile> {
|
||||
{
|
||||
let textures = match texture_kind {
|
||||
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||
};
|
||||
|
||||
if let Some(tile) = textures
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find_map(|texture| texture.allocate(size))
|
||||
{
|
||||
return Some(tile);
|
||||
}
|
||||
}
|
||||
|
||||
let texture = self.push_texture(size, texture_kind);
|
||||
texture.allocate(size)
|
||||
}
|
||||
|
||||
fn push_texture(
|
||||
&mut self,
|
||||
min_size: Size<DevicePixels>,
|
||||
kind: AtlasTextureKind,
|
||||
) -> &mut MetalAtlasTexture {
|
||||
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
|
||||
width: DevicePixels(1024),
|
||||
height: DevicePixels(1024),
|
||||
};
|
||||
// Max texture size on all modern Apple GPUs. Anything bigger than that crashes in validateWithDevice.
|
||||
const MAX_ATLAS_SIZE: Size<DevicePixels> = Size {
|
||||
width: DevicePixels(16384),
|
||||
height: DevicePixels(16384),
|
||||
};
|
||||
let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE);
|
||||
let texture_descriptor = metal::TextureDescriptor::new();
|
||||
texture_descriptor.set_width(size.width.into());
|
||||
texture_descriptor.set_height(size.height.into());
|
||||
let pixel_format;
|
||||
let usage;
|
||||
match kind {
|
||||
AtlasTextureKind::Monochrome => {
|
||||
pixel_format = metal::MTLPixelFormat::A8Unorm;
|
||||
usage = metal::MTLTextureUsage::ShaderRead;
|
||||
}
|
||||
AtlasTextureKind::Polychrome => {
|
||||
pixel_format = metal::MTLPixelFormat::BGRA8Unorm;
|
||||
usage = metal::MTLTextureUsage::ShaderRead;
|
||||
}
|
||||
}
|
||||
texture_descriptor.set_pixel_format(pixel_format);
|
||||
texture_descriptor.set_usage(usage);
|
||||
let metal_texture = self.device.new_texture(&texture_descriptor);
|
||||
|
||||
let texture_list = match kind {
|
||||
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||
};
|
||||
|
||||
let index = texture_list.free_list.pop();
|
||||
|
||||
let atlas_texture = MetalAtlasTexture {
|
||||
id: AtlasTextureId {
|
||||
index: index.unwrap_or(texture_list.textures.len()) as u32,
|
||||
kind,
|
||||
},
|
||||
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
|
||||
metal_texture: AssertSend(metal_texture),
|
||||
live_atlas_keys: 0,
|
||||
};
|
||||
|
||||
if let Some(ix) = index {
|
||||
texture_list.textures[ix] = Some(atlas_texture);
|
||||
texture_list.textures.get_mut(ix)
|
||||
} else {
|
||||
texture_list.textures.push(Some(atlas_texture));
|
||||
texture_list.textures.last_mut()
|
||||
}
|
||||
.unwrap()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture {
|
||||
let textures = match id.kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
};
|
||||
textures[id.index as usize].as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
struct MetalAtlasTexture {
|
||||
id: AtlasTextureId,
|
||||
allocator: BucketedAtlasAllocator,
|
||||
metal_texture: AssertSend<metal::Texture>,
|
||||
live_atlas_keys: u32,
|
||||
}
|
||||
|
||||
impl MetalAtlasTexture {
|
||||
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
|
||||
let allocation = self.allocator.allocate(size.into())?;
|
||||
let tile = AtlasTile {
|
||||
texture_id: self.id,
|
||||
tile_id: allocation.id.into(),
|
||||
bounds: Bounds {
|
||||
origin: allocation.rectangle.min.into(),
|
||||
size,
|
||||
},
|
||||
padding: 0,
|
||||
};
|
||||
self.live_atlas_keys += 1;
|
||||
Some(tile)
|
||||
}
|
||||
|
||||
fn upload(&self, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
|
||||
let region = metal::MTLRegion::new_2d(
|
||||
bounds.origin.x.into(),
|
||||
bounds.origin.y.into(),
|
||||
bounds.size.width.into(),
|
||||
bounds.size.height.into(),
|
||||
);
|
||||
self.metal_texture.replace_region(
|
||||
region,
|
||||
0,
|
||||
bytes.as_ptr() as *const _,
|
||||
bounds.size.width.to_bytes(self.bytes_per_pixel()) as u64,
|
||||
);
|
||||
}
|
||||
|
||||
fn bytes_per_pixel(&self) -> u8 {
|
||||
use metal::MTLPixelFormat::*;
|
||||
match self.metal_texture.pixel_format() {
|
||||
A8Unorm | R8Unorm => 1,
|
||||
RGBA8Unorm | BGRA8Unorm => 4,
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn decrement_ref_count(&mut self) {
|
||||
self.live_atlas_keys -= 1;
|
||||
}
|
||||
|
||||
fn is_unreferenced(&mut self) -> bool {
|
||||
self.live_atlas_keys == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size<DevicePixels>> for etagere::Size {
|
||||
fn from(size: Size<DevicePixels>) -> Self {
|
||||
etagere::Size::new(size.width.into(), size.height.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Point> for Point<DevicePixels> {
|
||||
fn from(value: etagere::Point) -> Self {
|
||||
Point {
|
||||
x: DevicePixels::from(value.x),
|
||||
y: DevicePixels::from(value.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Size> for Size<DevicePixels> {
|
||||
fn from(size: etagere::Size) -> Self {
|
||||
Size {
|
||||
width: DevicePixels::from(size.width),
|
||||
height: DevicePixels::from(size.height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Rectangle> for Bounds<DevicePixels> {
|
||||
fn from(rectangle: etagere::Rectangle) -> Self {
|
||||
Bounds {
|
||||
origin: rectangle.min.into(),
|
||||
size: rectangle.size().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
struct AssertSend<T>(T);
|
||||
|
||||
unsafe impl<T> Send for AssertSend<T> {}
|
||||
+1390
File diff suppressed because it is too large
Load Diff
+147
@@ -0,0 +1,147 @@
|
||||
#![allow(unused, non_upper_case_globals)]
|
||||
|
||||
use crate::{FontFallbacks, FontFeatures};
|
||||
use cocoa::appkit::CGFloat;
|
||||
use core_foundation::{
|
||||
array::{
|
||||
CFArray, CFArrayAppendArray, CFArrayAppendValue, CFArrayCreateMutable, CFArrayGetCount,
|
||||
CFArrayGetValueAtIndex, CFArrayRef, CFMutableArrayRef, kCFTypeArrayCallBacks,
|
||||
},
|
||||
base::{CFRelease, TCFType, kCFAllocatorDefault},
|
||||
dictionary::{
|
||||
CFDictionaryCreate, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks,
|
||||
},
|
||||
number::CFNumber,
|
||||
string::{CFString, CFStringRef},
|
||||
};
|
||||
use core_foundation_sys::locale::CFLocaleCopyPreferredLanguages;
|
||||
use core_graphics::{display::CFDictionary, geometry::CGAffineTransform};
|
||||
use core_text::{
|
||||
font::{CTFont, CTFontRef, cascade_list_for_languages},
|
||||
font_descriptor::{
|
||||
CTFontDescriptor, CTFontDescriptorCopyAttributes, CTFontDescriptorCreateCopyWithFeature,
|
||||
CTFontDescriptorCreateWithAttributes, CTFontDescriptorCreateWithNameAndSize,
|
||||
CTFontDescriptorRef, kCTFontCascadeListAttribute, kCTFontFeatureSettingsAttribute,
|
||||
},
|
||||
};
|
||||
use font_kit::font::Font as FontKitFont;
|
||||
use std::ptr;
|
||||
|
||||
pub fn apply_features_and_fallbacks(
|
||||
font: &mut FontKitFont,
|
||||
features: &FontFeatures,
|
||||
fallbacks: Option<&FontFallbacks>,
|
||||
) -> anyhow::Result<()> {
|
||||
unsafe {
|
||||
let mut keys = vec![kCTFontFeatureSettingsAttribute];
|
||||
let mut values = vec![generate_feature_array(features)];
|
||||
if let Some(fallbacks) = fallbacks
|
||||
&& !fallbacks.fallback_list().is_empty()
|
||||
{
|
||||
keys.push(kCTFontCascadeListAttribute);
|
||||
values.push(generate_fallback_array(
|
||||
fallbacks,
|
||||
font.native_font().as_concrete_TypeRef(),
|
||||
));
|
||||
}
|
||||
let attrs = CFDictionaryCreate(
|
||||
kCFAllocatorDefault,
|
||||
keys.as_ptr() as _,
|
||||
values.as_ptr() as _,
|
||||
keys.len() as isize,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks,
|
||||
);
|
||||
let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs);
|
||||
CFRelease(attrs as _);
|
||||
let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
|
||||
let new_font = CTFontCreateCopyWithAttributes(
|
||||
font.native_font().as_concrete_TypeRef(),
|
||||
0.0,
|
||||
std::ptr::null(),
|
||||
new_descriptor.as_concrete_TypeRef(),
|
||||
);
|
||||
let new_font = CTFont::wrap_under_create_rule(new_font);
|
||||
*font = font_kit::font::Font::from_native_font(&new_font);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_feature_array(features: &FontFeatures) -> CFMutableArrayRef {
|
||||
unsafe {
|
||||
let feature_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
|
||||
for (tag, value) in features.tag_value_list() {
|
||||
let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue];
|
||||
let values = [
|
||||
CFString::new(tag).as_CFTypeRef(),
|
||||
CFNumber::from(*value as i32).as_CFTypeRef(),
|
||||
];
|
||||
let dict = CFDictionaryCreate(
|
||||
kCFAllocatorDefault,
|
||||
&keys as *const _ as _,
|
||||
&values as *const _ as _,
|
||||
2,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks,
|
||||
);
|
||||
values.into_iter().for_each(|value| CFRelease(value));
|
||||
CFArrayAppendValue(feature_array, dict as _);
|
||||
CFRelease(dict as _);
|
||||
}
|
||||
feature_array
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_fallback_array(fallbacks: &FontFallbacks, font_ref: CTFontRef) -> CFMutableArrayRef {
|
||||
unsafe {
|
||||
let fallback_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
|
||||
for user_fallback in fallbacks.fallback_list() {
|
||||
let name = CFString::from(user_fallback.as_str());
|
||||
let fallback_desc =
|
||||
CTFontDescriptorCreateWithNameAndSize(name.as_concrete_TypeRef(), 0.0);
|
||||
CFArrayAppendValue(fallback_array, fallback_desc as _);
|
||||
CFRelease(fallback_desc as _);
|
||||
}
|
||||
append_system_fallbacks(fallback_array, font_ref);
|
||||
fallback_array
|
||||
}
|
||||
}
|
||||
|
||||
fn append_system_fallbacks(fallback_array: CFMutableArrayRef, font_ref: CTFontRef) {
|
||||
unsafe {
|
||||
let preferred_languages: CFArray<CFString> =
|
||||
CFArray::wrap_under_create_rule(CFLocaleCopyPreferredLanguages());
|
||||
|
||||
let default_fallbacks = CTFontCopyDefaultCascadeListForLanguages(
|
||||
font_ref,
|
||||
preferred_languages.as_concrete_TypeRef(),
|
||||
);
|
||||
let default_fallbacks: CFArray<CTFontDescriptor> =
|
||||
CFArray::wrap_under_create_rule(default_fallbacks);
|
||||
|
||||
default_fallbacks
|
||||
.iter()
|
||||
.filter(|desc| desc.font_path().is_some())
|
||||
.map(|desc| {
|
||||
CFArrayAppendValue(fallback_array, desc.as_concrete_TypeRef() as _);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "CoreText", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
static kCTFontOpenTypeFeatureTag: CFStringRef;
|
||||
static kCTFontOpenTypeFeatureValue: CFStringRef;
|
||||
|
||||
fn CTFontCreateCopyWithAttributes(
|
||||
font: CTFontRef,
|
||||
size: CGFloat,
|
||||
matrix: *const CGAffineTransform,
|
||||
attributes: CTFontDescriptorRef,
|
||||
) -> CTFontRef;
|
||||
fn CTFontCopyDefaultCascadeListForLanguages(
|
||||
font: CTFontRef,
|
||||
languagePrefList: CFArrayRef,
|
||||
) -> CFArrayRef;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user