logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
46
crates/gpui_windows/Cargo.toml
Normal file
46
crates/gpui_windows/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "gpui_windows"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/gpui_windows.rs"
|
||||
|
||||
[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]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
etagere = "0.2"
|
||||
futures.workspace = true
|
||||
image.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rand.workspace = true
|
||||
raw-window-handle = "0.6"
|
||||
smallvec.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
windows.workspace = true
|
||||
windows-core.workspace = true
|
||||
windows-numerics = "0.2"
|
||||
windows-registry = "0.5"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.scap]
|
||||
workspace = true
|
||||
optional = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
windows-registry = "0.5"
|
||||
1
crates/gpui_windows/LICENSE-APACHE
Symbolic link
1
crates/gpui_windows/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
242
crates/gpui_windows/build.rs
Normal file
242
crates/gpui_windows/build.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Compile HLSL shaders
|
||||
#[cfg(not(debug_assertions))]
|
||||
compile_shaders();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "windows", not(debug_assertions)))]
|
||||
mod shader_compilation {
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
pub fn compile_shaders() {
|
||||
let shader_path =
|
||||
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("src/shaders.hlsl");
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed={}", shader_path.display());
|
||||
|
||||
// Check if fxc.exe is available
|
||||
let fxc_path = find_fxc_compiler();
|
||||
|
||||
// Define all modules
|
||||
let modules = [
|
||||
"quad",
|
||||
"shadow",
|
||||
"path_rasterization",
|
||||
"path_sprite",
|
||||
"underline",
|
||||
"monochrome_sprite",
|
||||
"subpixel_sprite",
|
||||
"polychrome_sprite",
|
||||
];
|
||||
|
||||
let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
|
||||
if Path::new(&rust_binding_path).exists() {
|
||||
fs::remove_file(&rust_binding_path)
|
||||
.expect("Failed to remove existing Rust binding file");
|
||||
}
|
||||
for module in modules {
|
||||
compile_shader_for_module(
|
||||
module,
|
||||
&out_dir,
|
||||
&fxc_path,
|
||||
shader_path.to_str().unwrap(),
|
||||
&rust_binding_path,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("src/color_text_raster.hlsl");
|
||||
compile_shader_for_module(
|
||||
"emoji_rasterization",
|
||||
&out_dir,
|
||||
&fxc_path,
|
||||
shader_path.to_str().unwrap(),
|
||||
&rust_binding_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Locate `binary` in the newest installed Windows SDK.
|
||||
pub fn find_latest_windows_sdk_binary(
|
||||
binary: &str,
|
||||
) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
|
||||
let key = windows_registry::LOCAL_MACHINE
|
||||
.open("SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0")?;
|
||||
|
||||
let install_folder: String = key.get_string("InstallationFolder")?; // "C:\Program Files (x86)\Windows Kits\10\"
|
||||
let install_folder_bin = Path::new(&install_folder).join("bin");
|
||||
|
||||
let mut versions: Vec<_> = std::fs::read_dir(&install_folder_bin)?
|
||||
.flatten()
|
||||
.filter(|entry| entry.path().is_dir())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.collect();
|
||||
|
||||
versions.sort_by_key(|s| {
|
||||
s.split('.')
|
||||
.filter_map(|p| p.parse().ok())
|
||||
.collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
let arch = match std::env::consts::ARCH {
|
||||
"x86_64" => "x64",
|
||||
"aarch64" => "arm64",
|
||||
_ => Err(format!(
|
||||
"Unsupported architecture: {}",
|
||||
std::env::consts::ARCH
|
||||
))?,
|
||||
};
|
||||
|
||||
if let Some(highest_version) = versions.last() {
|
||||
return Ok(Some(
|
||||
install_folder_bin
|
||||
.join(highest_version)
|
||||
.join(arch)
|
||||
.join(binary),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
|
||||
fn find_fxc_compiler() -> String {
|
||||
// Check environment variable
|
||||
if let Ok(path) = std::env::var("GPUI_FXC_PATH")
|
||||
&& Path::new(&path).exists()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
// Try to find in PATH
|
||||
// NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe`
|
||||
if let Ok(output) = std::process::Command::new("where.exe")
|
||||
.arg("fxc.exe")
|
||||
.output()
|
||||
&& output.status.success()
|
||||
{
|
||||
let path = String::from_utf8_lossy(&output.stdout);
|
||||
return path.trim().to_string();
|
||||
}
|
||||
|
||||
if let Ok(Some(path)) = find_latest_windows_sdk_binary("fxc.exe") {
|
||||
return path.to_string_lossy().into_owned();
|
||||
}
|
||||
|
||||
panic!("Failed to find fxc.exe");
|
||||
}
|
||||
|
||||
fn compile_shader_for_module(
|
||||
module: &str,
|
||||
out_dir: &str,
|
||||
fxc_path: &str,
|
||||
shader_path: &str,
|
||||
rust_binding_path: &str,
|
||||
) {
|
||||
// Compile vertex shader
|
||||
let output_file = format!("{}/{}_vs.h", out_dir, module);
|
||||
let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
|
||||
compile_shader_impl(
|
||||
fxc_path,
|
||||
&format!("{module}_vertex"),
|
||||
&output_file,
|
||||
&const_name,
|
||||
shader_path,
|
||||
"vs_4_1",
|
||||
);
|
||||
generate_rust_binding(&const_name, &output_file, rust_binding_path);
|
||||
|
||||
// Compile fragment shader
|
||||
let output_file = format!("{}/{}_ps.h", out_dir, module);
|
||||
let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
|
||||
compile_shader_impl(
|
||||
fxc_path,
|
||||
&format!("{module}_fragment"),
|
||||
&output_file,
|
||||
&const_name,
|
||||
shader_path,
|
||||
"ps_4_1",
|
||||
);
|
||||
generate_rust_binding(&const_name, &output_file, rust_binding_path);
|
||||
}
|
||||
|
||||
fn compile_shader_impl(
|
||||
fxc_path: &str,
|
||||
entry_point: &str,
|
||||
output_path: &str,
|
||||
var_name: &str,
|
||||
shader_path: &str,
|
||||
target: &str,
|
||||
) {
|
||||
let output = Command::new(fxc_path)
|
||||
.args([
|
||||
"/T",
|
||||
target,
|
||||
"/E",
|
||||
entry_point,
|
||||
"/Fh",
|
||||
output_path,
|
||||
"/Vn",
|
||||
var_name,
|
||||
"/O3",
|
||||
shader_path,
|
||||
])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
if result.status.success() {
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"cargo::error=Shader compilation failed for {}:\n{}",
|
||||
entry_point,
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("cargo::error=Failed to run fxc for {}: {}", entry_point, e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
|
||||
let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
|
||||
let const_definition = {
|
||||
let global_var_start = header_content.find("const BYTE").unwrap();
|
||||
let global_var = &header_content[global_var_start..];
|
||||
let equal = global_var.find('=').unwrap();
|
||||
global_var[equal + 1..].trim()
|
||||
};
|
||||
let rust_binding = format!(
|
||||
"const {}: &[u8] = &{}\n",
|
||||
const_name,
|
||||
const_definition.replace('{', "[").replace('}', "]")
|
||||
);
|
||||
let mut options = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(output_path)
|
||||
.expect("Failed to open Rust binding file");
|
||||
options
|
||||
.write_all(rust_binding.as_bytes())
|
||||
.expect("Failed to write Rust binding file");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "windows", not(debug_assertions)))]
|
||||
use shader_compilation::compile_shaders;
|
||||
49
crates/gpui_windows/src/alpha_correction.hlsl
Normal file
49
crates/gpui_windows/src/alpha_correction.hlsl
Normal file
@@ -0,0 +1,49 @@
|
||||
// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.hlsl
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
float color_brightness(float3 color) {
|
||||
// REC. 601 luminance coefficients for perceived brightness
|
||||
return dot(color, float3(0.30f, 0.59f, 0.11f));
|
||||
}
|
||||
|
||||
float light_on_dark_contrast(float enhancedContrast, float3 color) {
|
||||
float brightness = color_brightness(color);
|
||||
float multiplier = saturate(4.0f * (0.75f - brightness));
|
||||
return enhancedContrast * multiplier;
|
||||
}
|
||||
|
||||
float enhance_contrast(float alpha, float k) {
|
||||
return alpha * (k + 1.0f) / (alpha * k + 1.0f);
|
||||
}
|
||||
|
||||
float3 enhance_contrast3(float3 alpha, float k) {
|
||||
return alpha * (k + 1.0f) / (alpha * k + 1.0f);
|
||||
}
|
||||
|
||||
float apply_alpha_correction(float a, float b, float4 g) {
|
||||
float brightness_adjustment = g.x * b + g.y;
|
||||
float correction = brightness_adjustment * a + (g.z * b + g.w);
|
||||
return a + a * (1.0f - a) * correction;
|
||||
}
|
||||
|
||||
float3 apply_alpha_correction3(float3 a, float3 b, float4 g) {
|
||||
float3 brightness_adjustment = g.x * b + g.y;
|
||||
float3 correction = brightness_adjustment * a + (g.z * b + g.w);
|
||||
return a + a * (1.0f - a) * correction;
|
||||
}
|
||||
|
||||
float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
|
||||
float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
|
||||
float brightness = color_brightness(color);
|
||||
|
||||
float contrasted = enhance_contrast(sample, enhanced_contrast);
|
||||
return apply_alpha_correction(contrasted, brightness, gamma_ratios);
|
||||
}
|
||||
|
||||
float3 apply_contrast_and_gamma_correction3(float3 sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
|
||||
float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
|
||||
|
||||
float3 contrasted = enhance_contrast3(sample, enhanced_contrast);
|
||||
return apply_alpha_correction3(contrasted, color, gamma_ratios);
|
||||
}
|
||||
388
crates/gpui_windows/src/clipboard.rs
Normal file
388
crates/gpui_windows/src/clipboard.rs
Normal file
@@ -0,0 +1,388 @@
|
||||
use std::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<()> {
|
||||
let wide: Vec<u16> = item.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(&item.text).to_ne_bytes();
|
||||
set_clipboard_bytes(&hash_bytes, *CLIPBOARD_HASH_FORMAT)?;
|
||||
|
||||
let wide: Vec<u16> = metadata.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_clipboard_bytes(&wide, *CLIPBOARD_METADATA_FORMAT)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_image(item: &Image) -> Result<()> {
|
||||
let native_format = match item.format {
|
||||
ImageFormat::Svg => Some(*CLIPBOARD_SVG_FORMAT),
|
||||
ImageFormat::Gif => Some(*CLIPBOARD_GIF_FORMAT),
|
||||
ImageFormat::Png => Some(*CLIPBOARD_PNG_FORMAT),
|
||||
ImageFormat::Jpeg => Some(*CLIPBOARD_JPG_FORMAT),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(format) = native_format {
|
||||
set_clipboard_bytes(item.bytes(), format)?;
|
||||
}
|
||||
|
||||
// Also provide a PNG copy for broad compatibility.
|
||||
// SVG can't be rasterized by the image crate, so skip it.
|
||||
if item.format != ImageFormat::Svg && native_format != Some(*CLIPBOARD_PNG_FORMAT) {
|
||||
if let Some(png_bytes) = convert_to_png(item.bytes(), item.format) {
|
||||
set_clipboard_bytes(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_to_png(bytes: &[u8], format: ImageFormat) -> Option<Vec<u8>> {
|
||||
let img_format = gpui_to_image_format(format)?;
|
||||
let image = image::load_from_memory_with_format(bytes, img_format)
|
||||
.map_err(|e| log::warn!("Failed to decode image for PNG conversion: {e}"))
|
||||
.ok()?;
|
||||
let mut buf = Vec::new();
|
||||
image
|
||||
.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
|
||||
.map_err(|e| log::warn!("Failed to encode PNG: {e}"))
|
||||
.ok()?;
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
fn read_string() -> Option<ClipboardEntry> {
|
||||
let text = get_clipboard_string(CF_UNICODETEXT.0 as u32)?;
|
||||
let metadata = read_clipboard_metadata(&text);
|
||||
Some(ClipboardEntry::String(ClipboardString { text, metadata }))
|
||||
}
|
||||
|
||||
fn read_clipboard_metadata(text: &str) -> Option<String> {
|
||||
let locked = get_clipboard_data(*CLIPBOARD_HASH_FORMAT)?;
|
||||
let hash_bytes: [u8; 8] = locked.as_bytes().get(..8)?.try_into().ok()?;
|
||||
let hash = u64::from_ne_bytes(hash_bytes);
|
||||
if hash != ClipboardString::text_hash(text) {
|
||||
return None;
|
||||
}
|
||||
get_clipboard_string(*CLIPBOARD_METADATA_FORMAT)
|
||||
}
|
||||
|
||||
fn read_image(format: u32) -> Option<ClipboardEntry> {
|
||||
let locked = get_clipboard_data(format)?;
|
||||
let (bytes, image_format) = if format == CF_DIB.0 as u32 {
|
||||
(convert_dib_to_bmp(locked.as_bytes())?, ImageFormat::Bmp)
|
||||
} else {
|
||||
let image_format = *IMAGE_FORMATS_MAP.get(&format)?;
|
||||
(locked.as_bytes().to_vec(), image_format)
|
||||
};
|
||||
let id = hash(&bytes);
|
||||
Some(ClipboardEntry::Image(Image {
|
||||
format: image_format,
|
||||
bytes,
|
||||
id,
|
||||
}))
|
||||
}
|
||||
|
||||
fn read_files() -> Option<ClipboardEntry> {
|
||||
let locked = get_clipboard_data(CF_HDROP.0 as u32)?;
|
||||
let hdrop = HDROP(locked.ptr as *mut _);
|
||||
let mut filenames = Vec::new();
|
||||
with_file_names(hdrop, |name| filenames.push(std::path::PathBuf::from(name)));
|
||||
Some(ClipboardEntry::ExternalPaths(ExternalPaths(
|
||||
filenames.into(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// DIB is BMP without the 14-byte BITMAPFILEHEADER. Prepend one.
|
||||
fn convert_dib_to_bmp(dib: &[u8]) -> Option<Vec<u8>> {
|
||||
if dib.len() < 40 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let header_size = u32::from_le_bytes(dib[0..4].try_into().ok()?);
|
||||
let bit_count = u16::from_le_bytes(dib[14..16].try_into().ok()?);
|
||||
let compression = u32::from_le_bytes(dib[16..20].try_into().ok()?);
|
||||
|
||||
let color_table_size = if bit_count <= 8 {
|
||||
let colors_used = u32::from_le_bytes(dib[32..36].try_into().ok()?);
|
||||
(if colors_used == 0 {
|
||||
1u32 << bit_count
|
||||
} else {
|
||||
colors_used
|
||||
}) * 4
|
||||
} else if compression == 3 {
|
||||
12 // BI_BITFIELDS
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let pixel_offset = 14 + header_size + color_table_size;
|
||||
let file_size = 14 + dib.len() as u32;
|
||||
|
||||
let mut bmp = Vec::with_capacity(file_size as usize);
|
||||
bmp.extend_from_slice(b"BM");
|
||||
bmp.extend_from_slice(&file_size.to_le_bytes());
|
||||
bmp.extend_from_slice(&[0u8; 4]); // reserved
|
||||
bmp.extend_from_slice(&pixel_offset.to_le_bytes());
|
||||
bmp.extend_from_slice(dib);
|
||||
Some(bmp)
|
||||
}
|
||||
|
||||
fn log_unsupported_clipboard_formats() {
|
||||
let count = unsafe { CountClipboardFormats() };
|
||||
let mut format = 0;
|
||||
for _ in 0..count {
|
||||
format = unsafe { EnumClipboardFormats(format) };
|
||||
let mut buffer = [0u16; 64];
|
||||
unsafe { GetClipboardFormatNameW(format, &mut buffer) };
|
||||
let format_name = String::from_utf16_lossy(&buffer);
|
||||
log::warn!(
|
||||
"Try to paste with unsupported clipboard format: {}, {}.",
|
||||
format,
|
||||
format_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn gpui_to_image_format(value: ImageFormat) -> Option<image::ImageFormat> {
|
||||
match value {
|
||||
ImageFormat::Png => Some(image::ImageFormat::Png),
|
||||
ImageFormat::Jpeg => Some(image::ImageFormat::Jpeg),
|
||||
ImageFormat::Webp => Some(image::ImageFormat::WebP),
|
||||
ImageFormat::Gif => Some(image::ImageFormat::Gif),
|
||||
ImageFormat::Bmp => Some(image::ImageFormat::Bmp),
|
||||
ImageFormat::Tiff => Some(image::ImageFormat::Tiff),
|
||||
other => {
|
||||
log::warn!("No image crate equivalent for format: {other:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ClipboardGuard;
|
||||
|
||||
impl ClipboardGuard {
|
||||
fn open() -> Option<Self> {
|
||||
match unsafe { OpenClipboard(None) } {
|
||||
Ok(()) => Some(Self),
|
||||
Err(e) => {
|
||||
log::error!("Failed to open clipboard: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ClipboardGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = unsafe { CloseClipboard() } {
|
||||
log::error!("Failed to close clipboard: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LockedGlobal {
|
||||
global: HGLOBAL,
|
||||
ptr: *const u8,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl LockedGlobal {
|
||||
fn lock(global: HGLOBAL) -> Option<Self> {
|
||||
let size = unsafe { GlobalSize(global) };
|
||||
let ptr = unsafe { GlobalLock(global) };
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
global,
|
||||
ptr: ptr as *const u8,
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LockedGlobal {
|
||||
fn drop(&mut self) {
|
||||
unsafe { GlobalUnlock(self.global).ok() };
|
||||
}
|
||||
}
|
||||
44
crates/gpui_windows/src/color_text_raster.hlsl
Normal file
44
crates/gpui_windows/src/color_text_raster.hlsl
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "alpha_correction.hlsl"
|
||||
|
||||
struct RasterVertexOutput {
|
||||
float4 position : SV_Position;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
};
|
||||
|
||||
RasterVertexOutput emoji_rasterization_vertex(uint vertexID : SV_VERTEXID)
|
||||
{
|
||||
RasterVertexOutput output;
|
||||
output.texcoord = float2((vertexID << 1) & 2, vertexID & 2);
|
||||
output.position = float4(output.texcoord * 2.0f - 1.0f, 0.0f, 1.0f);
|
||||
output.position.y = -output.position.y;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
struct PixelInput {
|
||||
float4 position: SV_Position;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct Bounds {
|
||||
int2 origin;
|
||||
int2 size;
|
||||
};
|
||||
|
||||
Texture2D<float> t_layer : register(t0);
|
||||
SamplerState s_layer : register(s0);
|
||||
|
||||
cbuffer GlyphLayerTextureParams : register(b0) {
|
||||
Bounds bounds;
|
||||
float4 run_color;
|
||||
float4 gamma_ratios;
|
||||
float grayscale_enhanced_contrast;
|
||||
float3 _pad;
|
||||
};
|
||||
|
||||
float4 emoji_rasterization_fragment(PixelInput input): SV_Target {
|
||||
float sample = t_layer.Sample(s_layer, input.texcoord.xy).r;
|
||||
float alpha_corrected = apply_contrast_and_gamma_correction(sample, run_color.rgb, grayscale_enhanced_contrast, gamma_ratios);
|
||||
float alpha = alpha_corrected * run_color.a;
|
||||
return float4(run_color.rgb * alpha, alpha);
|
||||
}
|
||||
207
crates/gpui_windows/src/destination_list.rs
Normal file
207
crates/gpui_windows/src/destination_list.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use itertools::Itertools;
|
||||
use smallvec::SmallVec;
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::PROPERTYKEY,
|
||||
Globalization::u_strlen,
|
||||
System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance, StructuredStorage::PROPVARIANT},
|
||||
UI::{
|
||||
Controls::INFOTIPSIZE,
|
||||
Shell::{
|
||||
Common::{IObjectArray, IObjectCollection},
|
||||
DestinationList, EnumerableObjectCollection, ICustomDestinationList, IShellLinkW,
|
||||
PropertiesSystem::IPropertyStore,
|
||||
ShellLink,
|
||||
},
|
||||
},
|
||||
},
|
||||
core::{GUID, HSTRING, Interface},
|
||||
};
|
||||
|
||||
use gpui::{Action, MenuItem, SharedString};
|
||||
|
||||
pub(crate) struct JumpList {
|
||||
pub(crate) dock_menus: Vec<DockMenuItem>,
|
||||
pub(crate) recent_workspaces: Arc<[SmallVec<[PathBuf; 2]>]>,
|
||||
}
|
||||
|
||||
impl JumpList {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
dock_menus: Vec::default(),
|
||||
recent_workspaces: Arc::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DockMenuItem {
|
||||
pub(crate) name: SharedString,
|
||||
pub(crate) description: SharedString,
|
||||
pub(crate) action: Box<dyn Action>,
|
||||
}
|
||||
|
||||
impl DockMenuItem {
|
||||
pub(crate) fn new(item: MenuItem) -> anyhow::Result<Self> {
|
||||
match item {
|
||||
MenuItem::Action { name, action, .. } => Ok(Self {
|
||||
name: name.clone(),
|
||||
description: if name == "New Window" {
|
||||
"Opens a new window".into()
|
||||
} else {
|
||||
name
|
||||
},
|
||||
action,
|
||||
}),
|
||||
_ => anyhow::bail!("Only `MenuItem::Action` is supported for dock menu on Windows."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This code is based on the example from Microsoft:
|
||||
// https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/RecipePropertyHandler/RecipePropertyHandler.cpp
|
||||
pub(crate) fn update_jump_list(
|
||||
recent_workspaces: &[SmallVec<[PathBuf; 2]>],
|
||||
dock_menus: &[(SharedString, SharedString)],
|
||||
) -> anyhow::Result<Vec<SmallVec<[PathBuf; 2]>>> {
|
||||
let (list, removed) = create_destination_list()?;
|
||||
add_recent_folders(&list, recent_workspaces, removed.as_ref())?;
|
||||
add_dock_menu(&list, dock_menus)?;
|
||||
unsafe { list.CommitList() }?;
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
// Copied from:
|
||||
// https://github.com/microsoft/windows-rs/blob/0fc3c2e5a13d4316d242bdeb0a52af611eba8bd4/crates/libs/windows/src/Windows/Win32/Storage/EnhancedStorage/mod.rs#L1881
|
||||
const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY {
|
||||
fmtid: GUID::from_u128(0xf29f85e0_4ff9_1068_ab91_08002b27b3d9),
|
||||
pid: 2,
|
||||
};
|
||||
|
||||
fn create_destination_list() -> anyhow::Result<(ICustomDestinationList, Vec<SmallVec<[PathBuf; 2]>>)>
|
||||
{
|
||||
let list: ICustomDestinationList =
|
||||
unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER) }?;
|
||||
|
||||
let mut slots = 0;
|
||||
let user_removed: IObjectArray = unsafe { list.BeginList(&mut slots) }?;
|
||||
|
||||
let count = unsafe { user_removed.GetCount() }?;
|
||||
if count == 0 {
|
||||
return Ok((list, Vec::new()));
|
||||
}
|
||||
|
||||
let mut removed = Vec::with_capacity(count as usize);
|
||||
for i in 0..count {
|
||||
let shell_link: IShellLinkW = unsafe { user_removed.GetAt(i)? };
|
||||
let description = {
|
||||
// INFOTIPSIZE is the maximum size of the buffer
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinkw-getdescription
|
||||
let mut buffer = [0u16; INFOTIPSIZE as usize];
|
||||
unsafe { shell_link.GetDescription(&mut buffer)? };
|
||||
let len = unsafe { u_strlen(buffer.as_ptr()) };
|
||||
String::from_utf16_lossy(&buffer[..len as usize])
|
||||
};
|
||||
let args = description.split('\n').map(PathBuf::from).collect();
|
||||
|
||||
removed.push(args);
|
||||
}
|
||||
|
||||
Ok((list, removed))
|
||||
}
|
||||
|
||||
fn add_dock_menu(
|
||||
list: &ICustomDestinationList,
|
||||
dock_menus: &[(SharedString, SharedString)],
|
||||
) -> anyhow::Result<()> {
|
||||
unsafe {
|
||||
let tasks: IObjectCollection =
|
||||
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
|
||||
for (idx, (name, description)) in dock_menus.iter().enumerate() {
|
||||
let argument = HSTRING::from(format!("--dock-action {}", idx));
|
||||
let description = HSTRING::from(description.as_str());
|
||||
let display = name.as_str();
|
||||
let task = create_shell_link(argument, description, None, display)?;
|
||||
tasks.AddObject(&task)?;
|
||||
}
|
||||
list.AddUserTasks(&tasks)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn add_recent_folders(
|
||||
list: &ICustomDestinationList,
|
||||
entries: &[SmallVec<[PathBuf; 2]>],
|
||||
removed: &Vec<SmallVec<[PathBuf; 2]>>,
|
||||
) -> anyhow::Result<()> {
|
||||
unsafe {
|
||||
let tasks: IObjectCollection =
|
||||
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
|
||||
|
||||
for folder_path in entries.iter().filter(|path| !removed.contains(path)) {
|
||||
let argument = HSTRING::from(
|
||||
folder_path
|
||||
.iter()
|
||||
.map(|path| format!("\"{}\"", path.display()))
|
||||
.join(" "),
|
||||
);
|
||||
|
||||
let description = HSTRING::from(
|
||||
folder_path
|
||||
.iter()
|
||||
.map(|path| path.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
);
|
||||
// simulate folder icon
|
||||
// https://github.com/microsoft/vscode/blob/7a5dc239516a8953105da34f84bae152421a8886/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts#L380
|
||||
let icon = HSTRING::from("explorer.exe");
|
||||
|
||||
let display = folder_path
|
||||
.iter()
|
||||
.map(|p| {
|
||||
p.file_name()
|
||||
.map(|name| name.to_string_lossy())
|
||||
.unwrap_or_else(|| p.to_string_lossy())
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
tasks.AddObject(&create_shell_link(
|
||||
argument,
|
||||
description,
|
||||
Some(icon),
|
||||
&display,
|
||||
)?)?;
|
||||
}
|
||||
|
||||
if tasks.GetCount().unwrap_or(0) > 0 {
|
||||
list.AppendCategory(&HSTRING::from("Recent Folders"), &tasks)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_shell_link(
|
||||
argument: HSTRING,
|
||||
description: HSTRING,
|
||||
icon: Option<HSTRING>,
|
||||
display: &str,
|
||||
) -> anyhow::Result<IShellLinkW> {
|
||||
unsafe {
|
||||
let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
|
||||
let exe_path = HSTRING::from(std::env::current_exe()?.as_os_str());
|
||||
link.SetPath(&exe_path)?;
|
||||
link.SetArguments(&argument)?;
|
||||
link.SetDescription(&description)?;
|
||||
if let Some(icon) = icon {
|
||||
link.SetIconLocation(&icon, 0)?;
|
||||
}
|
||||
let store: IPropertyStore = link.cast()?;
|
||||
let title = PROPVARIANT::from(display);
|
||||
store.SetValue(&PKEY_TITLE, &title)?;
|
||||
store.Commit()?;
|
||||
|
||||
Ok(link)
|
||||
}
|
||||
}
|
||||
359
crates/gpui_windows/src/direct_manipulation.rs
Normal file
359
crates/gpui_windows/src/direct_manipulation.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use ::util::ResultExt;
|
||||
use anyhow::Result;
|
||||
use gpui::*;
|
||||
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)
|
||||
}
|
||||
1920
crates/gpui_windows/src/direct_write.rs
Normal file
1920
crates/gpui_windows/src/direct_write.rs
Normal file
File diff suppressed because it is too large
Load Diff
320
crates/gpui_windows/src/directx_atlas.rs
Normal file
320
crates/gpui_windows/src/directx_atlas.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
use collections::FxHashMap;
|
||||
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::{
|
||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels,
|
||||
PlatformAtlas, Point, Size,
|
||||
};
|
||||
|
||||
pub(crate) struct DirectXAtlas(Mutex<DirectXAtlasState>);
|
||||
|
||||
struct DirectXAtlasState {
|
||||
device: ID3D11Device,
|
||||
device_context: ID3D11DeviceContext,
|
||||
monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
|
||||
polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
|
||||
subpixel_textures: AtlasTextureList<DirectXAtlasTexture>,
|
||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
||||
}
|
||||
|
||||
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(DirectXAtlasState {
|
||||
device: device.clone(),
|
||||
device_context: device_context.clone(),
|
||||
monochrome_textures: Default::default(),
|
||||
polychrome_textures: Default::default(),
|
||||
subpixel_textures: Default::default(),
|
||||
tiles_by_key: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn get_texture_view(
|
||||
&self,
|
||||
id: AtlasTextureId,
|
||||
) -> [Option<ID3D11ShaderResourceView>; 1] {
|
||||
let lock = self.0.lock();
|
||||
let tex = lock.texture(id);
|
||||
tex.view.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_device_lost(
|
||||
&self,
|
||||
device: &ID3D11Device,
|
||||
device_context: &ID3D11DeviceContext,
|
||||
) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.device = device.clone();
|
||||
lock.device_context = device_context.clone();
|
||||
lock.monochrome_textures = AtlasTextureList::default();
|
||||
lock.polychrome_textures = AtlasTextureList::default();
|
||||
lock.subpixel_textures = AtlasTextureList::default();
|
||||
lock.tiles_by_key.clear();
|
||||
}
|
||||
}
|
||||
|
||||
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>> {
|
||||
let mut lock = self.0.lock();
|
||||
if let Some(tile) = lock.tiles_by_key.get(key) {
|
||||
Ok(Some(*tile))
|
||||
} else {
|
||||
let Some((size, bytes)) = build()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let tile = lock
|
||||
.allocate(size, key.texture_kind())
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to allocate"))?;
|
||||
let texture = lock.texture(tile.texture_id);
|
||||
texture.upload(&lock.device_context, tile.bounds, &bytes);
|
||||
lock.tiles_by_key.insert(key.clone(), tile);
|
||||
Ok(Some(tile))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self, key: &AtlasKey) {
|
||||
let mut lock = self.0.lock();
|
||||
|
||||
let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let textures = match id.kind {
|
||||
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
|
||||
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
|
||||
AtlasTextureKind::Subpixel => &mut lock.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.decrement_ref_count();
|
||||
if texture.is_unreferenced() {
|
||||
textures.free_list.push(texture.id.index as usize);
|
||||
} else {
|
||||
*texture_slot = Some(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectXAtlasState {
|
||||
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) -> &DirectXAtlasTexture {
|
||||
match id.kind {
|
||||
AtlasTextureKind::Monochrome => &self.monochrome_textures[id.index as usize]
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
AtlasTextureKind::Polychrome => &self.polychrome_textures[id.index as usize]
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
AtlasTextureKind::Subpixel => {
|
||||
&self.subpixel_textures[id.index as usize].as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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],
|
||||
) {
|
||||
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),
|
||||
}
|
||||
}
|
||||
194
crates/gpui_windows/src/directx_devices.rs
Normal file
194
crates/gpui_windows/src/directx_devices.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use anyhow::{Context, Result};
|
||||
use itertools::Itertools;
|
||||
use util::ResultExt;
|
||||
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"
|
||||
))
|
||||
}
|
||||
}
|
||||
1957
crates/gpui_windows/src/directx_renderer.rs
Normal file
1957
crates/gpui_windows/src/directx_renderer.rs
Normal file
File diff suppressed because it is too large
Load Diff
180
crates/gpui_windows/src/dispatcher.rs
Normal file
180
crates/gpui_windows/src/dispatcher.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
thread::{ThreadId, current},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use util::ResultExt;
|
||||
use windows::{
|
||||
System::Threading::{
|
||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority,
|
||||
},
|
||||
Win32::{
|
||||
Foundation::{LPARAM, WPARAM},
|
||||
Media::{timeBeginPeriod, timeEndPeriod},
|
||||
System::Threading::{GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_TIME_CRITICAL},
|
||||
UI::WindowsAndMessaging::PostMessageW,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{HWND, SafeHwnd, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD};
|
||||
use gpui::{
|
||||
GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueSender, RunnableVariant,
|
||||
TaskTiming, ThreadTaskTimings, 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: WorkItemPriority, runnable: RunnableVariant) {
|
||||
let handler = {
|
||||
let mut task_wrapper = Some(runnable);
|
||||
WorkItemHandler::new(move |_| {
|
||||
let runnable = task_wrapper.take().unwrap();
|
||||
Self::execute_runnable(runnable);
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
ThreadPool::RunWithPriorityAsync(&handler, priority).log_err();
|
||||
}
|
||||
|
||||
fn dispatch_on_threadpool_after(&self, runnable: RunnableVariant, duration: Duration) {
|
||||
let handler = {
|
||||
let mut task_wrapper = Some(runnable);
|
||||
TimerElapsedHandler::new(move |_| {
|
||||
let runnable = task_wrapper.take().unwrap();
|
||||
Self::execute_runnable(runnable);
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn execute_runnable(runnable: RunnableVariant) {
|
||||
let start = Instant::now();
|
||||
|
||||
let location = runnable.metadata().location;
|
||||
let mut timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
gpui::profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
|
||||
let end = Instant::now();
|
||||
timing.end = Some(end);
|
||||
|
||||
gpui::profiler::add_task_timing(timing);
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDispatcher for WindowsDispatcher {
|
||||
fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
|
||||
let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock();
|
||||
ThreadTaskTimings::convert(&global_thread_timings)
|
||||
}
|
||||
|
||||
fn get_current_thread_timings(&self) -> gpui::ThreadTaskTimings {
|
||||
gpui::profiler::get_current_thread_task_timings()
|
||||
}
|
||||
|
||||
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 => WorkItemPriority::High,
|
||||
Priority::Medium => WorkItemPriority::Normal,
|
||||
Priority::Low => WorkItemPriority::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);
|
||||
}
|
||||
util::defer(Box::new(|| unsafe {
|
||||
timeEndPeriod(1);
|
||||
}))
|
||||
}
|
||||
}
|
||||
204
crates/gpui_windows/src/display.rs
Normal file
204
crates/gpui_windows/src/display.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use itertools::Itertools;
|
||||
use smallvec::SmallVec;
|
||||
use std::rc::Rc;
|
||||
use util::ResultExt;
|
||||
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))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
1654
crates/gpui_windows/src/events.rs
Normal file
1654
crates/gpui_windows/src/events.rs
Normal file
File diff suppressed because it is too large
Load Diff
40
crates/gpui_windows/src/gpui_windows.rs
Normal file
40
crates/gpui_windows/src/gpui_windows.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
mod clipboard;
|
||||
mod destination_list;
|
||||
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_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_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
crates/gpui_windows/src/keyboard.rs
Normal file
371
crates/gpui_windows/src/keyboard.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use windows::Win32::UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
||||
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
||||
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
||||
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
},
|
||||
WindowsAndMessaging::KL_NAMELENGTH,
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
};
|
||||
|
||||
pub(crate) struct WindowsKeyboardLayout {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsKeyboardMapper {
|
||||
key_to_vkey: HashMap<String, (u16, bool)>,
|
||||
vkey_to_key: HashMap<u16, String>,
|
||||
vkey_to_shifted: HashMap<u16, String>,
|
||||
}
|
||||
|
||||
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
mut keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
|
||||
else {
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
if shifted_key && keystroke.modifiers.shift {
|
||||
log::warn!(
|
||||
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
|
||||
keystroke.key
|
||||
);
|
||||
}
|
||||
|
||||
let shift = shifted_key || keystroke.modifiers.shift;
|
||||
keystroke.modifiers.shift = false;
|
||||
|
||||
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
|
||||
log::error!(
|
||||
"Failed to map key equivalent '{:?}' to a valid key",
|
||||
keystroke
|
||||
);
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
|
||||
keystroke.key = if shift {
|
||||
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
|
||||
log::error!(
|
||||
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
|
||||
keystroke,
|
||||
vkey
|
||||
);
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
shifted_key
|
||||
} else {
|
||||
key.clone()
|
||||
};
|
||||
|
||||
let modifiers = Modifiers {
|
||||
shift,
|
||||
..keystroke.modifiers
|
||||
};
|
||||
|
||||
KeybindingKeystroke::new(keystroke, modifiers, key)
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardLayout {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let mut buffer = [0u16; KL_NAMELENGTH as usize]; // KL_NAMELENGTH includes the null terminator
|
||||
unsafe { GetKeyboardLayoutNameW(&mut buffer)? };
|
||||
let id = String::from_utf16_lossy(&buffer[..buffer.len() - 1]); // Remove the null terminator
|
||||
let entry = windows_registry::LOCAL_MACHINE.open(format!(
|
||||
"System\\CurrentControlSet\\Control\\Keyboard Layouts\\{id}"
|
||||
))?;
|
||||
let name = entry.get_string("Layout Text")?;
|
||||
Ok(Self { id, name })
|
||||
}
|
||||
|
||||
pub(crate) fn unknown() -> Self {
|
||||
Self {
|
||||
id: "unknown".to_string(),
|
||||
name: "unknown".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardMapper {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut key_to_vkey = HashMap::default();
|
||||
let mut vkey_to_key = HashMap::default();
|
||||
let mut vkey_to_shifted = HashMap::default();
|
||||
for vkey in CANDIDATE_VKEYS {
|
||||
if let Some(key) = get_key_from_vkey(*vkey) {
|
||||
key_to_vkey.insert(key.clone(), (vkey.0, false));
|
||||
vkey_to_key.insert(vkey.0, key);
|
||||
}
|
||||
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
|
||||
if scan_code == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
|
||||
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
|
||||
vkey_to_shifted.insert(vkey.0, shifted_key);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
key_to_vkey,
|
||||
vkey_to_key,
|
||||
vkey_to_shifted,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
|
||||
if use_key_equivalents {
|
||||
get_vkey_from_key_with_us_layout(key)
|
||||
} else {
|
||||
self.key_to_vkey.get(key).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_keystroke_key(
|
||||
vkey: VIRTUAL_KEY,
|
||||
scan_code: u32,
|
||||
modifiers: &mut Modifiers,
|
||||
) -> Option<String> {
|
||||
if modifiers.shift && need_to_convert_to_shifted_key(vkey) {
|
||||
get_shifted_key(vkey, scan_code).inspect(|_| {
|
||||
modifiers.shift = false;
|
||||
})
|
||||
} else {
|
||||
get_key_from_vkey(vkey)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key_from_vkey(vkey: VIRTUAL_KEY) -> Option<String> {
|
||||
let key_data = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_CHAR) };
|
||||
if key_data == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// The high word contains dead key flag, the low word contains the character
|
||||
let key = char::from_u32(key_data & 0xFFFF)?;
|
||||
|
||||
Some(key.to_ascii_lowercase().to_string())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
|
||||
matches!(
|
||||
vkey,
|
||||
VK_OEM_3
|
||||
| VK_OEM_MINUS
|
||||
| VK_OEM_PLUS
|
||||
| VK_OEM_4
|
||||
| VK_OEM_5
|
||||
| VK_OEM_6
|
||||
| VK_OEM_1
|
||||
| VK_OEM_7
|
||||
| VK_OEM_COMMA
|
||||
| VK_OEM_PERIOD
|
||||
| VK_OEM_2
|
||||
| VK_OEM_102
|
||||
| VK_OEM_8
|
||||
| VK_ABNT_C1
|
||||
| VK_0
|
||||
| VK_1
|
||||
| VK_2
|
||||
| VK_3
|
||||
| VK_4
|
||||
| VK_5
|
||||
| VK_6
|
||||
| VK_7
|
||||
| VK_8
|
||||
| VK_9
|
||||
)
|
||||
}
|
||||
|
||||
fn get_shifted_key(vkey: VIRTUAL_KEY, scan_code: u32) -> Option<String> {
|
||||
generate_key_char(vkey, scan_code, false, true, false)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_key_char(
|
||||
vkey: VIRTUAL_KEY,
|
||||
scan_code: u32,
|
||||
control: bool,
|
||||
shift: bool,
|
||||
alt: bool,
|
||||
) -> Option<String> {
|
||||
let mut state = [0; 256];
|
||||
if control {
|
||||
state[VK_CONTROL.0 as usize] = 0x80;
|
||||
}
|
||||
if shift {
|
||||
state[VK_SHIFT.0 as usize] = 0x80;
|
||||
}
|
||||
if alt {
|
||||
state[VK_MENU.0 as usize] = 0x80;
|
||||
}
|
||||
|
||||
let mut buffer = [0; 8];
|
||||
let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 0x5) };
|
||||
|
||||
match len {
|
||||
len if len > 0 => String::from_utf16(&buffer[..len as usize])
|
||||
.ok()
|
||||
.filter(|candidate| {
|
||||
!candidate.is_empty() && !candidate.chars().next().unwrap().is_control()
|
||||
}),
|
||||
len if len < 0 => String::from_utf16(&buffer[..(-len as usize)]).ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
||||
match key {
|
||||
// ` => VK_OEM_3
|
||||
"`" => Some((VK_OEM_3.0, false)),
|
||||
"~" => Some((VK_OEM_3.0, true)),
|
||||
"1" => Some((VK_1.0, false)),
|
||||
"!" => Some((VK_1.0, true)),
|
||||
"2" => Some((VK_2.0, false)),
|
||||
"@" => Some((VK_2.0, true)),
|
||||
"3" => Some((VK_3.0, false)),
|
||||
"#" => Some((VK_3.0, true)),
|
||||
"4" => Some((VK_4.0, false)),
|
||||
"$" => Some((VK_4.0, true)),
|
||||
"5" => Some((VK_5.0, false)),
|
||||
"%" => Some((VK_5.0, true)),
|
||||
"6" => Some((VK_6.0, false)),
|
||||
"^" => Some((VK_6.0, true)),
|
||||
"7" => Some((VK_7.0, false)),
|
||||
"&" => Some((VK_7.0, true)),
|
||||
"8" => Some((VK_8.0, false)),
|
||||
"*" => Some((VK_8.0, true)),
|
||||
"9" => Some((VK_9.0, false)),
|
||||
"(" => Some((VK_9.0, true)),
|
||||
"0" => Some((VK_0.0, false)),
|
||||
")" => Some((VK_0.0, true)),
|
||||
"-" => Some((VK_OEM_MINUS.0, false)),
|
||||
"_" => Some((VK_OEM_MINUS.0, true)),
|
||||
"=" => Some((VK_OEM_PLUS.0, false)),
|
||||
"+" => Some((VK_OEM_PLUS.0, true)),
|
||||
"[" => Some((VK_OEM_4.0, false)),
|
||||
"{" => Some((VK_OEM_4.0, true)),
|
||||
"]" => Some((VK_OEM_6.0, false)),
|
||||
"}" => Some((VK_OEM_6.0, true)),
|
||||
"\\" => Some((VK_OEM_5.0, false)),
|
||||
"|" => Some((VK_OEM_5.0, true)),
|
||||
";" => Some((VK_OEM_1.0, false)),
|
||||
":" => Some((VK_OEM_1.0, true)),
|
||||
"'" => Some((VK_OEM_7.0, false)),
|
||||
"\"" => Some((VK_OEM_7.0, true)),
|
||||
"," => Some((VK_OEM_COMMA.0, false)),
|
||||
"<" => Some((VK_OEM_COMMA.0, true)),
|
||||
"." => Some((VK_OEM_PERIOD.0, false)),
|
||||
">" => Some((VK_OEM_PERIOD.0, true)),
|
||||
"/" => Some((VK_OEM_2.0, false)),
|
||||
"?" => Some((VK_OEM_2.0, true)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
||||
VK_OEM_3,
|
||||
VK_OEM_MINUS,
|
||||
VK_OEM_PLUS,
|
||||
VK_OEM_4,
|
||||
VK_OEM_5,
|
||||
VK_OEM_6,
|
||||
VK_OEM_1,
|
||||
VK_OEM_7,
|
||||
VK_OEM_COMMA,
|
||||
VK_OEM_PERIOD,
|
||||
VK_OEM_2,
|
||||
VK_OEM_102,
|
||||
VK_OEM_8,
|
||||
VK_ABNT_C1,
|
||||
VK_0,
|
||||
VK_1,
|
||||
VK_2,
|
||||
VK_3,
|
||||
VK_4,
|
||||
VK_5,
|
||||
VK_6,
|
||||
VK_7,
|
||||
VK_8,
|
||||
VK_9,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::WindowsKeyboardMapper;
|
||||
use gpui::{Keystroke, Modifiers, PlatformKeyboardMapper};
|
||||
|
||||
#[test]
|
||||
fn test_keyboard_mapper() {
|
||||
let mapper = WindowsKeyboardMapper::new();
|
||||
|
||||
// Normal case
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "a".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(*mapped.inner(), keystroke);
|
||||
assert_eq!(mapped.key(), "a");
|
||||
assert_eq!(*mapped.modifiers(), Modifiers::control());
|
||||
|
||||
// Shifted case, ctrl-$
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(*mapped.inner(), keystroke);
|
||||
assert_eq!(mapped.key(), "4");
|
||||
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
|
||||
|
||||
// Shifted case, but shift is true
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner().modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.key(), "4");
|
||||
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
|
||||
|
||||
// Windows style
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "4".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner().modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.inner().key, "$");
|
||||
assert_eq!(mapped.key(), "4");
|
||||
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
|
||||
}
|
||||
}
|
||||
1410
crates/gpui_windows/src/platform.rs
Normal file
1410
crates/gpui_windows/src/platform.rs
Normal file
File diff suppressed because it is too large
Load Diff
1233
crates/gpui_windows/src/shaders.hlsl
Normal file
1233
crates/gpui_windows/src/shaders.hlsl
Normal file
File diff suppressed because it is too large
Load Diff
87
crates/gpui_windows/src/system_settings.rs
Normal file
87
crates/gpui_windows/src/system_settings.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
ffi::{c_uint, c_void},
|
||||
};
|
||||
|
||||
use ::util::ResultExt;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SYSTEM_PARAMETERS_INFO_ACTION,
|
||||
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
|
||||
};
|
||||
|
||||
/// Windows settings pulled from SystemParametersInfo
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub(crate) struct WindowsSystemSettings {
|
||||
pub(crate) mouse_wheel_settings: MouseWheelSettings,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub(crate) struct MouseWheelSettings {
|
||||
/// SEE: SPI_GETWHEELSCROLLCHARS
|
||||
pub(crate) wheel_scroll_chars: Cell<u32>,
|
||||
/// SEE: SPI_GETWHEELSCROLLLINES
|
||||
pub(crate) wheel_scroll_lines: Cell<u32>,
|
||||
}
|
||||
|
||||
impl WindowsSystemSettings {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut settings = Self::default();
|
||||
settings.init();
|
||||
settings
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
self.mouse_wheel_settings.update();
|
||||
}
|
||||
|
||||
pub(crate) fn update(&self, wparam: usize) {
|
||||
match SYSTEM_PARAMETERS_INFO_ACTION(wparam as u32) {
|
||||
SPI_GETWHEELSCROLLLINES | SPI_GETWHEELSCROLLCHARS => self.update_mouse_wheel_settings(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_mouse_wheel_settings(&self) {
|
||||
self.mouse_wheel_settings.update();
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseWheelSettings {
|
||||
fn update(&self) {
|
||||
self.update_wheel_scroll_chars();
|
||||
self.update_wheel_scroll_lines();
|
||||
}
|
||||
|
||||
fn update_wheel_scroll_chars(&self) {
|
||||
let mut value = c_uint::default();
|
||||
let result = unsafe {
|
||||
SystemParametersInfoW(
|
||||
SPI_GETWHEELSCROLLCHARS,
|
||||
0,
|
||||
Some((&mut value) as *mut c_uint as *mut c_void),
|
||||
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
|
||||
)
|
||||
};
|
||||
|
||||
if result.log_err() != None && self.wheel_scroll_chars.get() != value {
|
||||
self.wheel_scroll_chars.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_wheel_scroll_lines(&self) {
|
||||
let mut value = c_uint::default();
|
||||
let result = unsafe {
|
||||
SystemParametersInfoW(
|
||||
SPI_GETWHEELSCROLLLINES,
|
||||
0,
|
||||
Some((&mut value) as *mut c_uint as *mut c_void),
|
||||
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
|
||||
)
|
||||
};
|
||||
|
||||
if result.log_err() != None && self.wheel_scroll_lines.get() != value {
|
||||
self.wheel_scroll_lines.set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
191
crates/gpui_windows/src/util.rs
Normal file
191
crates/gpui_windows/src/util.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use ::util::ResultExt;
|
||||
use anyhow::Context;
|
||||
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
|
||||
}
|
||||
81
crates/gpui_windows/src/vsync.rs
Normal file
81
crates/gpui_windows/src/vsync.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::{
|
||||
sync::LazyLock,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
1560
crates/gpui_windows/src/window.rs
Normal file
1560
crates/gpui_windows/src/window.rs
Normal file
File diff suppressed because it is too large
Load Diff
53
crates/gpui_windows/src/wrapper.rs
Normal file
53
crates/gpui_windows/src/wrapper.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct SafeCursor {
|
||||
raw: HCURSOR,
|
||||
}
|
||||
|
||||
unsafe impl Send for SafeCursor {}
|
||||
unsafe impl Sync for SafeCursor {}
|
||||
|
||||
impl From<HCURSOR> for SafeCursor {
|
||||
fn from(value: HCURSOR) -> Self {
|
||||
SafeCursor { raw: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SafeCursor {
|
||||
type Target = HCURSOR;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct SafeHwnd {
|
||||
raw: HWND,
|
||||
}
|
||||
|
||||
impl SafeHwnd {
|
||||
pub(crate) fn as_raw(&self) -> HWND {
|
||||
self.raw
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for SafeHwnd {}
|
||||
unsafe impl Sync for SafeHwnd {}
|
||||
|
||||
impl From<HWND> for SafeHwnd {
|
||||
fn from(value: HWND) -> Self {
|
||||
SafeHwnd { raw: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SafeHwnd {
|
||||
type Target = HWND;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user