Vendor gpui-pre-windows 0.3.8 unmodified (crates.io source)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-10 20:12:28 -04:00
1 parent 3b23a92b3c
commit 09479941ff
28 files changed
+20112

No files matched your search

+4882
View File
File diff suppressed because it is too large. Load diff
+243
View File
@@ -0,0 +1,243 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "gpui-pre-windows"
version = "0.3.8"
build = "build.rs"
publish = true
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Zed's `gpui_windows` crate (gpui-pre snapshot of zed@279fe07)"
readme = false
license = "Apache-2.0"
repository = "https://github.com/zed-industries/zed"
resolver = "2"
[package.metadata.cargo-shear]
ignored = ["scap"]
[package.metadata.gpui-pre]
zed-crate = "gpui_windows"
zed-version = "0.1.0"
zed-rev = "279fe070bb389b79652e52065b2f001edcc0b11b"
[features]
default = ["gpui/default"]
screen-capture = [
"gpui/screen-capture",
"scap",
]
test-support = ["gpui/test-support"]
[lib]
name = "gpui_windows"
path = "src/gpui_windows.rs"
doctest = false
[dependencies.gpui]
version = "=0.3.8"
default-features = false
package = "gpui-pre"
[target.'cfg(target_os = "windows")'.dependencies.accesskit]
version = "0.24.0"
features = ["enumn"]
[target.'cfg(target_os = "windows")'.dependencies.accesskit_windows]
version = "0.34"
[target.'cfg(target_os = "windows")'.dependencies.anyhow]
version = "1.0.86"
[target.'cfg(target_os = "windows")'.dependencies.collections]
version = "=0.3.8"
package = "gpui-pre-collections"
[target.'cfg(target_os = "windows")'.dependencies.dunce]
version = "1.0"
[target.'cfg(target_os = "windows")'.dependencies.etagere]
version = "0.2"
[target.'cfg(target_os = "windows")'.dependencies.futures]
version = "0.3.34"
[target.'cfg(target_os = "windows")'.dependencies.gpui_util]
version = "=0.3.8"
package = "gpui-pre-util"
[target.'cfg(target_os = "windows")'.dependencies.image]
version = "0.25.1"
features = [
"bmp",
"dds",
"exr",
"ff",
"gif",
"hdr",
"ico",
"jpeg",
"png",
"pnm",
"qoi",
"rayon",
"tga",
"tiff",
"webp",
]
default-features = false
[target.'cfg(target_os = "windows")'.dependencies.itertools]
version = "0.14.0"
[target.'cfg(target_os = "windows")'.dependencies.log]
version = "0.4.16"
features = [
"kv_unstable_serde",
"serde",
]
[target.'cfg(target_os = "windows")'.dependencies.parking_lot]
version = "0.12.1"
[target.'cfg(target_os = "windows")'.dependencies.rand]
version = "0.9"
[target.'cfg(target_os = "windows")'.dependencies.raw-window-handle]
version = "0.6"
[target.'cfg(target_os = "windows")'.dependencies.scap]
version = "0.0.8-zed"
optional = true
default-features = false
package = "zed-scap"
[target.'cfg(target_os = "windows")'.dependencies.scheduler]
version = "=0.3.8"
package = "gpui-pre-scheduler"
[target.'cfg(target_os = "windows")'.dependencies.smallvec]
version = "1.6"
features = [
"union",
"const_new",
]
[target.'cfg(target_os = "windows")'.dependencies.uuid]
version = "1.1.2"
features = [
"v4",
"v5",
"v7",
"serde",
]
[target.'cfg(target_os = "windows")'.dependencies.windows]
version = "0.62"
features = [
"Data_Xml_Dom",
"Foundation_Numerics",
"Globalization_DateTimeFormatting",
"Storage_Search",
"Storage_Streams",
"System_Threading",
"UI_Notifications",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D_Fxc",
"Win32_Graphics_DirectComposition",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_DirectManipulation",
"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_Storage_Packaging_Appx",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_Console",
"Win32_System_Diagnostics_Debug",
"Win32_System_DataExchange",
"Win32_System_IO",
"Win32_System_JobObjects",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_Performance",
"Win32_System_Pipes",
"Win32_System_Power",
"Win32_System_RestartManager",
"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_Input_Pointer",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem",
"Win32_UI_WindowsAndMessaging",
"Win32_Media",
"Win32_System_StationsAndDesktops",
]
[target.'cfg(target_os = "windows")'.dependencies.windows-core]
version = "0.62"
[target.'cfg(target_os = "windows")'.dependencies.windows-numerics]
version = "0.3"
[target.'cfg(target_os = "windows")'.dependencies.windows-registry]
version = "0.6.0"
[target.'cfg(target_os = "windows")'.build-dependencies.windows-registry]
version = "0.6.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
+59
View File
@@ -0,0 +1,59 @@
[package]
name = "gpui-pre-windows"
version = "0.3.8"
edition = { workspace = true }
publish = true
license = "Apache-2.0"
description = "Zed's `gpui_windows` crate (gpui-pre snapshot of zed@279fe07)"
repository = "https://github.com/zed-industries/zed"
[package.metadata.cargo-shear]
ignored = ["scap"]
[package.metadata.gpui-pre]
zed-crate = "gpui_windows"
zed-version = "0.1.0"
zed-rev = "279fe070bb389b79652e52065b2f001edcc0b11b"
[lib]
path = "src/gpui_windows.rs"
doctest = false
name = "gpui_windows"
[features]
default = ["gpui/default"]
test-support = ["gpui/test-support"]
screen-capture = ["gpui/screen-capture", "scap"]
[dependencies]
gpui = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
accesskit = { workspace = true }
accesskit_windows = { workspace = true }
anyhow = { workspace = true }
collections = { workspace = true }
dunce = { workspace = true }
etagere = { workspace = true }
futures = { workspace = true }
gpui_util = { workspace = true }
image = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
parking_lot = { workspace = true }
rand = { workspace = true }
raw-window-handle = { workspace = true }
scheduler = { workspace = true }
smallvec = { workspace = true }
uuid = { workspace = true }
windows = { workspace = true, features = ["Win32_System_StationsAndDesktops"] }
windows-core = { workspace = true }
windows-numerics = "0.3"
windows-registry = { workspace = true }
scap = { workspace = true, optional = true }
[target.'cfg(target_os = "windows")'.build-dependencies]
windows-registry = { workspace = true }
[lints]
workspace = true
+222
View File
@@ -0,0 +1,222 @@
Copyright 2022 - 2025 Zed Industries, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+242
View File
@@ -0,0 +1,242 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
fn main() {
#[cfg(target_os = "windows")]
{
// Compile HLSL shaders
#[cfg(not(debug_assertions))]
compile_shaders();
}
}
#[cfg(all(target_os = "windows", not(debug_assertions)))]
mod shader_compilation {
use std::{
fs,
io::Write,
path::{Path, PathBuf},
process::{self, Command},
};
pub fn compile_shaders() {
let shader_path =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("src/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",
"subpixel_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/color_text_raster.hlsl");
compile_shader_for_module(
"emoji_rasterization",
&out_dir,
&fxc_path,
shader_path.to_str().unwrap(),
&rust_binding_path,
);
}
}
/// Locate `binary` in the newest installed Windows SDK.
pub fn find_latest_windows_sdk_binary(
binary: &str,
) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
let key = windows_registry::LOCAL_MACHINE
.open("SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0")?;
let install_folder: String = key.get_string("InstallationFolder")?; // "C:\Program Files (x86)\Windows Kits\10\"
let install_folder_bin = Path::new(&install_folder).join("bin");
let mut versions: Vec<_> = std::fs::read_dir(&install_folder_bin)?
.flatten()
.filter(|entry| entry.path().is_dir())
.filter_map(|entry| entry.file_name().into_string().ok())
.collect();
versions.sort_by_key(|s| {
s.split('.')
.filter_map(|p| p.parse().ok())
.collect::<Vec<u32>>()
});
let arch = match std::env::consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
_ => Err(format!(
"Unsupported architecture: {}",
std::env::consts::ARCH
))?,
};
if let Some(highest_version) = versions.last() {
return Ok(Some(
install_folder_bin
.join(highest_version)
.join(arch)
.join(binary),
));
}
Ok(None)
}
/// 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();
}
if let Ok(Some(path)) = find_latest_windows_sdk_binary("fxc.exe") {
return path.to_string_lossy().into_owned();
}
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");
}
}
#[cfg(all(target_os = "windows", not(debug_assertions)))]
use shader_compilation::compile_shaders;
+49
View File
@@ -0,0 +1,49 @@
// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.hlsl
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
float color_brightness(float3 color) {
// REC. 601 luminance coefficients for perceived brightness
return dot(color, float3(0.30f, 0.59f, 0.11f));
}
float light_on_dark_contrast(float enhancedContrast, float3 color) {
float brightness = color_brightness(color);
float multiplier = saturate(4.0f * (0.75f - brightness));
return enhancedContrast * multiplier;
}
float enhance_contrast(float alpha, float k) {
return alpha * (k + 1.0f) / (alpha * k + 1.0f);
}
float3 enhance_contrast3(float3 alpha, float k) {
return alpha * (k + 1.0f) / (alpha * k + 1.0f);
}
float apply_alpha_correction(float a, float b, float4 g) {
float brightness_adjustment = g.x * b + g.y;
float correction = brightness_adjustment * a + (g.z * b + g.w);
return a + a * (1.0f - a) * correction;
}
float3 apply_alpha_correction3(float3 a, float3 b, float4 g) {
float3 brightness_adjustment = g.x * b + g.y;
float3 correction = brightness_adjustment * a + (g.z * b + g.w);
return a + a * (1.0f - a) * correction;
}
float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
float brightness = color_brightness(color);
float contrasted = enhance_contrast(sample, enhanced_contrast);
return apply_alpha_correction(contrasted, brightness, gamma_ratios);
}
float3 apply_contrast_and_gamma_correction3(float3 sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
float3 contrasted = enhance_contrast3(sample, enhanced_contrast);
return apply_alpha_correction3(contrasted, color, gamma_ratios);
}
+394
View File
@@ -0,0 +1,394 @@
use std::{borrow::Cow, sync::LazyLock};
use anyhow::Result;
use collections::FxHashMap;
use itertools::Itertools;
use windows::Win32::{
Foundation::{HANDLE, HGLOBAL},
System::{
DataExchange::{
CloseClipboard, CountClipboardFormats, EmptyClipboard, EnumClipboardFormats,
GetClipboardData, GetClipboardFormatNameW, OpenClipboard, RegisterClipboardFormatW,
SetClipboardData,
},
Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock},
Ole::{CF_DIB, CF_HDROP, CF_UNICODETEXT},
},
UI::Shell::{DragQueryFileW, HDROP},
};
use windows::core::{Owned, PCWSTR};
use gpui::{
ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,
};
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
static CLIPBOARD_HASH_FORMAT: LazyLock<u32> =
LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal text hash")));
static CLIPBOARD_METADATA_FORMAT: LazyLock<u32> =
LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal metadata")));
static CLIPBOARD_SVG_FORMAT: LazyLock<u32> =
LazyLock::new(|| register_clipboard_format(windows::core::w!("image/svg+xml")));
static CLIPBOARD_GIF_FORMAT: LazyLock<u32> =
LazyLock::new(|| register_clipboard_format(windows::core::w!("GIF")));
static CLIPBOARD_PNG_FORMAT: LazyLock<u32> =
LazyLock::new(|| register_clipboard_format(windows::core::w!("PNG")));
static CLIPBOARD_JPG_FORMAT: LazyLock<u32> =
LazyLock::new(|| register_clipboard_format(windows::core::w!("JFIF")));
static IMAGE_FORMATS_MAP: LazyLock<FxHashMap<u32, ImageFormat>> = LazyLock::new(|| {
let mut map = FxHashMap::default();
map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
map.insert(*CLIPBOARD_GIF_FORMAT, ImageFormat::Gif);
map.insert(*CLIPBOARD_JPG_FORMAT, ImageFormat::Jpeg);
map.insert(*CLIPBOARD_SVG_FORMAT, ImageFormat::Svg);
map
});
fn register_clipboard_format(format: PCWSTR) -> u32 {
let ret = unsafe { RegisterClipboardFormatW(format) };
if ret == 0 {
panic!(
"Error when registering clipboard format: {}",
std::io::Error::last_os_error()
);
}
log::debug!(
"Registered clipboard format {} as {}",
unsafe { format.display() },
ret
);
ret
}
fn get_clipboard_data(format: u32) -> Option<LockedGlobal> {
let global = HGLOBAL(unsafe { GetClipboardData(format).ok() }?.0);
LockedGlobal::lock(global)
}
pub(crate) fn write_to_clipboard(item: ClipboardItem) {
let Some(_clip) = ClipboardGuard::open() else {
return;
};
let result: Result<()> = (|| {
unsafe { EmptyClipboard()? };
for entry in item.entries() {
match entry {
ClipboardEntry::String(string) => write_string(string)?,
ClipboardEntry::Image(image) => write_image(image)?,
ClipboardEntry::ExternalPaths(_) => {}
}
}
Ok(())
})();
if let Err(e) = result {
log::error!("Failed to write to clipboard: {e}");
}
}
pub(crate) fn read_from_clipboard() -> Option<ClipboardItem> {
let _clip = ClipboardGuard::open()?;
let mut entries = Vec::new();
let mut have_text = false;
let mut have_image = false;
let mut have_files = false;
let count = unsafe { CountClipboardFormats() };
let mut format = 0;
for _ in 0..count {
format = unsafe { EnumClipboardFormats(format) };
if !have_text && format == CF_UNICODETEXT.0 as u32 {
if let Some(entry) = read_string() {
entries.push(entry);
have_text = true;
}
} else if !have_image && is_image_format(format) {
if let Some(entry) = read_image(format) {
entries.push(entry);
have_image = true;
}
} else if !have_files && format == CF_HDROP.0 as u32 {
if let Some(entry) = read_files() {
entries.push(entry);
have_files = true;
}
}
}
if entries.is_empty() {
log_unsupported_clipboard_formats();
return None;
}
Some(ClipboardItem { entries })
}
pub(crate) fn with_file_names<F>(hdrop: HDROP, mut f: F)
where
F: FnMut(String),
{
let file_count = unsafe { DragQueryFileW(hdrop, DRAGDROP_GET_FILES_COUNT, None) };
for file_index in 0..file_count {
let filename_length = unsafe { DragQueryFileW(hdrop, file_index, None) } as usize;
let mut buffer = vec![0u16; filename_length + 1];
let ret = unsafe { DragQueryFileW(hdrop, file_index, Some(buffer.as_mut_slice())) };
if ret == 0 {
log::error!("unable to read file name of dragged file");
continue;
}
match String::from_utf16(&buffer[0..filename_length]) {
Ok(file_name) => f(file_name),
Err(e) => log::error!("dragged file name is not UTF-16: {}", e),
}
}
}
fn set_clipboard_bytes<T>(data: &[T], format: u32) -> Result<()> {
unsafe {
let global = Owned::new(GlobalAlloc(GMEM_MOVEABLE, std::mem::size_of_val(data))?);
let ptr = GlobalLock(*global);
anyhow::ensure!(!ptr.is_null(), "GlobalLock returned null");
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr as _, data.len());
GlobalUnlock(*global).ok();
SetClipboardData(format, Some(HANDLE(global.0)))?;
// SetClipboardData succeeded — the system now owns the memory.
std::mem::forget(global);
}
Ok(())
}
fn get_clipboard_string(format: u32) -> Option<String> {
let locked = get_clipboard_data(format)?;
let bytes = locked.as_bytes();
let words_len = bytes.len() / std::mem::size_of::<u16>();
if words_len == 0 {
return Some(String::new());
}
let slice = unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const u16, words_len) };
let actual_len = slice.iter().position(|&c| c == 0).unwrap_or(words_len);
Some(String::from_utf16_lossy(&slice[..actual_len]))
}
fn is_image_format(format: u32) -> bool {
IMAGE_FORMATS_MAP.contains_key(&format) || format == CF_DIB.0 as u32
}
fn write_string(item: &ClipboardString) -> Result<()> {
// CF_UNICODETEXT is null-terminated, so replace embedded NUL characters with spaces.
let text = if item.text.contains('\0') {
Cow::Owned(item.text.replace('\0', " "))
} else {
Cow::Borrowed(item.text.as_str())
};
let wide: Vec<u16> = text.encode_utf16().chain(Some(0)).collect_vec();
set_clipboard_bytes(&wide, CF_UNICODETEXT.0 as u32)?;
if let Some(metadata) = item.metadata.as_ref() {
let hash_bytes = ClipboardString::text_hash(&text).to_ne_bytes();
set_clipboard_bytes(&hash_bytes, *CLIPBOARD_HASH_FORMAT)?;
let wide: Vec<u16> = metadata.encode_utf16().chain(Some(0)).collect_vec();
set_clipboard_bytes(&wide, *CLIPBOARD_METADATA_FORMAT)?;
}
Ok(())
}
fn write_image(item: &Image) -> Result<()> {
let native_format = match item.format {
ImageFormat::Svg => Some(*CLIPBOARD_SVG_FORMAT),
ImageFormat::Gif => Some(*CLIPBOARD_GIF_FORMAT),
ImageFormat::Png => Some(*CLIPBOARD_PNG_FORMAT),
ImageFormat::Jpeg => Some(*CLIPBOARD_JPG_FORMAT),
_ => None,
};
if let Some(format) = native_format {
set_clipboard_bytes(item.bytes(), format)?;
}
// Also provide a PNG copy for broad compatibility.
// SVG can't be rasterized by the image crate, so skip it.
if item.format != ImageFormat::Svg && native_format != Some(*CLIPBOARD_PNG_FORMAT) {
if let Some(png_bytes) = convert_to_png(item.bytes(), item.format) {
set_clipboard_bytes(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
}
}
Ok(())
}
fn convert_to_png(bytes: &[u8], format: ImageFormat) -> Option<Vec<u8>> {
let img_format = gpui_to_image_format(format)?;
let image = image::load_from_memory_with_format(bytes, img_format)
.map_err(|e| log::warn!("Failed to decode image for PNG conversion: {e}"))
.ok()?;
let mut buf = Vec::new();
image
.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
.map_err(|e| log::warn!("Failed to encode PNG: {e}"))
.ok()?;
Some(buf)
}
fn read_string() -> Option<ClipboardEntry> {
let text = get_clipboard_string(CF_UNICODETEXT.0 as u32)?;
let metadata = read_clipboard_metadata(&text);
Some(ClipboardEntry::String(ClipboardString { text, metadata }))
}
fn read_clipboard_metadata(text: &str) -> Option<String> {
let locked = get_clipboard_data(*CLIPBOARD_HASH_FORMAT)?;
let hash_bytes: [u8; 8] = locked.as_bytes().get(..8)?.try_into().ok()?;
let hash = u64::from_ne_bytes(hash_bytes);
if hash != ClipboardString::text_hash(text) {
return None;
}
get_clipboard_string(*CLIPBOARD_METADATA_FORMAT)
}
fn read_image(format: u32) -> Option<ClipboardEntry> {
let locked = get_clipboard_data(format)?;
let (bytes, image_format) = if format == CF_DIB.0 as u32 {
(convert_dib_to_bmp(locked.as_bytes())?, ImageFormat::Bmp)
} else {
let image_format = *IMAGE_FORMATS_MAP.get(&format)?;
(locked.as_bytes().to_vec(), image_format)
};
let id = hash(&bytes);
Some(ClipboardEntry::Image(Image {
format: image_format,
bytes,
id,
}))
}
fn read_files() -> Option<ClipboardEntry> {
let locked = get_clipboard_data(CF_HDROP.0 as u32)?;
let hdrop = HDROP(locked.ptr as *mut _);
let mut filenames = Vec::new();
with_file_names(hdrop, |name| filenames.push(std::path::PathBuf::from(name)));
Some(ClipboardEntry::ExternalPaths(ExternalPaths(
filenames.into(),
)))
}
/// DIB is BMP without the 14-byte BITMAPFILEHEADER. Prepend one.
fn convert_dib_to_bmp(dib: &[u8]) -> Option<Vec<u8>> {
if dib.len() < 40 {
return None;
}
let header_size = u32::from_le_bytes(dib[0..4].try_into().ok()?);
let bit_count = u16::from_le_bytes(dib[14..16].try_into().ok()?);
let compression = u32::from_le_bytes(dib[16..20].try_into().ok()?);
let color_table_size = if bit_count <= 8 {
let colors_used = u32::from_le_bytes(dib[32..36].try_into().ok()?);
(if colors_used == 0 {
1u32 << bit_count
} else {
colors_used
}) * 4
} else if compression == 3 {
12 // BI_BITFIELDS
} else {
0
};
let pixel_offset = 14 + header_size + color_table_size;
let file_size = 14 + dib.len() as u32;
let mut bmp = Vec::with_capacity(file_size as usize);
bmp.extend_from_slice(b"BM");
bmp.extend_from_slice(&file_size.to_le_bytes());
bmp.extend_from_slice(&[0u8; 4]); // reserved
bmp.extend_from_slice(&pixel_offset.to_le_bytes());
bmp.extend_from_slice(dib);
Some(bmp)
}
fn log_unsupported_clipboard_formats() {
let count = unsafe { CountClipboardFormats() };
let mut format = 0;
for _ in 0..count {
format = unsafe { EnumClipboardFormats(format) };
let mut buffer = [0u16; 64];
unsafe { GetClipboardFormatNameW(format, &mut buffer) };
let format_name = String::from_utf16_lossy(&buffer);
log::warn!(
"Try to paste with unsupported clipboard format: {}, {}.",
format,
format_name
);
}
}
fn gpui_to_image_format(value: ImageFormat) -> Option<image::ImageFormat> {
match value {
ImageFormat::Png => Some(image::ImageFormat::Png),
ImageFormat::Jpeg => Some(image::ImageFormat::Jpeg),
ImageFormat::Webp => Some(image::ImageFormat::WebP),
ImageFormat::Gif => Some(image::ImageFormat::Gif),
ImageFormat::Bmp => Some(image::ImageFormat::Bmp),
ImageFormat::Tiff => Some(image::ImageFormat::Tiff),
other => {
log::warn!("No image crate equivalent for format: {other:?}");
None
}
}
}
struct ClipboardGuard;
impl ClipboardGuard {
fn open() -> Option<Self> {
match unsafe { OpenClipboard(None) } {
Ok(()) => Some(Self),
Err(e) => {
log::error!("Failed to open clipboard: {e}");
None
}
}
}
}
impl Drop for ClipboardGuard {
fn drop(&mut self) {
if let Err(e) = unsafe { CloseClipboard() } {
log::error!("Failed to close clipboard: {e}");
}
}
}
struct LockedGlobal {
global: HGLOBAL,
ptr: *const u8,
size: usize,
}
impl LockedGlobal {
fn lock(global: HGLOBAL) -> Option<Self> {
let size = unsafe { GlobalSize(global) };
let ptr = unsafe { GlobalLock(global) };
if ptr.is_null() {
return None;
}
Some(Self {
global,
ptr: ptr as *const u8,
size,
})
}
fn as_bytes(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
}
}
impl Drop for LockedGlobal {
fn drop(&mut self) {
unsafe { GlobalUnlock(self.global).ok() };
}
}
+44
View File
@@ -0,0 +1,44 @@
#include "alpha_correction.hlsl"
struct RasterVertexOutput {
float4 position : SV_Position;
float2 texcoord : TEXCOORD0;
};
RasterVertexOutput emoji_rasterization_vertex(uint vertexID : SV_VERTEXID)
{
RasterVertexOutput output;
output.texcoord = float2((vertexID << 1) & 2, vertexID & 2);
output.position = float4(output.texcoord * 2.0f - 1.0f, 0.0f, 1.0f);
output.position.y = -output.position.y;
return output;
}
struct PixelInput {
float4 position: SV_Position;
float2 texcoord : TEXCOORD0;
};
struct Bounds {
int2 origin;
int2 size;
};
Texture2D<float> t_layer : register(t0);
SamplerState s_layer : register(s0);
cbuffer GlyphLayerTextureParams : register(b0) {
Bounds bounds;
float4 run_color;
float4 gamma_ratios;
float grayscale_enhanced_contrast;
float3 _pad;
};
float4 emoji_rasterization_fragment(PixelInput input): SV_Target {
float sample = t_layer.Sample(s_layer, input.texcoord.xy).r;
float alpha_corrected = apply_contrast_and_gamma_correction(sample, run_color.rgb, grayscale_enhanced_contrast, gamma_ratios);
float alpha = alpha_corrected * run_color.a;
return float4(run_color.rgb * alpha, alpha);
}
+207
View File
@@ -0,0 +1,207 @@
use std::{path::PathBuf, sync::Arc};
use itertools::Itertools;
use smallvec::SmallVec;
use windows::{
Win32::{
Foundation::PROPERTYKEY,
Globalization::u_strlen,
System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance, StructuredStorage::PROPVARIANT},
UI::{
Controls::INFOTIPSIZE,
Shell::{
Common::{IObjectArray, IObjectCollection},
DestinationList, EnumerableObjectCollection, ICustomDestinationList, IShellLinkW,
PropertiesSystem::IPropertyStore,
ShellLink,
},
},
},
core::{GUID, HSTRING, Interface},
};
use gpui::{Action, MenuItem, SharedString};
pub(crate) struct JumpList {
pub(crate) dock_menus: Vec<DockMenuItem>,
pub(crate) recent_workspaces: Arc<[SmallVec<[PathBuf; 2]>]>,
}
impl JumpList {
pub(crate) fn new() -> Self {
Self {
dock_menus: Vec::default(),
recent_workspaces: Arc::default(),
}
}
}
pub(crate) struct DockMenuItem {
pub(crate) name: SharedString,
pub(crate) description: SharedString,
pub(crate) action: Box<dyn Action>,
}
impl DockMenuItem {
pub(crate) fn new(item: MenuItem) -> anyhow::Result<Self> {
match item {
MenuItem::Action { name, action, .. } => Ok(Self {
name: name.clone(),
description: if name == "New Window" {
"Opens a new window".into()
} else {
name
},
action,
}),
_ => anyhow::bail!("Only `MenuItem::Action` is supported for dock menu on Windows."),
}
}
}
// This code is based on the example from Microsoft:
// https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/RecipePropertyHandler/RecipePropertyHandler.cpp
pub(crate) fn update_jump_list(
recent_workspaces: &[SmallVec<[PathBuf; 2]>],
dock_menus: &[(SharedString, SharedString)],
) -> anyhow::Result<Vec<SmallVec<[PathBuf; 2]>>> {
let (list, removed) = create_destination_list()?;
add_recent_folders(&list, recent_workspaces, removed.as_ref())?;
add_dock_menu(&list, dock_menus)?;
unsafe { list.CommitList() }?;
Ok(removed)
}
// Copied from:
// https://github.com/microsoft/windows-rs/blob/0fc3c2e5a13d4316d242bdeb0a52af611eba8bd4/crates/libs/windows/src/Windows/Win32/Storage/EnhancedStorage/mod.rs#L1881
const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY {
fmtid: GUID::from_u128(0xf29f85e0_4ff9_1068_ab91_08002b27b3d9),
pid: 2,
};
fn create_destination_list() -> anyhow::Result<(ICustomDestinationList, Vec<SmallVec<[PathBuf; 2]>>)>
{
let list: ICustomDestinationList =
unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER) }?;
let mut slots = 0;
let user_removed: IObjectArray = unsafe { list.BeginList(&mut slots) }?;
let count = unsafe { user_removed.GetCount() }?;
if count == 0 {
return Ok((list, Vec::new()));
}
let mut removed = Vec::with_capacity(count as usize);
for i in 0..count {
let shell_link: IShellLinkW = unsafe { user_removed.GetAt(i)? };
let description = {
// INFOTIPSIZE is the maximum size of the buffer
// see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinkw-getdescription
let mut buffer = [0u16; INFOTIPSIZE as usize];
unsafe { shell_link.GetDescription(&mut buffer)? };
let len = unsafe { u_strlen(buffer.as_ptr()) };
String::from_utf16_lossy(&buffer[..len as usize])
};
let args = description.split('\n').map(PathBuf::from).collect();
removed.push(args);
}
Ok((list, removed))
}
fn add_dock_menu(
list: &ICustomDestinationList,
dock_menus: &[(SharedString, SharedString)],
) -> anyhow::Result<()> {
unsafe {
let tasks: IObjectCollection =
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
for (idx, (name, description)) in dock_menus.iter().enumerate() {
let argument = HSTRING::from(format!("--dock-action {}", idx));
let description = HSTRING::from(description.as_str());
let display = name.as_str();
let task = create_shell_link(argument, description, None, display)?;
tasks.AddObject(&task)?;
}
list.AddUserTasks(&tasks)?;
Ok(())
}
}
fn add_recent_folders(
list: &ICustomDestinationList,
entries: &[SmallVec<[PathBuf; 2]>],
removed: &Vec<SmallVec<[PathBuf; 2]>>,
) -> anyhow::Result<()> {
unsafe {
let tasks: IObjectCollection =
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
for folder_path in entries.iter().filter(|path| !removed.contains(path)) {
let argument = HSTRING::from(
folder_path
.iter()
.map(|path| format!("\"{}\"", path.display()))
.join(" "),
);
let description = HSTRING::from(
folder_path
.iter()
.map(|path| path.to_string_lossy())
.collect::<Vec<_>>()
.join("\n"),
);
// simulate folder icon
// https://github.com/microsoft/vscode/blob/7a5dc239516a8953105da34f84bae152421a8886/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts#L380
let icon = HSTRING::from("explorer.exe");
let display = folder_path
.iter()
.map(|p| {
p.file_name()
.map(|name| name.to_string_lossy())
.unwrap_or_else(|| p.to_string_lossy())
})
.join(", ");
tasks.AddObject(&create_shell_link(
argument,
description,
Some(icon),
&display,
)?)?;
}
if tasks.GetCount().unwrap_or(0) > 0 {
list.AppendCategory(&HSTRING::from("Recent Folders"), &tasks)?;
}
Ok(())
}
}
fn create_shell_link(
argument: HSTRING,
description: HSTRING,
icon: Option<HSTRING>,
display: &str,
) -> anyhow::Result<IShellLinkW> {
unsafe {
let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
let exe_path = HSTRING::from(std::env::current_exe()?.as_os_str());
link.SetPath(&exe_path)?;
link.SetArguments(&argument)?;
link.SetDescription(&description)?;
if let Some(icon) = icon {
link.SetIconLocation(&icon, 0)?;
}
let store: IPropertyStore = link.cast()?;
let title = PROPVARIANT::from(display);
store.SetValue(&PKEY_TITLE, &title)?;
store.Commit()?;
Ok(link)
}
}
+340
View File
@@ -0,0 +1,340 @@
use std::{
cell::RefCell,
rc::Rc,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use anyhow::{Context as _, Result, anyhow};
use futures::{
channel::oneshot,
future::{Either, select},
lock::Mutex,
};
use gpui::ForegroundExecutor;
use gpui_util::ResultExt;
use windows::{
Win32::{
Foundation::*,
System::{Com::*, Threading::GetCurrentThreadId},
UI::{
Controls::{TASKDIALOG_NOTIFICATIONS, TDN_BUTTON_CLICKED},
WindowsAndMessaging::*,
},
},
core::{BOOL, HRESULT, w},
};
use crate::SafeHwnd;
pub(crate) struct DialogOwner {
hwnd: HWND,
closed: Arc<AtomicBool>,
serialization: Mutex<()>,
}
impl DialogOwner {
pub(crate) fn new(hwnd: HWND) -> Rc<Self> {
Rc::new(Self {
hwnd,
closed: Arc::new(AtomicBool::new(false)),
serialization: Mutex::new(()),
})
}
pub(crate) fn close(&self) {
self.closed.store(true, Ordering::Release);
}
pub(crate) async fn when_idle(&self) {
// After close(), queued requests cannot start a native dialog. Only the
// active dialog can still use the HWND, and it holds this lock until exit.
let _guard = self.serialization.lock().await;
}
}
#[derive(Clone, Default)]
struct Cancellation {
requested: Arc<AtomicBool>,
owner_closed: Option<Arc<AtomicBool>>,
}
impl Cancellation {
fn new(owner: Option<&DialogOwner>) -> Self {
Self {
owner_closed: owner.map(|owner| owner.closed.clone()),
..Self::default()
}
}
fn is_requested(&self) -> bool {
self.requested.load(Ordering::Acquire)
|| self
.owner_closed
.as_ref()
.is_some_and(|closed| closed.load(Ordering::Acquire))
}
}
/// Native modal loops may synchronously message their owner, so the foreground
/// thread must keep pumping messages rather than waiting for the dialog thread.
/// Holding the owner's lock through native completion also allows window
/// destruction to wait for the dialog without blocking the foreground thread.
pub(crate) fn show_dialog<T: Send + 'static>(
owner: Option<Rc<DialogOwner>>,
executor: &ForegroundExecutor,
dialog: impl FnOnce(HWND) -> Result<T> + Send + 'static,
) -> oneshot::Receiver<Result<T>> {
let (mut sender, receiver) = oneshot::channel();
let cancellation = Cancellation::new(owner.as_deref());
if cancellation.is_requested() {
sender.send(Err(anyhow!("dialog owner has closed"))).ok();
return receiver;
}
let hwnd = owner.as_ref().map(|owner| SafeHwnd::from(owner.hwnd));
executor
.spawn(async move {
// Let Windows manage enabling and activating the owner. Overlapping
// modal loops could otherwise re-enable it while another dialog is open.
let serialization = if let Some(owner) = owner.as_ref() {
match select(owner.serialization.lock(), sender.cancellation()).await {
Either::Left((guard, _)) => Some(guard),
Either::Right(((), _)) => return,
}
} else {
None
};
if sender.is_canceled() || cancellation.is_requested() {
return;
}
let (result_sender, mut result_receiver) = oneshot::channel();
let thread = std::thread::Builder::new()
.name("windows-native-dialog".into())
.spawn({
let cancellation = cancellation.clone();
move || {
let result = (|| {
let _apartment = Apartment::new()?;
let _timer = CancellationTimer::new(cancellation.clone())?;
if cancellation.is_requested() {
return Err(anyhow!("dialog cancelled"));
}
let result = dialog(hwnd.map(|hwnd| hwnd.as_raw()).unwrap_or_default());
if cancellation.is_requested() {
return Err(anyhow!("dialog cancelled"));
}
result
})();
result_sender.send(result).ok();
}
});
if let Err(error) = thread {
drop(serialization);
sender.send(Err(error.into())).ok();
return;
}
let result = match select(&mut result_receiver, sender.cancellation()).await {
Either::Left((result, _)) => result,
Either::Right(((), _)) => {
cancellation.requested.store(true, Ordering::Release);
result_receiver.await
}
};
// Neither launch another dialog nor destroy the owner until the
// native loop exits, even if the caller no longer wants its result.
drop(serialization);
sender
.send(result.unwrap_or_else(|error| Err(error.into())))
.ok();
})
.detach();
receiver
}
struct Apartment;
impl Apartment {
fn new() -> Result<Self> {
unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok()? };
Ok(Self)
}
}
impl Drop for Apartment {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
thread_local! {
static CANCELLATION: RefCell<Option<Cancellation>> = const { RefCell::new(None) };
}
struct CancellationTimer(usize);
impl CancellationTimer {
fn new(cancellation: Cancellation) -> Result<Self> {
let timer = unsafe { SetTimer(None, 0, 100, Some(cancel_dialog)) };
if timer == 0 {
return Err(windows::core::Error::from_thread())
.context("starting native dialog cancellation timer");
}
CANCELLATION.with(|slot| slot.replace(Some(cancellation)));
Ok(Self(timer))
}
}
impl Drop for CancellationTimer {
fn drop(&mut self) {
CANCELLATION.with(|slot| slot.take());
unsafe { KillTimer(None, self.0).log_err() };
}
}
unsafe extern "system" fn cancel_dialog(_: HWND, _: u32, _: usize, _: u32) {
if cancellation_requested() {
unsafe {
EnumThreadWindows(GetCurrentThreadId(), Some(close_dialog), LPARAM(0))
.ok()
.log_err();
};
}
}
fn cancellation_requested() -> bool {
CANCELLATION.with(|slot| {
slot.borrow()
.as_ref()
.is_some_and(Cancellation::is_requested)
})
}
pub(crate) unsafe extern "system" fn task_dialog_callback(
_: HWND,
notification: TASKDIALOG_NOTIFICATIONS,
button: WPARAM,
_: LPARAM,
has_cancel_button: isize,
) -> HRESULT {
// WM_CLOSE requires TDF_ALLOW_DIALOG_CANCELLATION, which also enables Esc/X.
// Only allow those user gestures when the prompt offered a Cancel button.
if notification == TDN_BUTTON_CLICKED
&& button.0 == IDCANCEL.0 as usize
&& has_cancel_button == 0
&& !cancellation_requested()
{
S_FALSE
} else {
S_OK
}
}
unsafe extern "system" fn close_dialog(hwnd: HWND, _: LPARAM) -> BOOL {
// Shell dialogs can open further modal dialogs. Close only dialog windows
// on this dedicated thread, not hidden COM infrastructure windows.
let mut class_name = [0u16; 32];
let length = unsafe { GetClassNameW(hwnd, &mut class_name) } as usize;
let dialog_class = w!("#32770");
if class_name.get(..length) == Some(unsafe { dialog_class.as_wide() }) {
unsafe { PostMessageW(Some(hwnd), WM_CLOSE, WPARAM(0), LPARAM(0)).log_err() };
}
TRUE
}
#[cfg(test)]
mod tests {
use super::*;
use futures::FutureExt as _;
#[test]
fn task_dialog_cancellation_requires_cancel_button_or_internal_request() {
let cancellation = Cancellation::default();
CANCELLATION.with(|slot| slot.replace(Some(cancellation.clone())));
for internally_cancelled in [false, true] {
cancellation
.requested
.store(internally_cancelled, Ordering::Release);
for has_cancel_button in [false, true] {
for button in [IDCANCEL.0, IDOK.0, -1] {
let result = unsafe {
task_dialog_callback(
HWND::default(),
TDN_BUTTON_CLICKED,
WPARAM(button as usize),
LPARAM(0),
has_cancel_button as isize,
)
};
let should_veto =
button == IDCANCEL.0 && !has_cancel_button && !internally_cancelled;
assert_eq!(result, if should_veto { S_FALSE } else { S_OK });
}
}
}
CANCELLATION.with(|slot| slot.take());
}
#[test]
fn closing_owner_cancels_every_outstanding_dialog() {
let owner = DialogOwner::new(HWND::default());
let first = Cancellation::new(Some(&owner));
let second = Cancellation::new(Some(&owner));
assert!(!first.is_requested());
assert!(!second.is_requested());
owner.close();
owner.close();
assert!(first.is_requested());
assert!(second.is_requested());
assert!(Cancellation::new(Some(&owner)).is_requested());
}
#[test]
fn closed_owner_waits_for_active_dialog_but_queued_requests_stay_cancelled() {
let owner = DialogOwner::new(HWND::default());
let active = owner.serialization.try_lock().expect("first dialog");
let queued = Cancellation::new(Some(&owner));
owner.close();
assert!(owner.when_idle().now_or_never().is_none());
drop(active);
assert!(owner.when_idle().now_or_never().is_some());
let _guard = owner.serialization.try_lock().expect("queued request");
assert!(queued.is_requested());
}
#[test]
fn cancelling_one_request_does_not_cancel_its_owner_or_other_requests() {
let owner = DialogOwner::new(HWND::default());
let first = Cancellation::new(Some(&owner));
let second = Cancellation::new(Some(&owner));
first.requested.store(true, Ordering::Release);
assert!(first.is_requested());
assert!(!second.is_requested());
assert!(!owner.closed.load(Ordering::Acquire));
}
#[test]
fn dialogs_serialize_per_owner() {
let owner = DialogOwner::new(HWND::default());
let other_owner = DialogOwner::new(HWND::default());
let guard = owner.serialization.try_lock().expect("first dialog");
assert!(owner.serialization.try_lock().is_none());
assert!(other_owner.serialization.try_lock().is_some());
drop(guard);
assert!(owner.serialization.try_lock().is_some());
}
#[test]
fn ownerless_request_can_be_cancelled_without_a_window() {
let cancellation = Cancellation::new(None);
assert!(!cancellation.is_requested());
cancellation.requested.store(true, Ordering::Release);
assert!(cancellation.is_requested());
}
}
+359
View File
@@ -0,0 +1,359 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use anyhow::Result;
use gpui::*;
use gpui_util::ResultExt;
use windows::Win32::{
Foundation::*,
Graphics::{DirectManipulation::*, Gdi::*},
System::Com::*,
UI::{Input::Pointer::*, WindowsAndMessaging::*},
};
use crate::*;
/// Default viewport size in pixels. The actual content size doesn't matter
/// because we're using the viewport only for gesture recognition, not for
/// visual output.
const DEFAULT_VIEWPORT_SIZE: i32 = 1000;
pub(crate) struct DirectManipulationHandler {
manager: IDirectManipulationManager,
update_manager: IDirectManipulationUpdateManager,
viewport: IDirectManipulationViewport,
_handler_cookie: u32,
window: HWND,
scale_factor: Rc<Cell<f32>>,
pending_events: Rc<RefCell<Vec<PlatformInput>>>,
}
impl DirectManipulationHandler {
pub fn new(window: HWND, scale_factor: f32) -> Result<Self> {
unsafe {
let manager: IDirectManipulationManager =
CoCreateInstance(&DirectManipulationManager, None, CLSCTX_INPROC_SERVER)?;
let update_manager: IDirectManipulationUpdateManager = manager.GetUpdateManager()?;
let viewport: IDirectManipulationViewport = manager.CreateViewport(None, window)?;
let configuration = DIRECTMANIPULATION_CONFIGURATION_INTERACTION
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA
| DIRECTMANIPULATION_CONFIGURATION_RAILS_X
| DIRECTMANIPULATION_CONFIGURATION_RAILS_Y
| DIRECTMANIPULATION_CONFIGURATION_SCALING;
viewport.ActivateConfiguration(configuration)?;
viewport.SetViewportOptions(
DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE
| DIRECTMANIPULATION_VIEWPORT_OPTIONS_DISABLEPIXELSNAPPING,
)?;
let mut rect = RECT {
left: 0,
top: 0,
right: DEFAULT_VIEWPORT_SIZE,
bottom: DEFAULT_VIEWPORT_SIZE,
};
viewport.SetViewportRect(&mut rect)?;
manager.Activate(window)?;
viewport.Enable()?;
let scale_factor = Rc::new(Cell::new(scale_factor));
let pending_events = Rc::new(RefCell::new(Vec::new()));
let event_handler: IDirectManipulationViewportEventHandler =
DirectManipulationEventHandler::new(
window,
Rc::clone(&scale_factor),
Rc::clone(&pending_events),
)
.into();
let handler_cookie = viewport.AddEventHandler(Some(window), &event_handler)?;
update_manager.Update(None)?;
Ok(Self {
manager,
update_manager,
viewport,
_handler_cookie: handler_cookie,
window,
scale_factor,
pending_events,
})
}
}
pub fn set_scale_factor(&self, scale_factor: f32) {
self.scale_factor.set(scale_factor);
}
pub fn on_pointer_hit_test(&self, wparam: WPARAM) {
unsafe {
let pointer_id = wparam.loword() as u32;
let mut pointer_type = POINTER_INPUT_TYPE::default();
if GetPointerType(pointer_id, &mut pointer_type).is_ok() && pointer_type == PT_TOUCHPAD
{
self.viewport.SetContact(pointer_id).log_err();
}
}
}
pub fn update(&self) {
unsafe {
self.update_manager.Update(None).log_err();
}
}
pub fn drain_events(&self) -> Vec<PlatformInput> {
std::mem::take(&mut *self.pending_events.borrow_mut())
}
}
impl Drop for DirectManipulationHandler {
fn drop(&mut self) {
unsafe {
self.viewport.Stop().log_err();
self.viewport.Abandon().log_err();
self.manager.Deactivate(self.window).log_err();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureKind {
None,
Scroll,
Pinch,
}
#[windows_core::implement(IDirectManipulationViewportEventHandler)]
struct DirectManipulationEventHandler {
window: HWND,
scale_factor: Rc<Cell<f32>>,
gesture_kind: Cell<GestureKind>,
last_scale: Cell<f32>,
last_x_offset: Cell<f32>,
last_y_offset: Cell<f32>,
scroll_phase: Cell<TouchPhase>,
pending_events: Rc<RefCell<Vec<PlatformInput>>>,
}
impl DirectManipulationEventHandler {
fn new(
window: HWND,
scale_factor: Rc<Cell<f32>>,
pending_events: Rc<RefCell<Vec<PlatformInput>>>,
) -> Self {
Self {
window,
scale_factor,
gesture_kind: Cell::new(GestureKind::None),
last_scale: Cell::new(1.0),
last_x_offset: Cell::new(0.0),
last_y_offset: Cell::new(0.0),
scroll_phase: Cell::new(TouchPhase::Started),
pending_events,
}
}
fn end_gesture(&self) {
let position = self.mouse_position();
let modifiers = current_modifiers();
match self.gesture_kind.get() {
GestureKind::Scroll => {
self.pending_events
.borrow_mut()
.push(PlatformInput::ScrollWheel(ScrollWheelEvent {
position,
delta: ScrollDelta::Pixels(point(px(0.0), px(0.0))),
modifiers,
touch_phase: TouchPhase::Ended,
}));
}
GestureKind::Pinch => {
self.pending_events
.borrow_mut()
.push(PlatformInput::Pinch(PinchEvent {
position,
delta: 0.0,
modifiers,
phase: TouchPhase::Ended,
}));
}
GestureKind::None => {}
}
self.gesture_kind.set(GestureKind::None);
}
fn mouse_position(&self) -> Point<Pixels> {
let scale_factor = self.scale_factor.get();
unsafe {
let mut point: POINT = std::mem::zeroed();
let _ = GetCursorPos(&mut point);
let _ = ScreenToClient(self.window, &mut point);
logical_point(point.x as f32, point.y as f32, scale_factor)
}
}
}
impl IDirectManipulationViewportEventHandler_Impl for DirectManipulationEventHandler_Impl {
fn OnViewportStatusChanged(
&self,
viewport: windows_core::Ref<'_, IDirectManipulationViewport>,
current: DIRECTMANIPULATION_STATUS,
previous: DIRECTMANIPULATION_STATUS,
) -> windows_core::Result<()> {
if current == previous {
return Ok(());
}
// A new gesture interrupted inertia, so end the old sequence.
if current == DIRECTMANIPULATION_RUNNING && previous == DIRECTMANIPULATION_INERTIA {
self.end_gesture();
}
if current == DIRECTMANIPULATION_READY {
self.end_gesture();
// Reset the content transform so the viewport is ready for the next gesture.
// ZoomToRect triggers a second RUNNING -> READY cycle, so prevent an infinite loop here.
if self.last_scale.get() != 1.0
|| self.last_x_offset.get() != 0.0
|| self.last_y_offset.get() != 0.0
{
if let Some(viewport) = viewport.as_ref() {
unsafe {
viewport
.ZoomToRect(
0.0,
0.0,
DEFAULT_VIEWPORT_SIZE as f32,
DEFAULT_VIEWPORT_SIZE as f32,
false,
)
.log_err();
}
}
}
self.last_scale.set(1.0);
self.last_x_offset.set(0.0);
self.last_y_offset.set(0.0);
}
Ok(())
}
fn OnViewportUpdated(
&self,
_viewport: windows_core::Ref<'_, IDirectManipulationViewport>,
) -> windows_core::Result<()> {
Ok(())
}
fn OnContentUpdated(
&self,
_viewport: windows_core::Ref<'_, IDirectManipulationViewport>,
content: windows_core::Ref<'_, IDirectManipulationContent>,
) -> windows_core::Result<()> {
let content = content.as_ref().ok_or(E_POINTER)?;
// Get the 6-element content transform: [scale, 0, 0, scale, tx, ty]
let mut xform = [0.0f32; 6];
unsafe {
content.GetContentTransform(&mut xform)?;
}
let scale = xform[0];
let scale_factor = self.scale_factor.get();
let x_offset = xform[4] / scale_factor;
let y_offset = xform[5] / scale_factor;
if scale == 0.0 {
return Ok(());
}
let last_scale = self.last_scale.get();
let last_x = self.last_x_offset.get();
let last_y = self.last_y_offset.get();
if float_equals(scale, last_scale)
&& float_equals(x_offset, last_x)
&& float_equals(y_offset, last_y)
{
return Ok(());
}
let position = self.mouse_position();
let modifiers = current_modifiers();
// Direct Manipulation reports both translation and scale in every content update.
// Translation values can shift during a pinch due to the zoom center shifting.
// We classify each gesture as either scroll or pinch and only emit one type of event.
// We allow Scroll -> Pinch (a pinch can start with a small pan) but not the reverse.
if !float_equals(scale, 1.0) {
if self.gesture_kind.get() != GestureKind::Pinch {
self.end_gesture();
self.gesture_kind.set(GestureKind::Pinch);
self.pending_events
.borrow_mut()
.push(PlatformInput::Pinch(PinchEvent {
position,
delta: 0.0,
modifiers,
phase: TouchPhase::Started,
}));
}
} else if self.gesture_kind.get() == GestureKind::None {
self.gesture_kind.set(GestureKind::Scroll);
self.scroll_phase.set(TouchPhase::Started);
}
match self.gesture_kind.get() {
GestureKind::Scroll => {
let dx = x_offset - last_x;
let dy = y_offset - last_y;
let touch_phase = self.scroll_phase.get();
self.scroll_phase.set(TouchPhase::Moved);
self.pending_events
.borrow_mut()
.push(PlatformInput::ScrollWheel(ScrollWheelEvent {
position,
delta: ScrollDelta::Pixels(point(px(dx), px(dy))),
modifiers,
touch_phase,
}));
}
GestureKind::Pinch => {
let scale_delta = scale / last_scale;
self.pending_events
.borrow_mut()
.push(PlatformInput::Pinch(PinchEvent {
position,
delta: scale_delta - 1.0,
modifiers,
phase: TouchPhase::Moved,
}));
}
GestureKind::None => {}
}
self.last_scale.set(scale);
self.last_x_offset.set(x_offset);
self.last_y_offset.set(y_offset);
Ok(())
}
}
fn float_equals(f1: f32, f2: f32) -> bool {
const EPSILON_SCALE: f32 = 0.00001;
(f1 - f2).abs() < EPSILON_SCALE * f1.abs().max(f2.abs()).max(EPSILON_SCALE)
}
File diff suppressed because it is too large. Load diff
+420
View File
@@ -0,0 +1,420 @@
use etagere::BucketedAtlasAllocator;
use parking_lot::Mutex;
use windows::Win32::Graphics::{
Direct3D11::{
D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D,
},
Dxgi::Common::*,
};
use gpui::{
AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList,
AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size,
};
pub(crate) struct DirectXAtlas(Mutex<AtlasState<DirectXAtlasTextures>>);
struct DirectXAtlasTextures {
device: ID3D11Device,
device_context: ID3D11DeviceContext,
monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
subpixel_textures: AtlasTextureList<DirectXAtlasTexture>,
}
struct DirectXAtlasTexture {
id: AtlasTextureId,
bytes_per_pixel: u32,
allocator: BucketedAtlasAllocator,
texture: ID3D11Texture2D,
view: [Option<ID3D11ShaderResourceView>; 1],
live_atlas_keys: u32,
}
impl DirectXAtlas {
pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self {
DirectXAtlas(Mutex::new(AtlasState::new(DirectXAtlasTextures {
device: device.clone(),
device_context: device_context.clone(),
monochrome_textures: Default::default(),
polychrome_textures: Default::default(),
subpixel_textures: Default::default(),
})))
}
/// Returns the view backing `id`, or `None` once every tile in it has been
/// removed. A scene can still reference such a texture when a cached view
/// replays a paint from before the image was dropped, so callers must skip
/// those sprites rather than assume the texture exists.
pub(crate) fn get_texture_view(
&self,
id: AtlasTextureId,
) -> Option<[Option<ID3D11ShaderResourceView>; 1]> {
let lock = self.0.lock();
let texture = lock.backend.texture(id)?;
Some(texture.view.clone())
}
pub(crate) fn handle_device_lost(
&self,
device: &ID3D11Device,
device_context: &ID3D11DeviceContext,
) {
let mut lock = self.0.lock();
lock.clear(|textures| {
textures.device = device.clone();
textures.device_context = device_context.clone();
textures.monochrome_textures = AtlasTextureList::default();
textures.polychrome_textures = AtlasTextureList::default();
textures.subpixel_textures = AtlasTextureList::default();
});
}
}
impl PlatformAtlas for DirectXAtlas {
fn get_or_insert_with<'a>(
&self,
key: AtlasKey,
build: &mut dyn FnMut() -> anyhow::Result<
Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
>,
) -> anyhow::Result<Option<AtlasTile>> {
self.0.lock().get_or_insert_with(key, build)
}
fn remove(&self, key: &AtlasKey) {
self.0.lock().remove(key);
}
}
impl AtlasBackend for DirectXAtlasTextures {
fn insert(
&mut self,
kind: AtlasTextureKind,
size: Size<DevicePixels>,
bytes: &[u8],
) -> anyhow::Result<AtlasTile> {
let tile = self
.allocate(size, kind)
.ok_or_else(|| anyhow::anyhow!("failed to allocate"))?;
let texture = self
.texture(tile.texture_id)
.ok_or_else(|| anyhow::anyhow!("allocated tile refers to a missing texture"))?;
texture.upload(&self.device_context, tile.bounds, bytes);
Ok(tile)
}
fn remove(&mut self, tile: AtlasTile) {
let id = tile.texture_id;
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
};
let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else {
return;
};
if let Some(mut texture) = texture_slot.take() {
texture.allocator.deallocate(tile.tile_id.into());
texture.decrement_ref_count();
if texture.is_unreferenced() {
textures.free_list.push(texture.id.index as usize);
} else {
*texture_slot = Some(texture);
}
}
}
}
impl DirectXAtlasTextures {
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,
AtlasTextureKind::Subpixel => &mut self.subpixel_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,
) -> Option<&mut DirectXAtlasTexture> {
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(1024),
height: DevicePixels(1024),
};
// Max texture size for DirectX. See:
// https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-limits
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 pixel_format;
let bind_flag;
let bytes_per_pixel;
match kind {
AtlasTextureKind::Monochrome => {
pixel_format = DXGI_FORMAT_R8_UNORM;
bind_flag = D3D11_BIND_SHADER_RESOURCE;
bytes_per_pixel = 1;
}
AtlasTextureKind::Polychrome => {
pixel_format = DXGI_FORMAT_B8G8R8A8_UNORM;
bind_flag = D3D11_BIND_SHADER_RESOURCE;
bytes_per_pixel = 4;
}
AtlasTextureKind::Subpixel => {
pixel_format = DXGI_FORMAT_R8G8B8A8_UNORM;
bind_flag = D3D11_BIND_SHADER_RESOURCE;
bytes_per_pixel = 4;
}
}
let texture_desc = D3D11_TEXTURE2D_DESC {
Width: size.width.0 as u32,
Height: size.height.0 as u32,
MipLevels: 1,
ArraySize: 1,
Format: pixel_format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: bind_flag.0 as u32,
CPUAccessFlags: 0,
MiscFlags: 0,
};
let mut texture: Option<ID3D11Texture2D> = None;
unsafe {
// This only returns None if the device is lost, which we will recreate later.
// So it's ok to return None here.
self.device
.CreateTexture2D(&texture_desc, None, Some(&mut texture))
.ok()?;
}
let texture = texture.unwrap();
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
};
let index = texture_list.free_list.pop();
let view = unsafe {
let mut view = None;
self.device
.CreateShaderResourceView(&texture, None, Some(&mut view))
.ok()?;
[view]
};
let atlas_texture = DirectXAtlasTexture {
id: AtlasTextureId {
index: index.unwrap_or(texture_list.textures.len()) as u32,
kind,
},
bytes_per_pixel,
allocator: etagere::BucketedAtlasAllocator::new(device_size_to_etagere(size)),
texture,
view,
live_atlas_keys: 0,
};
if let Some(ix) = index {
texture_list.textures[ix] = Some(atlas_texture);
texture_list.textures.get_mut(ix).unwrap().as_mut()
} else {
texture_list.textures.push(Some(atlas_texture));
texture_list.textures.last_mut().unwrap().as_mut()
}
}
fn texture(&self, id: AtlasTextureId) -> Option<&DirectXAtlasTexture> {
let textures = match id.kind {
AtlasTextureKind::Monochrome => &self.monochrome_textures,
AtlasTextureKind::Polychrome => &self.polychrome_textures,
AtlasTextureKind::Subpixel => &self.subpixel_textures,
};
textures.textures.get(id.index as usize)?.as_ref()
}
}
impl DirectXAtlasTexture {
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
let allocation = self.allocator.allocate(device_size_to_etagere(size))?;
let tile = AtlasTile {
texture_id: self.id,
tile_id: allocation.id.into(),
bounds: Bounds {
origin: etagere_point_to_device(allocation.rectangle.min),
size,
},
padding: 0,
};
self.live_atlas_keys += 1;
Some(tile)
}
fn upload(
&self,
device_context: &ID3D11DeviceContext,
bounds: Bounds<DevicePixels>,
bytes: &[u8],
) {
// `UpdateSubresource` reads `row_pitch * height` bytes from `bytes` based on the
// `D3D11_BOX` below. If the caller hands us a slice shorter than that, the driver would
// over-read past the end of the source buffer (potentially by multiple megabytes), so bail
// out instead. This is a first-insert path rather than a per-frame one, so the check is
// effectively free.
let row_bytes = bounds.size.width.to_bytes(self.bytes_per_pixel as u8) as usize;
let expected = row_bytes * bounds.size.height.0.max(0) as usize;
if bytes.len() < expected {
log::error!(
"DirectXAtlasTexture::upload: source slice is {} bytes but the {}x{} region \
requires {} bytes; skipping upload to avoid a driver over-read",
bytes.len(),
bounds.size.width.0,
bounds.size.height.0,
expected,
);
return;
}
unsafe {
device_context.UpdateSubresource(
&self.texture,
0,
Some(&D3D11_BOX {
left: bounds.left().0 as u32,
top: bounds.top().0 as u32,
front: 0,
right: bounds.right().0 as u32,
bottom: bounds.bottom().0 as u32,
back: 1,
}),
bytes.as_ptr() as _,
bounds.size.width.to_bytes(self.bytes_per_pixel as u8),
0,
);
}
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys -= 1;
}
fn is_unreferenced(&mut self) -> bool {
self.live_atlas_keys == 0
}
}
fn device_size_to_etagere(size: Size<DevicePixels>) -> etagere::Size {
etagere::Size::new(size.width.into(), size.height.into())
}
fn etagere_point_to_device(value: etagere::Point) -> Point<DevicePixels> {
Point {
x: DevicePixels::from(value.x),
y: DevicePixels::from(value.y),
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{ImageId, RenderImageParams};
use std::borrow::Cow;
use windows::Win32::{
Foundation::HMODULE,
Graphics::{
Direct3D::D3D_DRIVER_TYPE_WARP,
Direct3D11::{D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION, D3D11CreateDevice},
},
};
fn create_atlas() -> Option<DirectXAtlas> {
let mut device: Option<ID3D11Device> = None;
let mut device_context: Option<ID3D11DeviceContext> = None;
unsafe {
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_WARP,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut device_context),
)
}
.ok()?;
Some(DirectXAtlas::new(&device?, &device_context?))
}
fn make_image_key(image_id: usize) -> AtlasKey {
AtlasKey::Image(RenderImageParams {
image_id: ImageId(image_id),
frame_index: 0,
})
}
fn insert_tile(atlas: &DirectXAtlas, key: AtlasKey, size: Size<DevicePixels>) -> AtlasTile {
atlas
.get_or_insert_with(key, &mut || {
let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4;
Ok(Some((size, Cow::Owned(vec![0u8; byte_count]))))
})
.expect("allocation should succeed")
.expect("callback returns Some")
}
#[test]
fn test_remove_deallocates_tile_space_for_reuse() {
let Some(atlas) = create_atlas() else {
return;
};
let small = Size {
width: DevicePixels(64),
height: DevicePixels(64),
};
let big = Size {
width: DevicePixels(700),
height: DevicePixels(700),
};
let keeper_key = make_image_key(1);
let big_key_a = make_image_key(2);
let big_key_b = make_image_key(3);
let keeper_tile = insert_tile(&atlas, keeper_key, small);
let tile_a = insert_tile(&atlas, big_key_a.clone(), big);
assert_eq!(keeper_tile.texture_id, tile_a.texture_id);
atlas.remove(&big_key_a);
let tile_b = insert_tile(&atlas, big_key_b, big);
assert_eq!(tile_b.texture_id, keeper_tile.texture_id);
}
}
+194
View File
@@ -0,0 +1,194 @@
use anyhow::{Context, Result};
use gpui_util::ResultExt;
use itertools::Itertools;
use windows::Win32::{
Foundation::HMODULE,
Graphics::{
Direct3D::{
D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL, D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1,
},
Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_DEBUG,
D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS,
D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext,
},
Dxgi::{
CreateDXGIFactory2, DXGI_CREATE_FACTORY_DEBUG, DXGI_CREATE_FACTORY_FLAGS,
IDXGIAdapter1, IDXGIFactory6,
},
},
};
use windows::core::Interface;
pub(crate) fn try_to_recover_from_device_lost<T>(mut f: impl FnMut() -> Result<T>) -> Result<T> {
(0..5)
.map(|i| {
if i > 0 {
// Add a small delay before retrying
std::thread::sleep(std::time::Duration::from_millis(100 + i * 10));
}
f()
})
.find_or_last(Result::is_ok)
.unwrap()
.context("DirectXRenderer failed to recover from lost device after multiple attempts")
}
#[derive(Clone)]
pub(crate) struct DirectXDevices {
pub(crate) adapter: IDXGIAdapter1,
pub(crate) dxgi_factory: IDXGIFactory6,
pub(crate) device: ID3D11Device,
pub(crate) device_context: ID3D11DeviceContext,
}
impl DirectXDevices {
pub(crate) fn new() -> Result<Self> {
let debug_layer_available = check_debug_layer_available();
let dxgi_factory =
get_dxgi_factory(debug_layer_available).context("Creating DXGI factory")?;
let (adapter, device, device_context, feature_level) =
get_adapter(&dxgi_factory, debug_layer_available).context("Getting DXGI adapter")?;
match feature_level {
D3D_FEATURE_LEVEL_11_1 => {
log::info!("Created device with Direct3D 11.1 feature level.")
}
D3D_FEATURE_LEVEL_11_0 => {
log::info!("Created device with Direct3D 11.0 feature level.")
}
D3D_FEATURE_LEVEL_10_1 => {
log::info!("Created device with Direct3D 10.1 feature level.")
}
_ => unreachable!(),
}
Ok(Self {
adapter,
dxgi_factory,
device,
device_context,
})
}
}
#[inline]
fn check_debug_layer_available() -> bool {
#[cfg(debug_assertions)]
{
use windows::Win32::Graphics::Dxgi::{DXGIGetDebugInterface1, IDXGIInfoQueue};
unsafe { DXGIGetDebugInterface1::<IDXGIInfoQueue>(0) }
.log_err()
.is_some()
}
#[cfg(not(debug_assertions))]
{
false
}
}
#[inline]
fn get_dxgi_factory(debug_layer_available: bool) -> Result<IDXGIFactory6> {
let factory_flag = if debug_layer_available {
DXGI_CREATE_FACTORY_DEBUG
} else {
#[cfg(debug_assertions)]
log::warn!(
"Failed to get DXGI debug interface. DirectX debugging features will be disabled."
);
DXGI_CREATE_FACTORY_FLAGS::default()
};
unsafe { Ok(CreateDXGIFactory2(factory_flag)?) }
}
#[inline]
fn get_adapter(
dxgi_factory: &IDXGIFactory6,
debug_layer_available: bool,
) -> Result<(
IDXGIAdapter1,
ID3D11Device,
ID3D11DeviceContext,
D3D_FEATURE_LEVEL,
)> {
for adapter_index in 0.. {
let adapter: IDXGIAdapter1 = unsafe { dxgi_factory.EnumAdapters(adapter_index)?.cast()? };
if let Ok(desc) = unsafe { adapter.GetDesc1() } {
let gpu_name = String::from_utf16_lossy(&desc.Description)
.trim_matches(char::from(0))
.to_string();
log::info!("Using GPU: {}", gpu_name);
}
// Check to see whether the adapter supports Direct3D 11 and create
// the device if it does.
let mut context: Option<ID3D11DeviceContext> = None;
let mut feature_level = D3D_FEATURE_LEVEL::default();
if let Some(device) = get_device(
&adapter,
Some(&mut context),
Some(&mut feature_level),
debug_layer_available,
)
.log_err()
{
return Ok((adapter, device, context.unwrap(), feature_level));
}
}
unreachable!()
}
#[inline]
fn get_device(
adapter: &IDXGIAdapter1,
context: Option<*mut Option<ID3D11DeviceContext>>,
feature_level: Option<*mut D3D_FEATURE_LEVEL>,
debug_layer_available: bool,
) -> Result<ID3D11Device> {
let mut device: Option<ID3D11Device> = None;
let device_flags = if debug_layer_available {
D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG
} else {
D3D11_CREATE_DEVICE_BGRA_SUPPORT
};
unsafe {
D3D11CreateDevice(
adapter,
D3D_DRIVER_TYPE_UNKNOWN,
HMODULE::default(),
device_flags,
// 4x MSAA is required for Direct3D Feature Level 10.1 or better
Some(&[
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
]),
D3D11_SDK_VERSION,
Some(&mut device),
feature_level,
context,
)?;
}
let device = device.unwrap();
let mut data = D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS::default();
unsafe {
device
.CheckFeatureSupport(
D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS,
&mut data as *mut _ as _,
std::mem::size_of::<D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS>() as u32,
)
.context("Checking GPU device feature support")?;
}
if data
.ComputeShaders_Plus_RawAndStructuredBuffers_Via_Shader_4_x
.as_bool()
{
Ok(device)
} else {
Err(anyhow::anyhow!(
"Required feature StructuredBuffer is not supported by GPU/driver"
))
}
}
File diff suppressed because it is too large. Load diff
+191
View File
@@ -0,0 +1,191 @@
use std::{
ffi::c_void,
ptr::NonNull,
sync::atomic::{AtomicBool, Ordering},
thread::{ThreadId, current},
time::Duration,
};
use anyhow::Context;
use gpui_util::ResultExt;
use windows::Win32::{
Foundation::{FILETIME, LPARAM, WPARAM},
Media::{timeBeginPeriod, timeEndPeriod},
System::Threading::{
CloseThreadpoolTimer, CreateThreadpoolTimer, GetCurrentThread, PTP_CALLBACK_INSTANCE,
PTP_TIMER, SetThreadPriority, SetThreadpoolTimer, THREAD_PRIORITY_TIME_CRITICAL,
TP_CALLBACK_ENVIRON_V3, TP_CALLBACK_PRIORITY, TP_CALLBACK_PRIORITY_HIGH,
TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL, TrySubmitThreadpoolCallback,
},
UI::WindowsAndMessaging::PostMessageW,
};
use crate::{HWND, SafeHwnd, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD};
use gpui::{
PlatformDispatcher, Priority, PriorityQueueSender, RunnableVariant, TimerResolutionGuard,
};
pub(crate) struct WindowsDispatcher {
pub(crate) wake_posted: AtomicBool,
main_sender: PriorityQueueSender<RunnableVariant>,
main_thread_id: ThreadId,
pub(crate) platform_window_handle: SafeHwnd,
validation_number: usize,
}
impl WindowsDispatcher {
pub(crate) fn new(
main_sender: PriorityQueueSender<RunnableVariant>,
platform_window_handle: HWND,
validation_number: usize,
) -> Self {
let main_thread_id = current().id();
let platform_window_handle = platform_window_handle.into();
WindowsDispatcher {
main_sender,
main_thread_id,
platform_window_handle,
validation_number,
wake_posted: AtomicBool::new(false),
}
}
fn dispatch_on_threadpool(&self, priority: TP_CALLBACK_PRIORITY, runnable: RunnableVariant) {
let environ = TP_CALLBACK_ENVIRON_V3 {
Version: 3,
CallbackPriority: priority,
Size: size_of::<TP_CALLBACK_ENVIRON_V3>() as u32,
..Default::default()
};
// If the thread pool never runs our callback, the matching `from_raw` is never called, which leaks the runnable.
// Dropping the scheduled runnable would cancel its task and make the next poll of any awaiter panic. Since we expect
// the scenario to usually happen during shutdown, this leak is acceptable.
let context = runnable.into_raw().as_ptr() as *mut c_void;
unsafe {
TrySubmitThreadpoolCallback(Some(run_work_callback), Some(context), Some(&environ))
.log_err();
}
}
fn dispatch_on_threadpool_after(&self, runnable: RunnableVariant, duration: Duration) {
let context = runnable.into_raw().as_ptr() as *mut c_void;
unsafe {
if let Ok(timer) = CreateThreadpoolTimer(Some(run_timer_callback), Some(context), None)
{
// Negative FILETIME expresses a relative delay in 100ns ticks
let ticks = (duration.as_nanos() / 100).min(i64::MAX as u128) as i64;
let due = (-ticks) as u64;
let due_time = FILETIME {
dwLowDateTime: due as u32,
dwHighDateTime: (due >> 32) as u32,
};
SetThreadpoolTimer(timer, Some(&due_time), 0, None);
}
}
}
#[inline(always)]
pub(crate) fn execute_runnable(runnable: RunnableVariant) {
let location = runnable.metadata().location;
let spawned = runnable.metadata().spawned;
gpui::profiler::update_running_task(spawned, location);
runnable.run();
gpui::profiler::save_task_timing();
}
}
impl PlatformDispatcher for WindowsDispatcher {
fn is_main_thread(&self) -> bool {
current().id() == self.main_thread_id
}
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
let priority = match priority {
Priority::RealtimeAudio => {
panic!("RealtimeAudio priority should use spawn_realtime, not dispatch")
}
Priority::High => TP_CALLBACK_PRIORITY_HIGH,
Priority::Medium => TP_CALLBACK_PRIORITY_NORMAL,
Priority::Low => TP_CALLBACK_PRIORITY_LOW,
};
self.dispatch_on_threadpool(priority, runnable);
}
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) {
match self.main_sender.send(priority, runnable) {
Ok(_) => {
if !self.wake_posted.swap(true, Ordering::AcqRel) {
unsafe {
PostMessageW(
Some(self.platform_window_handle.as_raw()),
WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD,
WPARAM(self.validation_number),
LPARAM(0),
)
.log_err();
}
}
}
Err(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: RunnableVariant) {
self.dispatch_on_threadpool_after(runnable, duration);
}
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
std::thread::spawn(move || {
// SAFETY: always safe to call
let thread_handle = unsafe { GetCurrentThread() };
// SAFETY: thread_handle is a valid handle to the current thread
unsafe { SetThreadPriority(thread_handle, THREAD_PRIORITY_TIME_CRITICAL) }
.context("thread priority")
.log_err();
f();
});
}
fn increase_timer_resolution(&self) -> TimerResolutionGuard {
unsafe {
timeBeginPeriod(1);
}
gpui_util::defer(Box::new(|| unsafe {
timeEndPeriod(1);
}))
}
}
unsafe extern "system" fn run_work_callback(
_instance: PTP_CALLBACK_INSTANCE,
context: *mut c_void,
) {
let runnable = unsafe { RunnableVariant::from_raw(NonNull::new_unchecked(context as *mut ())) };
WindowsDispatcher::execute_runnable(runnable);
}
unsafe extern "system" fn run_timer_callback(
_instance: PTP_CALLBACK_INSTANCE,
context: *mut c_void,
timer: PTP_TIMER,
) {
let runnable = unsafe { RunnableVariant::from_raw(NonNull::new_unchecked(context as *mut ())) };
WindowsDispatcher::execute_runnable(runnable);
unsafe { CloseThreadpoolTimer(timer) };
}
+210
View File
@@ -0,0 +1,210 @@
use gpui_util::ResultExt;
use itertools::Itertools;
use smallvec::SmallVec;
use std::rc::Rc;
use uuid::Uuid;
use windows::{
Win32::{
Foundation::*,
Graphics::Gdi::*,
UI::{
HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI},
WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI,
},
},
core::*,
};
use crate::logical_point;
use gpui::{Bounds, DevicePixels, DisplayId, Pixels, PlatformDisplay, point, size};
#[derive(Debug, Clone, Copy)]
pub(crate) struct WindowsDisplay {
pub handle: HMONITOR,
pub display_id: DisplayId,
scale_factor: f32,
bounds: Bounds<Pixels>,
visible_bounds: Bounds<Pixels>,
physical_bounds: Bounds<DevicePixels>,
uuid: Uuid,
}
// The `HMONITOR` is thread-safe.
unsafe impl Send for WindowsDisplay {}
unsafe impl Sync for WindowsDisplay {}
impl WindowsDisplay {
pub(crate) fn new(display_id: DisplayId) -> Option<Self> {
let handle = HMONITOR(u64::from(display_id) as _);
let info = get_monitor_info(handle).log_err()?;
let monitor_size = info.monitorInfo.rcMonitor;
let work_area = info.monitorInfo.rcWork;
let uuid = generate_uuid(&info.szDevice);
let scale_factor = get_scale_factor_for_monitor(handle).log_err()?;
let physical_size = size(
(monitor_size.right - monitor_size.left).into(),
(monitor_size.bottom - monitor_size.top).into(),
);
Some(WindowsDisplay {
handle,
display_id,
scale_factor,
bounds: Bounds {
origin: logical_point(
monitor_size.left as f32,
monitor_size.top as f32,
scale_factor,
),
size: physical_size.to_pixels(scale_factor),
},
visible_bounds: Bounds {
origin: logical_point(work_area.left as f32, work_area.top as f32, scale_factor),
size: size(
(work_area.right - work_area.left) as f32 / scale_factor,
(work_area.bottom - work_area.top) as f32 / scale_factor,
)
.map(gpui::px),
},
physical_bounds: Bounds {
origin: point(monitor_size.left.into(), monitor_size.top.into()),
size: physical_size,
},
uuid,
})
}
pub(crate) fn display_id_for_monitor(monitor: HMONITOR) -> DisplayId {
DisplayId::new(monitor.0 as u64)
}
pub fn primary_monitor() -> Option<Self> {
// https://devblogs.microsoft.com/oldnewthing/20070809-00/?p=25643
const POINT_ZERO: POINT = POINT { x: 0, y: 0 };
let monitor = unsafe { MonitorFromPoint(POINT_ZERO, MONITOR_DEFAULTTOPRIMARY) };
if monitor.is_invalid() {
log::error!(
"can not find the primary monitor: {}",
std::io::Error::last_os_error()
);
return None;
}
WindowsDisplay::new(Self::display_id_for_monitor(monitor))
}
/// The DPI scale factor of this monitor, independent of whatever monitor
/// a not-yet-positioned window currently happens to be on.
pub(crate) fn scale_factor(&self) -> f32 {
self.scale_factor
}
/// Check if the center point of given bounds is inside this monitor
pub fn check_given_bounds(&self, bounds: Bounds<Pixels>) -> bool {
let center = bounds.center();
let center = POINT {
x: (center.x.as_f32() * self.scale_factor) as i32,
y: (center.y.as_f32() * self.scale_factor) as i32,
};
let monitor = unsafe { MonitorFromPoint(center, MONITOR_DEFAULTTONULL) };
if monitor.is_invalid() {
false
} else {
let Some(display) = WindowsDisplay::new(Self::display_id_for_monitor(monitor)) else {
return false;
};
display.uuid == self.uuid
}
}
pub fn displays() -> Vec<Rc<dyn PlatformDisplay>> {
available_monitors()
.into_iter()
.filter_map(|handle| {
Some(
Rc::new(WindowsDisplay::new(Self::display_id_for_monitor(handle))?)
as Rc<dyn PlatformDisplay>,
)
})
.collect()
}
pub fn physical_bounds(&self) -> Bounds<DevicePixels> {
self.physical_bounds
}
}
impl PlatformDisplay for WindowsDisplay {
fn id(&self) -> DisplayId {
self.display_id
}
fn uuid(&self) -> anyhow::Result<Uuid> {
Ok(self.uuid)
}
fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
fn visible_bounds(&self) -> Bounds<Pixels> {
self.visible_bounds
}
}
fn available_monitors() -> SmallVec<[HMONITOR; 4]> {
let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new();
unsafe {
EnumDisplayMonitors(
None,
None,
Some(monitor_enum_proc),
LPARAM(&mut monitors as *mut _ as _),
)
.ok()
.log_err();
}
monitors
}
unsafe extern "system" fn monitor_enum_proc(
hmonitor: HMONITOR,
_hdc: HDC,
_place: *mut RECT,
data: LPARAM,
) -> BOOL {
let monitors = data.0 as *mut SmallVec<[HMONITOR; 4]>;
unsafe { (*monitors).push(hmonitor) };
BOOL(1)
}
fn get_monitor_info(hmonitor: HMONITOR) -> anyhow::Result<MONITORINFOEXW> {
let mut monitor_info: MONITORINFOEXW = unsafe { std::mem::zeroed() };
monitor_info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
let status = unsafe {
GetMonitorInfoW(
hmonitor,
&mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO,
)
};
if status.as_bool() {
Ok(monitor_info)
} else {
Err(anyhow::anyhow!(std::io::Error::last_os_error()))
}
}
fn generate_uuid(device_name: &[u16]) -> Uuid {
let name = device_name
.iter()
.flat_map(|&a| a.to_be_bytes())
.collect_vec();
Uuid::new_v5(&Uuid::NAMESPACE_DNS, &name)
}
fn get_scale_factor_for_monitor(monitor: HMONITOR) -> Result<f32> {
let mut dpi_x = 0;
let mut dpi_y = 0;
unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) }?;
assert_eq!(dpi_x, dpi_y);
Ok(dpi_x as f32 / USER_DEFAULT_SCREEN_DPI as f32)
}
File diff suppressed because it is too large. Load diff
+43
View File
@@ -0,0 +1,43 @@
#![cfg(target_os = "windows")]
mod clipboard;
mod destination_list;
mod dialog;
mod direct_manipulation;
mod direct_write;
mod directx_atlas;
mod directx_devices;
mod directx_renderer;
mod dispatcher;
mod display;
mod events;
mod keyboard;
mod platform;
mod system_notifications;
mod system_settings;
mod util;
mod vsync;
mod window;
mod wrapper;
pub(crate) use clipboard::*;
pub(crate) use destination_list::*;
pub(crate) use direct_write::*;
pub(crate) use directx_atlas::*;
pub(crate) use directx_devices::*;
pub(crate) use directx_renderer::*;
pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use events::*;
pub(crate) use keyboard::*;
pub(crate) use platform::*;
pub(crate) use system_notifications::*;
pub(crate) use system_settings::*;
pub(crate) use util::*;
pub(crate) use vsync::*;
pub(crate) use window::*;
pub(crate) use wrapper::*;
pub use platform::WindowsPlatform;
pub(crate) use windows::Win32::Foundation::HWND;
+371
View File
@@ -0,0 +1,371 @@
use anyhow::Result;
use collections::HashMap;
use windows::Win32::UI::{
Input::KeyboardAndMouse::{
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
},
WindowsAndMessaging::KL_NAMELENGTH,
};
use gpui::{
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
};
pub(crate) struct WindowsKeyboardLayout {
id: String,
name: String,
}
pub(crate) struct WindowsKeyboardMapper {
key_to_vkey: HashMap<String, (u16, bool)>,
vkey_to_key: HashMap<u16, String>,
vkey_to_shifted: HashMap<u16, String>,
}
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.name
}
}
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
fn map_key_equivalent(
&self,
mut keystroke: Keystroke,
use_key_equivalents: bool,
) -> KeybindingKeystroke {
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
else {
return KeybindingKeystroke::from_keystroke(keystroke);
};
if shifted_key && keystroke.modifiers.shift {
log::warn!(
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
keystroke.key
);
}
let shift = shifted_key || keystroke.modifiers.shift;
keystroke.modifiers.shift = false;
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
log::error!(
"Failed to map key equivalent '{:?}' to a valid key",
keystroke
);
return KeybindingKeystroke::from_keystroke(keystroke);
};
keystroke.key = if shift {
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
log::error!(
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
keystroke,
vkey
);
return KeybindingKeystroke::from_keystroke(keystroke);
};
shifted_key
} else {
key.clone()
};
let modifiers = Modifiers {
shift,
..keystroke.modifiers
};
KeybindingKeystroke::new(keystroke, modifiers, key)
}
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
None
}
}
impl WindowsKeyboardLayout {
pub(crate) fn new() -> Result<Self> {
let mut buffer = [0u16; KL_NAMELENGTH as usize]; // KL_NAMELENGTH includes the null terminator
unsafe { GetKeyboardLayoutNameW(&mut buffer)? };
let id = String::from_utf16_lossy(&buffer[..buffer.len() - 1]); // Remove the null terminator
let entry = windows_registry::LOCAL_MACHINE.open(format!(
"System\\CurrentControlSet\\Control\\Keyboard Layouts\\{id}"
))?;
let name = entry.get_string("Layout Text")?;
Ok(Self { id, name })
}
pub(crate) fn unknown() -> Self {
Self {
id: "unknown".to_string(),
name: "unknown".to_string(),
}
}
}
impl WindowsKeyboardMapper {
pub(crate) fn new() -> Self {
let mut key_to_vkey = HashMap::default();
let mut vkey_to_key = HashMap::default();
let mut vkey_to_shifted = HashMap::default();
for vkey in CANDIDATE_VKEYS {
if let Some(key) = get_key_from_vkey(*vkey) {
key_to_vkey.insert(key.clone(), (vkey.0, false));
vkey_to_key.insert(vkey.0, key);
}
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
if scan_code == 0 {
continue;
}
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
vkey_to_shifted.insert(vkey.0, shifted_key);
}
}
Self {
key_to_vkey,
vkey_to_key,
vkey_to_shifted,
}
}
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
if use_key_equivalents {
get_vkey_from_key_with_us_layout(key)
} else {
self.key_to_vkey.get(key).cloned()
}
}
}
pub(crate) fn get_keystroke_key(
vkey: VIRTUAL_KEY,
scan_code: u32,
modifiers: &mut Modifiers,
) -> Option<String> {
if modifiers.shift && need_to_convert_to_shifted_key(vkey) {
get_shifted_key(vkey, scan_code).inspect(|_| {
modifiers.shift = false;
})
} else {
get_key_from_vkey(vkey)
}
}
fn get_key_from_vkey(vkey: VIRTUAL_KEY) -> Option<String> {
let key_data = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_CHAR) };
if key_data == 0 {
return None;
}
// The high word contains dead key flag, the low word contains the character
let key = char::from_u32(key_data & 0xFFFF)?;
Some(key.to_ascii_lowercase().to_string())
}
#[inline]
fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
matches!(
vkey,
VK_OEM_3
| VK_OEM_MINUS
| VK_OEM_PLUS
| VK_OEM_4
| VK_OEM_5
| VK_OEM_6
| VK_OEM_1
| VK_OEM_7
| VK_OEM_COMMA
| VK_OEM_PERIOD
| VK_OEM_2
| VK_OEM_102
| VK_OEM_8
| VK_ABNT_C1
| VK_0
| VK_1
| VK_2
| VK_3
| VK_4
| VK_5
| VK_6
| VK_7
| VK_8
| VK_9
)
}
fn get_shifted_key(vkey: VIRTUAL_KEY, scan_code: u32) -> Option<String> {
generate_key_char(vkey, scan_code, false, true, false)
}
pub(crate) fn generate_key_char(
vkey: VIRTUAL_KEY,
scan_code: u32,
control: bool,
shift: bool,
alt: bool,
) -> Option<String> {
let mut state = [0; 256];
if control {
state[VK_CONTROL.0 as usize] = 0x80;
}
if shift {
state[VK_SHIFT.0 as usize] = 0x80;
}
if alt {
state[VK_MENU.0 as usize] = 0x80;
}
let mut buffer = [0; 8];
let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 0x5) };
match len {
len if len > 0 => String::from_utf16(&buffer[..len as usize])
.ok()
.filter(|candidate| {
!candidate.is_empty() && !candidate.chars().next().unwrap().is_control()
}),
len if len < 0 => String::from_utf16(&buffer[..(-len as usize)]).ok(),
_ => None,
}
}
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
match key {
// ` => VK_OEM_3
"`" => Some((VK_OEM_3.0, false)),
"~" => Some((VK_OEM_3.0, true)),
"1" => Some((VK_1.0, false)),
"!" => Some((VK_1.0, true)),
"2" => Some((VK_2.0, false)),
"@" => Some((VK_2.0, true)),
"3" => Some((VK_3.0, false)),
"#" => Some((VK_3.0, true)),
"4" => Some((VK_4.0, false)),
"$" => Some((VK_4.0, true)),
"5" => Some((VK_5.0, false)),
"%" => Some((VK_5.0, true)),
"6" => Some((VK_6.0, false)),
"^" => Some((VK_6.0, true)),
"7" => Some((VK_7.0, false)),
"&" => Some((VK_7.0, true)),
"8" => Some((VK_8.0, false)),
"*" => Some((VK_8.0, true)),
"9" => Some((VK_9.0, false)),
"(" => Some((VK_9.0, true)),
"0" => Some((VK_0.0, false)),
")" => Some((VK_0.0, true)),
"-" => Some((VK_OEM_MINUS.0, false)),
"_" => Some((VK_OEM_MINUS.0, true)),
"=" => Some((VK_OEM_PLUS.0, false)),
"+" => Some((VK_OEM_PLUS.0, true)),
"[" => Some((VK_OEM_4.0, false)),
"{" => Some((VK_OEM_4.0, true)),
"]" => Some((VK_OEM_6.0, false)),
"}" => Some((VK_OEM_6.0, true)),
"\\" => Some((VK_OEM_5.0, false)),
"|" => Some((VK_OEM_5.0, true)),
";" => Some((VK_OEM_1.0, false)),
":" => Some((VK_OEM_1.0, true)),
"'" => Some((VK_OEM_7.0, false)),
"\"" => Some((VK_OEM_7.0, true)),
"," => Some((VK_OEM_COMMA.0, false)),
"<" => Some((VK_OEM_COMMA.0, true)),
"." => Some((VK_OEM_PERIOD.0, false)),
">" => Some((VK_OEM_PERIOD.0, true)),
"/" => Some((VK_OEM_2.0, false)),
"?" => Some((VK_OEM_2.0, true)),
_ => None,
}
}
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
VK_OEM_3,
VK_OEM_MINUS,
VK_OEM_PLUS,
VK_OEM_4,
VK_OEM_5,
VK_OEM_6,
VK_OEM_1,
VK_OEM_7,
VK_OEM_COMMA,
VK_OEM_PERIOD,
VK_OEM_2,
VK_OEM_102,
VK_OEM_8,
VK_ABNT_C1,
VK_0,
VK_1,
VK_2,
VK_3,
VK_4,
VK_5,
VK_6,
VK_7,
VK_8,
VK_9,
];
#[cfg(test)]
mod tests {
use crate::WindowsKeyboardMapper;
use gpui::{Keystroke, Modifiers, PlatformKeyboardMapper};
#[test]
fn test_keyboard_mapper() {
let mapper = WindowsKeyboardMapper::new();
// Normal case
let keystroke = Keystroke {
modifiers: Modifiers::control(),
key: "a".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(*mapped.inner(), keystroke);
assert_eq!(mapped.key(), "a");
assert_eq!(*mapped.modifiers(), Modifiers::control());
// Shifted case, ctrl-$
let keystroke = Keystroke {
modifiers: Modifiers::control(),
key: "$".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(*mapped.inner(), keystroke);
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
// Shifted case, but shift is true
let keystroke = Keystroke {
modifiers: Modifiers::control_shift(),
key: "$".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner().modifiers, Modifiers::control());
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
// Windows style
let keystroke = Keystroke {
modifiers: Modifiers::control_shift(),
key: "4".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner().modifiers, Modifiers::control());
assert_eq!(mapped.inner().key, "$");
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+198
View File
@@ -0,0 +1,198 @@
//! System notifications as Windows toast notifications.
use std::cell::RefCell;
use std::collections::{HashMap, hash_map::DefaultHasher};
use std::hash::{Hash as _, Hasher as _};
use std::rc::Rc;
use futures::StreamExt as _;
use futures::channel::mpsc;
use gpui::{
ForegroundExecutor, SharedString, SystemNotification, SystemNotificationResponse, Task,
};
use windows::Data::Xml::Dom::XmlDocument;
use windows::Foundation::TypedEventHandler;
use windows::UI::Notifications::{
ToastActivatedEventArgs, ToastNotification, ToastNotificationManager, ToastNotifier,
};
use windows::core::{IInspectable, Interface as _, h};
type ResponseCallback = Rc<RefCell<Option<Box<dyn FnMut(SystemNotificationResponse)>>>>;
pub(crate) struct SystemNotificationState {
notifier: Option<ToastNotifier>,
active_toasts: HashMap<SharedString, ToastNotification>,
response_sender: mpsc::UnboundedSender<SystemNotificationResponse>,
response_receiver: Option<mpsc::UnboundedReceiver<SystemNotificationResponse>>,
callback: ResponseCallback,
_response_task: Option<Task<()>>,
}
impl SystemNotificationState {
pub(crate) fn new() -> Self {
let (response_sender, response_receiver) = mpsc::unbounded();
Self {
notifier: None,
active_toasts: HashMap::new(),
response_sender,
response_receiver: Some(response_receiver),
callback: Rc::new(RefCell::new(None)),
_response_task: None,
}
}
pub(crate) fn show(
&mut self,
has_package_identity: bool,
app_identity: Option<(&str, &str)>,
notification: SystemNotification,
) -> windows::core::Result<()> {
let Some(notifier) = self.notifier(has_package_identity, app_identity)? else {
return Ok(());
};
let document = toast_document(&notification)?;
let toast = ToastNotification::CreateToastNotification(&document)?;
// Windows caps toast tags at 64 characters (post-Creators Update),
// so hash the arbitrary GPUI tag down to a fixed-width value.
let tag = {
let mut hasher = DefaultHasher::new();
notification.tag.hash(&mut hasher);
format!("{:016x}", hasher.finish())
};
toast.SetTag(&tag.into())?;
let sender = self.response_sender.clone();
let response_tag = notification.tag.clone();
toast.Activated(&TypedEventHandler::<ToastNotification, IInspectable>::new(
move |_sender, arguments| {
let action_id = arguments
.as_ref()
.and_then(|arguments| arguments.cast::<ToastActivatedEventArgs>().ok())
.and_then(|arguments| arguments.Arguments().ok())
.filter(|arguments| !arguments.is_empty())
.map(|arguments| SharedString::from(arguments.to_string()));
sender
.unbounded_send(SystemNotificationResponse {
tag: response_tag.clone(),
action_id,
})
.ok();
Ok(())
},
))?;
if let Some(previous) = self.active_toasts.remove(&notification.tag) {
notifier.Hide(&previous)?;
}
notifier.Show(&toast)?;
self.active_toasts.insert(notification.tag, toast);
Ok(())
}
pub(crate) fn dismiss(&mut self, tag: &str) {
let Some(toast) = self.active_toasts.remove(tag) else {
return;
};
let Some(notifier) = &self.notifier else {
return;
};
if let Err(error) = notifier.Hide(&toast) {
log::warn!("failed to dismiss system notification: {error}");
}
}
pub(crate) fn on_response(
&mut self,
executor: &ForegroundExecutor,
callback: Box<dyn FnMut(SystemNotificationResponse)>,
) {
*self.callback.borrow_mut() = Some(callback);
if let Some(mut receiver) = self.response_receiver.take() {
let callback = self.callback.clone();
self._response_task = Some(executor.spawn(async move {
while let Some(response) = receiver.next().await {
// Take the callback out for the call: it may re-enter the
// platform (e.g. to dismiss the notification it was told
// about) or replace itself.
let taken = callback.borrow_mut().take();
if let Some(mut taken) = taken {
taken(response);
callback.borrow_mut().get_or_insert(taken);
}
}
}));
}
}
fn notifier(
&mut self,
has_package_identity: bool,
app_identity: Option<(&str, &str)>,
) -> windows::core::Result<Option<ToastNotifier>> {
if let Some(notifier) = &self.notifier {
return Ok(Some(notifier.clone()));
}
let notifier = if has_package_identity {
ToastNotificationManager::CreateToastNotifier()?
} else {
let Some((app_identifier, app_name)) = app_identity else {
log::warn!(
"cannot show a system notification without an app identity; \
call `App::set_app_identity` during startup"
);
return Ok(None);
};
register_app_user_model_id(app_identifier, app_name);
ToastNotificationManager::CreateToastNotifierWithId(&app_identifier.into())?
};
self.notifier = Some(notifier.clone());
Ok(Some(notifier))
}
}
fn toast_document(notification: &SystemNotification) -> windows::core::Result<XmlDocument> {
let document = XmlDocument::new()?;
let toast = document.CreateElement(h!("toast"))?;
document.AppendChild(&toast)?;
let visual = document.CreateElement(h!("visual"))?;
toast.AppendChild(&visual)?;
let binding = document.CreateElement(h!("binding"))?;
binding.SetAttribute(h!("template"), h!("ToastGeneric"))?;
visual.AppendChild(&binding)?;
for text in [&notification.title, &notification.body] {
let element = document.CreateElement(h!("text"))?;
element.SetInnerText(&text.as_ref().into())?;
binding.AppendChild(&element)?;
}
if !notification.actions.is_empty() {
let actions = document.CreateElement(h!("actions"))?;
toast.AppendChild(&actions)?;
for action in &notification.actions {
let action_element = document.CreateElement(h!("action"))?;
action_element.SetAttribute(h!("content"), &action.label.as_ref().into())?;
action_element.SetAttribute(h!("arguments"), &action.id.as_ref().into())?;
actions.AppendChild(&action_element)?;
}
}
let audio = document.CreateElement(h!("audio"))?;
audio.SetAttribute(h!("silent"), h!("true"))?;
toast.AppendChild(&audio)?;
Ok(document)
}
/// Registers the app's AUMID so toasts display correctly for an unpackaged app without a Start Menu shortcut.
fn register_app_user_model_id(app_identifier: &str, app_name: &str) {
let result = windows_registry::CURRENT_USER
.create(format!(r"Software\Classes\AppUserModelId\{app_identifier}"))
.and_then(|key| key.set_string("DisplayName", app_name));
if let Err(error) = result {
log::warn!("failed to register AppUserModelID; notifications may not display: {error}");
}
}
+87
View File
@@ -0,0 +1,87 @@
use std::{
cell::Cell,
ffi::{c_uint, c_void},
};
use gpui_util::ResultExt;
use windows::Win32::UI::WindowsAndMessaging::{
SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SYSTEM_PARAMETERS_INFO_ACTION,
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
};
/// Windows settings pulled from SystemParametersInfo
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow
#[derive(Default, Debug, Clone)]
pub(crate) struct WindowsSystemSettings {
pub(crate) mouse_wheel_settings: MouseWheelSettings,
}
#[derive(Default, Debug, Clone)]
pub(crate) struct MouseWheelSettings {
/// SEE: SPI_GETWHEELSCROLLCHARS
pub(crate) wheel_scroll_chars: Cell<u32>,
/// SEE: SPI_GETWHEELSCROLLLINES
pub(crate) wheel_scroll_lines: Cell<u32>,
}
impl WindowsSystemSettings {
pub(crate) fn new() -> Self {
let mut settings = Self::default();
settings.init();
settings
}
fn init(&mut self) {
self.mouse_wheel_settings.update();
}
pub(crate) fn update(&self, wparam: usize) {
match SYSTEM_PARAMETERS_INFO_ACTION(wparam as u32) {
SPI_GETWHEELSCROLLLINES | SPI_GETWHEELSCROLLCHARS => self.update_mouse_wheel_settings(),
_ => {}
}
}
fn update_mouse_wheel_settings(&self) {
self.mouse_wheel_settings.update();
}
}
impl MouseWheelSettings {
fn update(&self) {
self.update_wheel_scroll_chars();
self.update_wheel_scroll_lines();
}
fn update_wheel_scroll_chars(&self) {
let mut value = c_uint::default();
let result = unsafe {
SystemParametersInfoW(
SPI_GETWHEELSCROLLCHARS,
0,
Some((&mut value) as *mut c_uint as *mut c_void),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
)
};
if result.log_err() != None && self.wheel_scroll_chars.get() != value {
self.wheel_scroll_chars.set(value);
}
}
fn update_wheel_scroll_lines(&self) {
let mut value = c_uint::default();
let result = unsafe {
SystemParametersInfoW(
SPI_GETWHEELSCROLLLINES,
0,
Some((&mut value) as *mut c_uint as *mut c_void),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
)
};
if result.log_err() != None && self.wheel_scroll_lines.get() != value {
self.wheel_scroll_lines.set(value);
}
}
}
+191
View File
@@ -0,0 +1,191 @@
use std::sync::OnceLock;
use anyhow::Context;
use gpui_util::ResultExt;
use windows::{
UI::{
Color,
ViewManagement::{UIColorType, UISettings},
},
Win32::{
Foundation::*, Graphics::Dwm::*, System::LibraryLoader::LoadLibraryA,
UI::WindowsAndMessaging::*,
},
core::{BOOL, PCSTR},
};
use crate::*;
use gpui::*;
pub(crate) trait HiLoWord {
fn hiword(&self) -> u16;
fn loword(&self) -> u16;
fn signed_hiword(&self) -> i16;
fn signed_loword(&self) -> i16;
}
impl HiLoWord for WPARAM {
fn hiword(&self) -> u16 {
((self.0 >> 16) & 0xFFFF) as u16
}
fn loword(&self) -> u16 {
(self.0 & 0xFFFF) as u16
}
fn signed_hiword(&self) -> i16 {
((self.0 >> 16) & 0xFFFF) as i16
}
fn signed_loword(&self) -> i16 {
(self.0 & 0xFFFF) as i16
}
}
impl HiLoWord for LPARAM {
fn hiword(&self) -> u16 {
((self.0 >> 16) & 0xFFFF) as u16
}
fn loword(&self) -> u16 {
(self.0 & 0xFFFF) as u16
}
fn signed_hiword(&self) -> i16 {
((self.0 >> 16) & 0xFFFF) as i16
}
fn signed_loword(&self) -> i16 {
(self.0 & 0xFFFF) as i16
}
}
pub(crate) unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize {
#[cfg(target_pointer_width = "64")]
unsafe {
GetWindowLongPtrW(hwnd, nindex)
}
#[cfg(target_pointer_width = "32")]
unsafe {
GetWindowLongW(hwnd, nindex) as isize
}
}
pub(crate) unsafe fn set_window_long(
hwnd: HWND,
nindex: WINDOW_LONG_PTR_INDEX,
dwnewlong: isize,
) -> isize {
#[cfg(target_pointer_width = "64")]
unsafe {
SetWindowLongPtrW(hwnd, nindex, dwnewlong)
}
#[cfg(target_pointer_width = "32")]
unsafe {
SetWindowLongW(hwnd, nindex, dwnewlong as i32) as isize
}
}
pub(crate) fn windows_credentials_target_name(url: &str) -> String {
format!("zed:url={}", url)
}
pub(crate) fn load_cursor(style: CursorStyle) -> Option<HCURSOR> {
static ARROW: OnceLock<SafeCursor> = OnceLock::new();
static IBEAM: OnceLock<SafeCursor> = OnceLock::new();
static CROSS: OnceLock<SafeCursor> = OnceLock::new();
static HAND: OnceLock<SafeCursor> = OnceLock::new();
static SIZEWE: OnceLock<SafeCursor> = OnceLock::new();
static SIZENS: OnceLock<SafeCursor> = OnceLock::new();
static SIZENWSE: OnceLock<SafeCursor> = OnceLock::new();
static SIZENESW: OnceLock<SafeCursor> = OnceLock::new();
static NO: OnceLock<SafeCursor> = OnceLock::new();
let (lock, name) = match style {
CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => (&IBEAM, IDC_IBEAM),
CursorStyle::Crosshair => (&CROSS, IDC_CROSS),
CursorStyle::PointingHand | CursorStyle::DragLink => (&HAND, IDC_HAND),
CursorStyle::ResizeLeft
| CursorStyle::ResizeRight
| CursorStyle::ResizeLeftRight
| CursorStyle::ResizeColumn => (&SIZEWE, IDC_SIZEWE),
CursorStyle::ResizeUp
| CursorStyle::ResizeDown
| CursorStyle::ResizeUpDown
| CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS),
CursorStyle::ResizeUpLeftDownRight => (&SIZENWSE, IDC_SIZENWSE),
CursorStyle::ResizeUpRightDownLeft => (&SIZENESW, IDC_SIZENESW),
CursorStyle::OperationNotAllowed => (&NO, IDC_NO),
_ => (&ARROW, IDC_ARROW),
};
Some(
*(*lock.get_or_init(|| {
HCURSOR(
unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) }
.log_err()
.unwrap_or_default()
.0,
)
.into()
})),
)
}
/// This function is used to configure the dark mode for the window built-in title bar.
pub(crate) fn configure_dwm_dark_mode(hwnd: HWND, appearance: WindowAppearance) {
let dark_mode_enabled: BOOL = match appearance {
WindowAppearance::Dark | WindowAppearance::VibrantDark => true.into(),
WindowAppearance::Light | WindowAppearance::VibrantLight => false.into(),
};
unsafe {
DwmSetWindowAttribute(
hwnd,
DWMWA_USE_IMMERSIVE_DARK_MODE,
&dark_mode_enabled as *const _ as _,
std::mem::size_of::<BOOL>() as u32,
)
.log_err();
}
}
#[inline]
pub(crate) fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point<Pixels> {
Point {
x: px(x / scale_factor),
y: px(y / scale_factor),
}
}
// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes
#[inline]
pub(crate) fn system_appearance() -> Result<WindowAppearance> {
let ui_settings = UISettings::new()?;
let foreground_color = ui_settings.GetColorValue(UIColorType::Foreground)?;
// If the foreground is light, then is_color_light will evaluate to true,
// meaning Dark mode is enabled.
if is_color_light(&foreground_color) {
Ok(WindowAppearance::Dark)
} else {
Ok(WindowAppearance::Light)
}
}
#[inline(always)]
fn is_color_light(color: &Color) -> bool {
((5 * color.G as u32) + (2 * color.R as u32) + color.B as u32) > (8 * 128)
}
pub(crate) fn with_dll_library<R, F>(dll_name: PCSTR, f: F) -> Result<R>
where
F: FnOnce(HMODULE) -> Result<R>,
{
let library = unsafe {
LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))?
};
let result = f(library);
unsafe {
FreeLibrary(library)
.with_context(|| format!("Freeing dll: {}", dll_name.display()))
.log_err();
}
result
}
+119
View File
@@ -0,0 +1,119 @@
use std::{
sync::LazyLock,
time::{Duration, Instant},
};
use anyhow::{Context, Result};
use gpui::FrameRequestSource;
use gpui_util::ResultExt;
use windows::Win32::{
Foundation::HWND,
Graphics::Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo},
System::Performance::QueryPerformanceFrequency,
};
static QPC_TICKS_PER_SECOND: LazyLock<u64> = LazyLock::new(|| {
let mut frequency = 0;
// On systems that run Windows XP or later, the function will always succeed and
// will thus never return zero.
unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() };
frequency as u64
});
const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1);
const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz
pub(crate) struct VSyncProvider {
interval: Duration,
f: Box<dyn Fn() -> bool>,
}
impl VSyncProvider {
pub(crate) fn new() -> Self {
let interval = get_dwm_interval()
.context("Failed to get DWM interval")
.log_err()
.unwrap_or(DEFAULT_VSYNC_INTERVAL);
let f = Box::new(|| unsafe { DwmFlush().is_ok() });
Self { interval, f }
}
pub(crate) fn wait_for_vsync(&self) -> FrameRequestSource {
let vsync_start = Instant::now();
let wait_succeeded = (self.f)();
let elapsed = vsync_start.elapsed();
// DwmFlush and DCompositionWaitForCompositorClock returns very early
// instead of waiting until vblank when the monitor goes to sleep or is
// unplugged (nothing to present due to desktop occlusion). We use 1ms as
// a threshold for the duration of the wait functions and fallback to
// Sleep() if it returns before that. This could happen during normal
// operation for the first call after the vsync thread becomes non-idle,
// but it shouldn't happen often.
if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD {
log::trace!("VSyncProvider::wait_for_vsync() took less time than expected");
std::thread::sleep(self.interval);
FrameRequestSource::LocalSchedule
} else {
FrameRequestSource::NativeCallback
}
}
}
fn get_dwm_interval() -> Result<Duration> {
let mut timing_info = DWM_TIMING_INFO {
cbSize: std::mem::size_of::<DWM_TIMING_INFO>() as u32,
..Default::default()
};
unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?;
let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND);
// Check for interval values that are impossibly low. A 29 microsecond
// interval was seen (from a qpcRefreshPeriod of 60).
if interval < VSYNC_INTERVAL_THRESHOLD {
Ok(retrieve_duration(
timing_info.rateRefresh.uiDenominator as u64,
timing_info.rateRefresh.uiNumerator as u64,
))
} else {
Ok(interval)
}
}
#[inline]
fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration {
let ticks_per_microsecond = ticks_per_second / 1_000_000;
Duration::from_micros(counts / ticks_per_microsecond)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn successful_compositor_wait_reports_native_callback() {
let provider = VSyncProvider {
interval: Duration::ZERO,
f: Box::new(|| {
std::thread::sleep(VSYNC_INTERVAL_THRESHOLD * 2);
true
}),
};
assert_eq!(
provider.wait_for_vsync(),
FrameRequestSource::NativeCallback
);
}
#[test]
fn failed_compositor_wait_reports_local_schedule_even_after_threshold() {
let provider = VSyncProvider {
interval: Duration::ZERO,
f: Box::new(|| {
std::thread::sleep(VSYNC_INTERVAL_THRESHOLD * 2);
false
}),
};
assert_eq!(provider.wait_for_vsync(), FrameRequestSource::LocalSchedule);
}
}
File diff suppressed because it is too large. Load diff
+53
View File
@@ -0,0 +1,53 @@
use std::ops::Deref;
use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR};
#[derive(Debug, Clone, Copy)]
pub(crate) struct SafeCursor {
raw: HCURSOR,
}
unsafe impl Send for SafeCursor {}
unsafe impl Sync for SafeCursor {}
impl From<HCURSOR> for SafeCursor {
fn from(value: HCURSOR) -> Self {
SafeCursor { raw: value }
}
}
impl Deref for SafeCursor {
type Target = HCURSOR;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SafeHwnd {
raw: HWND,
}
impl SafeHwnd {
pub(crate) fn as_raw(&self) -> HWND {
self.raw
}
}
unsafe impl Send for SafeHwnd {}
unsafe impl Sync for SafeHwnd {}
impl From<HWND> for SafeHwnd {
fn from(value: HWND) -> Self {
SafeHwnd { raw: value }
}
}
impl Deref for SafeHwnd {
type Target = HWND;
fn deref(&self) -> &Self::Target {
&self.raw
}
}