feat(borders): use direct2d for anti-aliasing

This commit overhauls the "Komorebi" borders implementation to use
Direct2D, which enables anti-aliasing for rounded borders.

A lot of the heavy lifting was done by @lukeyou05 in the tacky-borders
project, which this commit largely adapts to komorebi. @lukeyou05
provided an incredible amount of guidance and feedback on the
implementation of this feature on the komorebi Discord.

This commit is a squashed interactive rebase of the following commits:

238271a71e
feat(borders): initial impl of direct2d border drawing

5525a382b9
feat(borders): avoid multiple render target creation calls

431970d7b6
feat(borders): reduce redraws to improve perf

47cb19e54a
feat(borders): remove black pixels around direct2d corners

3857d1a46c
feat(borders): clean up render targets on destroy
This commit is contained in:
LGUG2Z
2024-11-03 10:10:43 -08:00
parent e707a14b8a
commit e4e94fd1a6
3 changed files with 220 additions and 91 deletions

View File

@@ -44,11 +44,15 @@ which = "7"
version = "0.58" version = "0.58"
features = [ features = [
"implement", "implement",
"Foundation_Numerics",
"Win32_System_Com", "Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray "Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation", "Win32_Foundation",
"Win32_Graphics_Dwm", "Win32_Graphics_Dwm",
"Win32_Graphics_Gdi", "Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_RemoteDesktop", "Win32_System_RemoteDesktop",
"Win32_System_Threading", "Win32_System_Threading",

View File

@@ -3,47 +3,78 @@ use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET; use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH; use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::FOCUS_STATE; use crate::border_manager::FOCUS_STATE;
use crate::border_manager::RENDER_TARGETS;
use crate::border_manager::STYLE; use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER; use crate::border_manager::Z_ORDER;
use crate::core::BorderStyle;
use crate::core::Rect;
use crate::windows_api; use crate::windows_api;
use crate::WindowsApi; use crate::WindowsApi;
use crate::WINDOWS_11; use crate::WINDOWS_11;
use std::ops::Deref;
use crate::core::BorderStyle;
use crate::core::Rect;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::mpsc; use std::sync::mpsc;
use std::time::Duration; use std::sync::LazyLock;
use windows::core::PCWSTR; use windows::Foundation::Numerics::Matrix3x2;
use windows::Win32::Foundation::BOOL; use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF; use windows::Win32::Foundation::FALSE;
use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM; use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT; use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::TRUE;
use windows::Win32::Foundation::WPARAM; use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
use windows::Win32::Graphics::Direct2D::D2D1_HWND_RENDER_TARGET_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
use windows::Win32::Graphics::Gdi::BeginPaint; use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreatePen; use windows::Win32::Graphics::Gdi::CreateRectRgn;
use windows::Win32::Graphics::Gdi::DeleteObject;
use windows::Win32::Graphics::Gdi::EndPaint; use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::InvalidateRect; use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT; use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW; use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW; use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW; use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage; use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage; use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::MSG; use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY; use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT; use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WM_SIZE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW; use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows_core::PCWSTR;
#[allow(clippy::expect_used)]
static RENDER_FACTORY: LazyLock<ID2D1Factory> = unsafe {
LazyLock::new(|| {
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
.expect("creating RENDER_FACTORY failed")
})
};
static BRUSH_PROPERTIES: LazyLock<D2D1_BRUSH_PROPERTIES> =
LazyLock::new(|| D2D1_BRUSH_PROPERTIES {
opacity: 1.0,
transform: Matrix3x2::identity(),
});
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL { pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) }; let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
@@ -83,7 +114,6 @@ impl Border {
let window_class = WNDCLASSW { let window_class = WNDCLASSW {
hInstance: h_module.into(), hInstance: h_module.into(),
lpszClassName: class_name, lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback), lpfnWndProc: Some(Self::callback),
hbrBackground: WindowsApi::create_solid_brush(0), hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default() ..Default::default()
@@ -110,32 +140,80 @@ impl Border {
let _ = TranslateMessage(&msg); let _ = TranslateMessage(&msg);
DispatchMessageW(&msg); DispatchMessageW(&msg);
} }
std::thread::sleep(Duration::from_millis(10))
} }
Ok(()) Ok(())
}); });
Ok(Self { let hwnd = hwnd_receiver.recv()?;
hwnd: hwnd_receiver.recv()?, let border = Self { hwnd };
})
// I have literally no idea, apparently this is to get rid of the black pixels
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
unsafe {
let pos: i32 = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
let hrgn = CreateRectRgn(pos, 0, pos + 1, 1);
let mut bh: DWM_BLURBEHIND = Default::default();
if !hrgn.is_invalid() {
bh = DWM_BLURBEHIND {
dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION,
fEnable: TRUE,
hRgnBlur: hrgn,
fTransitionOnMaximized: FALSE,
};
}
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
}
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
hwnd: HWND(windows_api::as_ptr!(hwnd)),
pixelSize: Default::default(),
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
};
let render_target_properties = D2D1_RENDER_TARGET_PROPERTIES {
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_UNKNOWN,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
dpiX: 96.0,
dpiY: 96.0,
..Default::default()
};
match unsafe {
RENDER_FACTORY
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
} {
Ok(render_target) => unsafe {
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
let mut render_targets = RENDER_TARGETS.lock();
render_targets.insert(hwnd, render_target);
Ok(border)
},
Err(error) => Err(error.into()),
}
} }
pub fn destroy(&self) -> color_eyre::Result<()> { pub fn destroy(&self) -> color_eyre::Result<()> {
let mut render_targets = RENDER_TARGETS.lock();
render_targets.remove(&self.hwnd);
WindowsApi::close_window(self.hwnd) WindowsApi::close_window(self.hwnd)
} }
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> { pub fn update(&self, rect: &Rect, should_invalidate: bool) -> color_eyre::Result<()> {
// Make adjustments to the border // Make adjustments to the border
let mut rect = *rect; let mut rect = *rect;
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst)); rect.add_margin(BORDER_WIDTH.load(Ordering::Relaxed));
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst)); rect.add_padding(-BORDER_OFFSET.load(Ordering::Relaxed));
// Update the position of the border if required // Update the position of the border if required
// This effectively handles WM_MOVE
// Also if I remove this no borders render at all lol
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) { if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?; WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
should_invalidate = true;
} }
// Invalidate the rect to trigger the callback to update colours etc. // Invalidate the rect to trigger the callback to update colours etc.
@@ -158,72 +236,104 @@ impl Border {
) -> LRESULT { ) -> LRESULT {
unsafe { unsafe {
match message { match message {
WM_PAINT => { WM_SIZE | WM_PAINT => {
let mut ps = PAINTSTRUCT::default(); if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
let hdc = BeginPaint(window, &mut ps); let render_targets = RENDER_TARGETS.lock();
if let Some(render_target) = render_targets.get(&(window.0 as isize)) {
// With the rect that we set in Self::update let pixel_size = D2D_SIZE_U {
match WindowsApi::window_rect(window.0 as isize) { width: rect.right as u32,
Ok(rect) => { height: rect.bottom as u32,
// Grab the focus kind for this border
let window_kind = {
FOCUS_STATE
.lock()
.get(&(window.0 as isize))
.copied()
.unwrap_or(WindowKind::Unfocused)
}; };
// Set up the brush to draw the border let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
let hpen = CreatePen( let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(window_kind_colour(window_kind)),
);
let hbrush = WindowsApi::create_solid_brush(0); let rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
// Draw the border let _ = render_target.Resize(&pixel_size);
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush); // Get window kind and color
// TODO(raggi): this is approximately the correct curvature for let window_kind = FOCUS_STATE
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but .lock()
// often the bottom right has a different shape. Furthermore if .get(&(window.0 as isize))
// the window was made with DWMWCP_ROUNDSMALL then this is the .copied()
// wrong size. In the future we should read the DWM properties .unwrap_or(WindowKind::Unfocused);
// of windows and attempt to match appropriately.
match STYLE.load() { let color = window_kind_colour(window_kind);
BorderStyle::System => { let color = D2D1_COLOR_F {
if *WINDOWS_11 { r: ((color & 0xFF) as f32) / 255.0,
// TODO: error handling g: (((color >> 8) & 0xFF) as f32) / 255.0,
let _ = b: (((color >> 16) & 0xFF) as f32) / 255.0,
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20); a: 1.0,
} else { };
// TODO: error handling
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom); if let Ok(brush) = render_target
.CreateSolidColorBrush(&color, Some(BRUSH_PROPERTIES.deref()))
{
render_target.BeginDraw();
render_target.Clear(None);
// Calculate border radius based on style
let style = match STYLE.load() {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
} }
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
};
match style {
BorderStyle::Rounded => {
let radius = 8.0 + border_width as f32 / 2.0;
let rounded_rect = D2D1_ROUNDED_RECT {
rect,
radiusX: radius,
radiusY: radius,
};
render_target.DrawRoundedRectangle(
&rounded_rect,
&brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
let rect = D2D_RECT_F {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
};
render_target.DrawRectangle(
&rect,
&brush,
border_width as f32,
None,
);
}
_ => {}
} }
BorderStyle::Rounded => {
// TODO: error handling let _ = render_target.EndDraw(None, None);
let _ = RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
} // If we don't do this we'll get spammed with WM_PAINT according to Raymond Chen
BorderStyle::Square => { // https://stackoverflow.com/questions/41783234/why-does-my-call-to-d2d1rendertargetdrawtext-result-in-a-wm-paint-being-se#comment70756781_41783234
// TODO: error handling let _ = BeginPaint(window, &mut PAINTSTRUCT::default());
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom); let _ = EndPaint(window, &PAINTSTRUCT::default());
}
} }
// TODO: error handling
let _ = DeleteObject(hpen);
// TODO: error handling
let _ = DeleteObject(hbrush);
}
Err(error) => {
tracing::error!("could not get border rect: {}", error.to_string())
} }
} }
// TODO: error handling
let _ = EndPaint(window, &ps);
LRESULT(0) LRESULT(0)
} }
WM_DESTROY => { WM_DESTROY => {

View File

@@ -1,7 +1,6 @@
#![deny(clippy::unwrap_used, clippy::expect_used)] #![deny(clippy::unwrap_used, clippy::expect_used)]
mod border; mod border;
use crate::core::BorderImplementation; use crate::core::BorderImplementation;
use crate::core::BorderStyle; use crate::core::BorderStyle;
use crate::core::WindowKind; use crate::core::WindowKind;
@@ -30,6 +29,10 @@ use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::sync::OnceLock; use std::sync::OnceLock;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
use windows::Win32::System::Threading::GetCurrentThread;
use windows::Win32::System::Threading::SetThreadPriority;
use windows::Win32::System::Threading::THREAD_PRIORITY_TIME_CRITICAL;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8); pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1); pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
@@ -57,6 +60,8 @@ lazy_static! {
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new()); static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new()); static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new()); static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
static ref RENDER_TARGETS: Mutex<HashMap<isize, ID2D1HwndRenderTarget>> =
Mutex::new(HashMap::new());
} }
pub struct Notification(pub Option<isize>); pub struct Notification(pub Option<isize>);
@@ -95,6 +100,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
borders.clear(); borders.clear();
BORDERS_MONITORS.lock().clear(); BORDERS_MONITORS.lock().clear();
FOCUS_STATE.lock().clear(); FOCUS_STATE.lock().clear();
RENDER_TARGETS.lock().clear();
let mut remaining_hwnds = vec![]; let mut remaining_hwnds = vec![];
@@ -125,13 +131,22 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
} }
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) { pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop { std::thread::spawn(move || {
match handle_notifications(wm.clone()) { unsafe {
Ok(()) => { if let Err(error) = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)
tracing::warn!("restarting finished thread"); {
tracing::error!("{error}");
} }
Err(error) => { }
tracing::warn!("restarting failed thread: {}", error);
loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
} }
} }
}); });
@@ -444,7 +459,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if rect != new_rect { if rect != new_rect {
rect = new_rect; rect = new_rect;
border.update(&rect, true)?; border.update(&rect, false)?;
} }
} }