feat(borders): add windows accent implementation

This commit adds the ability for users to select between komorebi's
implementation of borders with variable widths and the native Windows 11
implementation of thin "accent" borders.

The new border_implementation configuration option defaults to
BorderImplementation::Komorebi in order to preserve compat with existing
user configurations.

This option will be most useful for people who prefer ultra-thin
borders, and people who want borders to be animated along with
application windows if they are using the animation feature being worked
on in PR #685.
This commit is contained in:
LGUG2Z
2024-06-20 20:13:22 -07:00
parent 9a65a4ae92
commit 9e37baa88a
9 changed files with 407 additions and 218 deletions
+33 -1
View File
@@ -142,6 +142,7 @@ pub enum SocketMessage {
BorderStyle(BorderStyle), BorderStyle(BorderStyle),
BorderWidth(i32), BorderWidth(i32),
BorderOffset(i32), BorderOffset(i32),
BorderImplementation(BorderImplementation),
Transparency(bool), Transparency(bool),
TransparencyAlpha(u8), TransparencyAlpha(u8),
InvisibleBorders(Rect), InvisibleBorders(Rect),
@@ -219,7 +220,17 @@ pub enum StackbarLabel {
} }
#[derive( #[derive(
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema, Default,
Copy,
Clone,
Debug,
Eq,
PartialEq,
Display,
Serialize,
Deserialize,
JsonSchema,
ValueEnum,
)] )]
pub enum BorderStyle { pub enum BorderStyle {
#[default] #[default]
@@ -231,6 +242,27 @@ pub enum BorderStyle {
Square, Square,
} }
#[derive(
Default,
Copy,
Clone,
Debug,
Eq,
PartialEq,
Display,
Serialize,
Deserialize,
JsonSchema,
ValueEnum,
)]
pub enum BorderImplementation {
#[default]
/// Use the adjustable komorebi border implementation
Komorebi,
/// Use the thin Windows accent border implementation
Windows,
}
#[derive( #[derive(
Copy, Copy,
Clone, Clone,
+3 -11
View File
@@ -1,12 +1,9 @@
use crate::border_manager::window_kind_colour;
use crate::border_manager::WindowKind; 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::FOCUSED;
use crate::border_manager::FOCUS_STATE; use crate::border_manager::FOCUS_STATE;
use crate::border_manager::MONOCLE;
use crate::border_manager::STACK;
use crate::border_manager::STYLE; use crate::border_manager::STYLE;
use crate::border_manager::UNFOCUSED;
use crate::border_manager::Z_ORDER; use crate::border_manager::Z_ORDER;
use crate::WindowsApi; use crate::WindowsApi;
use crate::WINDOWS_11; use crate::WINDOWS_11;
@@ -165,7 +162,7 @@ impl Border {
match WindowsApi::window_rect(window) { match WindowsApi::window_rect(window) {
Ok(rect) => { Ok(rect) => {
// Grab the focus kind for this border // Grab the focus kind for this border
let focus_kind = { let window_kind = {
FOCUS_STATE FOCUS_STATE
.lock() .lock()
.get(&window.0) .get(&window.0)
@@ -177,12 +174,7 @@ impl Border {
let hpen = CreatePen( let hpen = CreatePen(
PS_SOLID | PS_INSIDEFRAME, PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst), BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(match focus_kind { COLORREF(window_kind_colour(window_kind)),
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
WindowKind::Stack => STACK.load(Ordering::SeqCst),
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
}),
); );
let hbrush = WindowsApi::create_solid_brush(0); let hbrush = WindowsApi::create_solid_brush(0);
+70 -4
View File
@@ -6,6 +6,7 @@ use crossbeam_channel::Receiver;
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell; use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume; use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::BorderImplementation;
use komorebi_core::BorderStyle; use komorebi_core::BorderStyle;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
@@ -17,6 +18,7 @@ use std::collections::HashMap;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32; use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32; use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::sync::OnceLock; use std::sync::OnceLock;
use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::HWND;
@@ -35,10 +37,13 @@ pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1); pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true); pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
lazy_static! { lazy_static! {
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom); pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System); pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
AtomicCell::new(BorderImplementation::Windows);
pub static ref FOCUSED: AtomicU32 = pub static ref FOCUSED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245)))); AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
pub static ref UNFOCUSED: AtomicU32 = pub static ref UNFOCUSED: AtomicU32 =
@@ -59,7 +64,7 @@ pub struct Notification;
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new(); static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) { pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20)) CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
} }
fn event_tx() -> Sender<Notification> { fn event_tx() -> Sender<Notification> {
@@ -109,6 +114,15 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
Ok(()) Ok(())
} }
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
match focus_kind {
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
WindowKind::Stack => STACK.load(Ordering::SeqCst),
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
}
}
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 || loop {
match handle_notifications(wm.clone()) { match handle_notifications(wm.clone()) {
@@ -125,6 +139,7 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> { pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening"); tracing::info!("listening");
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
let receiver = event_rx(); let receiver = event_rx();
event_tx().send(Notification)?; event_tx().send(Notification)?;
@@ -141,6 +156,48 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let pending_move_op = state.pending_move_op; let pending_move_op = state.pending_move_op;
drop(state); drop(state);
match IMPLEMENTATION.load() {
BorderImplementation::Windows => {
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let window_kind = if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
};
monocle
.focused_window()
.copied()
.unwrap_or_default()
.set_accent(window_kind_colour(window_kind))?;
continue 'monitors;
}
for (idx, c) in ws.containers().iter().enumerate() {
let window_kind = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
{
WindowKind::Unfocused
} else if c.windows().len() > 1 {
WindowKind::Stack
} else {
WindowKind::Single
};
c.focused_window()
.copied()
.unwrap_or_default()
.set_accent(window_kind_colour(window_kind))?;
}
}
}
}
BorderImplementation::Komorebi => {
let mut should_process_notification = true; let mut should_process_notification = true;
if monitors == previous_snapshot if monitors == previous_snapshot
@@ -175,6 +232,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// If borders are disabled // If borders are disabled
if !BORDER_ENABLED.load_consume() if !BORDER_ENABLED.load_consume()
// Or if they are temporarily disabled
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
// Or if the wm is paused // Or if the wm is paused
|| is_paused || is_paused
// Or if we are handling an alt-tab across workspaces // Or if we are handling an alt-tab across workspaces
@@ -198,7 +257,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if !ws.tile() { if !ws.tile() {
let mut to_remove = vec![]; let mut to_remove = vec![];
for (id, border) in borders.iter() { for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx { if borders_monitors.get(id).copied().unwrap_or_default()
== monitor_idx
{
border.destroy()?; border.destroy()?;
to_remove.push(id.clone()); to_remove.push(id.clone());
} }
@@ -247,7 +308,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let border_hwnd = border.hwnd; let border_hwnd = border.hwnd;
let mut to_remove = vec![]; let mut to_remove = vec![];
for (id, b) in borders.iter() { for (id, b) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx if borders_monitors.get(id).copied().unwrap_or_default()
== monitor_idx
&& border_hwnd != b.hwnd && border_hwnd != b.hwnd
{ {
b.destroy()?; b.destroy()?;
@@ -269,7 +331,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if is_maximized { if is_maximized {
let mut to_remove = vec![]; let mut to_remove = vec![];
for (id, border) in borders.iter() { for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx { if borders_monitors.get(id).copied().unwrap_or_default()
== monitor_idx
{
border.destroy()?; border.destroy()?;
to_remove.push(id.clone()); to_remove.push(id.clone());
} }
@@ -386,6 +450,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
} }
} }
} }
}
}
previous_snapshot = monitors; previous_snapshot = monitors;
previous_pending_move_op = pending_move_op; previous_pending_move_op = pending_move_op;
+15
View File
@@ -27,6 +27,7 @@ use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy; use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ApplicationIdentifier; use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis; use komorebi_core::Axis;
use komorebi_core::BorderImplementation;
use komorebi_core::FocusFollowsMouseImplementation; use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::Layout; use komorebi_core::Layout;
use komorebi_core::MoveBehaviour; use komorebi_core::MoveBehaviour;
@@ -39,6 +40,7 @@ use komorebi_core::WindowContainerBehaviour;
use komorebi_core::WindowKind; use komorebi_core::WindowKind;
use crate::border_manager; use crate::border_manager;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE; use crate::border_manager::STYLE;
use crate::colour::Rgb; use crate::colour::Rgb;
use crate::current_virtual_desktop; use crate::current_virtual_desktop;
@@ -1241,6 +1243,19 @@ impl WindowManager {
SocketMessage::Border(enable) => { SocketMessage::Border(enable) => {
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst); border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
} }
SocketMessage::BorderImplementation(implementation) => {
IMPLEMENTATION.store(implementation);
match IMPLEMENTATION.load() {
BorderImplementation::Komorebi => {
self.remove_all_accents()?;
}
BorderImplementation::Windows => {
border_manager::destroy_all_borders()?;
}
}
border_manager::send_notification();
}
SocketMessage::BorderColour(kind, r, g, b) => match kind { SocketMessage::BorderColour(kind, r, g, b) => match kind {
WindowKind::Single => { WindowKind::Single => {
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst); border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
+17
View File
@@ -1,5 +1,6 @@
use crate::border_manager; use crate::border_manager;
use crate::border_manager::ZOrder; use crate::border_manager::ZOrder;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE; use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER; use crate::border_manager::Z_ORDER;
use crate::colour::Colour; use crate::colour::Colour;
@@ -32,6 +33,7 @@ use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS; use crate::REGEX_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS; use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES; use crate::WORKSPACE_RULES;
use komorebi_core::BorderImplementation;
use komorebi_core::StackbarLabel; use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode; use komorebi_core::StackbarMode;
@@ -279,6 +281,9 @@ pub struct StaticConfig {
/// Active window border z-order (default: System) /// Active window border z-order (default: System)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub border_z_order: Option<ZOrder>, pub border_z_order: Option<ZOrder>,
/// Display an active window border (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub border_implementation: Option<BorderImplementation>,
/// Add transparency to unfocused windows (default: false) /// Add transparency to unfocused windows (default: false)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub transparency: Option<bool>, pub transparency: Option<bool>,
@@ -483,6 +488,7 @@ impl From<&WindowManager> for StaticConfig {
), ),
border_style: Option::from(STYLE.load()), border_style: Option::from(STYLE.load()),
border_z_order: Option::from(Z_ORDER.load()), border_z_order: Option::from(Z_ORDER.load()),
border_implementation: Option::from(IMPLEMENTATION.load()),
default_workspace_padding: Option::from( default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst), DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
), ),
@@ -557,6 +563,17 @@ impl StaticConfig {
} }
STYLE.store(self.border_style.unwrap_or_default()); STYLE.store(self.border_style.unwrap_or_default());
IMPLEMENTATION.store(self.border_implementation.unwrap_or_default());
match IMPLEMENTATION.load() {
BorderImplementation::Komorebi => {
border_manager::destroy_all_borders()?;
}
BorderImplementation::Windows => {
// TODO: figure out how to call wm.remove_all_accents here
}
}
border_manager::send_notification();
transparency_manager::TRANSPARENCY_ENABLED transparency_manager::TRANSPARENCY_ENABLED
.store(self.transparency.unwrap_or(false), Ordering::SeqCst); .store(self.transparency.unwrap_or(false), Ordering::SeqCst);
+8
View File
@@ -275,6 +275,14 @@ impl Window {
self.update_ex_style(&ex_style) self.update_ex_style(&ex_style)
} }
pub fn set_accent(self, colour: u32) -> Result<()> {
WindowsApi::set_window_accent(self.hwnd, Some(colour))
}
pub fn remove_accent(self) -> Result<()> {
WindowsApi::set_window_accent(self.hwnd, None)
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn update_style(self, style: &WindowStyle) -> Result<()> { pub fn update_style(self, style: &WindowStyle) -> Result<()> {
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?) WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
+19
View File
@@ -947,6 +947,8 @@ impl WindowManager {
window.opaque()?; window.opaque()?;
} }
window.remove_accent()?;
window.restore(); window.restore();
} }
} }
@@ -956,6 +958,23 @@ impl WindowManager {
Ok(()) Ok(())
} }
#[tracing::instrument(skip(self))]
pub fn remove_all_accents(&mut self) -> Result<()> {
tracing::info!("removing all window accents");
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
for containers in workspace.containers_mut() {
for window in containers.windows_mut() {
window.remove_accent()?;
}
}
}
}
Ok(())
}
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
fn handle_unmanaged_window_behaviour(&self) -> Result<()> { fn handle_unmanaged_window_behaviour(&self) -> Result<()> {
if matches!( if matches!(
+15
View File
@@ -21,7 +21,9 @@ use windows::Win32::Foundation::POINT;
use windows::Win32::Foundation::WPARAM; use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute; use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute; use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
use windows::Win32::Graphics::Dwm::DWMWA_BORDER_COLOR;
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED; use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use windows::Win32::Graphics::Dwm::DWMWA_COLOR_NONE;
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS; use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE; use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE;
use windows::Win32::Graphics::Dwm::DWMWCP_ROUND; use windows::Win32::Graphics::Dwm::DWMWCP_ROUND;
@@ -955,6 +957,19 @@ impl WindowsApi {
.process() .process()
} }
pub fn set_window_accent(hwnd: isize, color: Option<u32>) -> Result<()> {
let col_ref = COLORREF(color.unwrap_or(DWMWA_COLOR_NONE));
unsafe {
DwmSetWindowAttribute(
HWND(hwnd),
DWMWA_BORDER_COLOR,
std::ptr::addr_of!(col_ref).cast(),
4,
)
}
.process()
}
pub fn create_border_window(name: PCWSTR, instance: HMODULE) -> Result<isize> { pub fn create_border_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
unsafe { unsafe {
let hwnd = CreateWindowExW( let hwnd = CreateWindowExW(
+25
View File
@@ -684,6 +684,19 @@ struct BorderOffset {
/// Desired offset of the window border /// Desired offset of the window border
offset: i32, offset: i32,
} }
#[derive(Parser)]
struct BorderStyle {
/// Desired border style
#[clap(value_enum)]
style: komorebi_core::BorderStyle,
}
#[derive(Parser)]
struct BorderImplementation {
/// Desired border implementation
#[clap(value_enum)]
style: komorebi_core::BorderImplementation,
}
#[derive(Parser)] #[derive(Parser)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
@@ -1176,6 +1189,12 @@ enum SubCommand {
#[clap(arg_required_else_help = true)] #[clap(arg_required_else_help = true)]
#[clap(alias = "active-window-border-offset")] #[clap(alias = "active-window-border-offset")]
BorderOffset(BorderOffset), BorderOffset(BorderOffset),
/// Set the border style
#[clap(arg_required_else_help = true)]
BorderStyle(BorderStyle),
/// Set the border implementation
#[clap(arg_required_else_help = true)]
BorderImplementation(BorderImplementation),
/// Enable or disable transparency for unfocused windows /// Enable or disable transparency for unfocused windows
#[clap(arg_required_else_help = true)] #[clap(arg_required_else_help = true)]
Transparency(Transparency), Transparency(Transparency),
@@ -2269,6 +2288,12 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
SubCommand::BorderOffset(arg) => { SubCommand::BorderOffset(arg) => {
send_message(&SocketMessage::BorderOffset(arg.offset).as_bytes()?)?; send_message(&SocketMessage::BorderOffset(arg.offset).as_bytes()?)?;
} }
SubCommand::BorderStyle(arg) => {
send_message(&SocketMessage::BorderStyle(arg.style).as_bytes()?)?;
}
SubCommand::BorderImplementation(arg) => {
send_message(&SocketMessage::BorderImplementation(arg.style).as_bytes()?)?;
}
SubCommand::Transparency(arg) => { SubCommand::Transparency(arg) => {
send_message(&SocketMessage::Transparency(arg.boolean_state.into()).as_bytes()?)?; send_message(&SocketMessage::Transparency(arg.boolean_state.into()).as_bytes()?)?;
} }