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:
1 parent
3b23a92b3c
commit
09479941ff
28 files changed
+20112
No files matched your search
+4882
File diff suppressed because it is too large.
Load diff
Vendored
+243
@@ -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
@@ -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
@@ -0,0 +1,222 @@
|
||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
Vendored
+242
@@ -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;
|
||||
@@ -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
@@ -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() };
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
+2170
File diff suppressed because it is too large.
Load diff
+420
@@ -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
@@ -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"
|
||||
))
|
||||
}
|
||||
}
|
||||
+2070
File diff suppressed because it is too large.
Load diff
+191
@@ -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
@@ -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)
|
||||
}
|
||||
+1812
File diff suppressed because it is too large.
Load diff
+43
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
+1939
File diff suppressed because it is too large.
Load diff
+1268
File diff suppressed because it is too large.
Load diff
+198
@@ -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(¬ification)?;
|
||||
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(¬ification.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 [¬ification.title, ¬ification.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 ¬ification.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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+191
@@ -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
|
||||
}
|
||||
Vendored
+119
@@ -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);
|
||||
}
|
||||
}
|
||||
+1735
File diff suppressed because it is too large.
Load diff
+53
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user