Skip to content

Commit 47593a0

Browse files
authored
feat: add window transparency effect (#864)
1 parent 5199f62 commit 47593a0

File tree

7 files changed

+245
-17
lines changed

7 files changed

+245
-17
lines changed

packages/wm/src/app_command.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::{
1212
cycle_focus, disable_binding_mode, enable_binding_mode,
1313
reload_config, shell_exec, toggle_pause,
1414
},
15-
Direction, LengthValue, RectDelta, TilingDirection,
15+
Direction, LengthValue, OpacityValue, RectDelta, TilingDirection,
1616
},
1717
containers::{
1818
commands::{
@@ -226,6 +226,10 @@ pub enum InvokeCommand {
226226
#[clap(required = true, value_enum)]
227227
visibility: TitleBarVisibility,
228228
},
229+
SetOpacity {
230+
#[clap(required = true, allow_hyphen_values = true)]
231+
opacity: OpacityValue,
232+
},
229233
ShellExec {
230234
#[clap(long, action)]
231235
hide_window: bool,
@@ -602,6 +606,15 @@ impl InvokeCommand {
602606
_ => Ok(()),
603607
}
604608
}
609+
InvokeCommand::SetOpacity { opacity } => {
610+
match subject_container.as_window_container() {
611+
Ok(window) => {
612+
_ = window.native().set_opacity(opacity.clone());
613+
Ok(())
614+
}
615+
_ => Ok(()),
616+
}
617+
}
605618
InvokeCommand::ShellExec {
606619
hide_window,
607620
command,

packages/wm/src/common/commands/platform_sync.rs

+26-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use tokio::task;
55
use tracing::warn;
66

77
use crate::{
8-
common::{platform::Platform, DisplayState},
8+
common::{platform::Platform, DisplayState, OpacityValue},
99
containers::{
1010
traits::{CommonGetters, PositionGetters},
1111
Container, WindowContainer,
@@ -211,8 +211,6 @@ fn apply_window_effects(
211211
is_focused: bool,
212212
config: &UserConfig,
213213
) {
214-
// TODO: Be able to add transparency to windows.
215-
216214
let window_effects = &config.value.window_effects;
217215

218216
let effect_config = match is_focused {
@@ -238,6 +236,12 @@ fn apply_window_effects(
238236
{
239237
apply_corner_effect(&window, effect_config);
240238
}
239+
240+
if window_effects.focused_window.transparency.enabled
241+
|| window_effects.other_windows.transparency.enabled
242+
{
243+
apply_transparency_effect(&window, effect_config);
244+
}
241245
}
242246

243247
fn apply_border_effect(
@@ -282,3 +286,22 @@ fn apply_corner_effect(
282286

283287
_ = window.native().set_corner_style(corner_style);
284288
}
289+
290+
fn apply_transparency_effect(
291+
window: &WindowContainer,
292+
effect_config: &WindowEffectConfig,
293+
) {
294+
_ = window
295+
.native()
296+
.set_opacity(if effect_config.transparency.enabled {
297+
effect_config.transparency.opacity.clone()
298+
} else {
299+
// This code is only reached if the transparency effect is only
300+
// enabled in one of the two window effect configurations. In
301+
// this case, reset the opacity to default.
302+
OpacityValue {
303+
amount: 255,
304+
is_delta: false,
305+
}
306+
})
307+
}

packages/wm/src/common/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod display_state;
55
pub mod events;
66
mod length_value;
77
mod memo;
8+
mod opacity_value;
89
pub mod platform;
910
mod point;
1011
mod rect;
@@ -18,6 +19,7 @@ pub use direction::*;
1819
pub use display_state::*;
1920
pub use length_value::*;
2021
pub use memo::*;
22+
pub use opacity_value::*;
2123
pub use point::*;
2224
pub use rect::*;
2325
pub use rect_delta::*;
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use std::str::FromStr;
2+
3+
use anyhow::Context;
4+
use regex::Regex;
5+
use serde::{Deserialize, Deserializer, Serialize};
6+
7+
#[derive(Debug, Clone, PartialEq, Serialize)]
8+
pub struct OpacityValue {
9+
pub amount: i16,
10+
pub is_delta: bool,
11+
}
12+
13+
impl Default for OpacityValue {
14+
fn default() -> Self {
15+
Self {
16+
amount: 255,
17+
is_delta: false,
18+
}
19+
}
20+
}
21+
22+
impl FromStr for OpacityValue {
23+
type Err = anyhow::Error;
24+
25+
/// Parses a string for an opacity value. The string can be a number
26+
/// or a percentage. If the string starts with a sign, the value is
27+
/// interpreted as a delta.
28+
///
29+
/// Example:
30+
/// ```
31+
/// # use wm::common::{OpacityValue};
32+
/// # use std::str::FromStr;
33+
/// let check = OpacityValue {
34+
/// amount: 191,
35+
/// is_delta: false,
36+
/// };
37+
/// let parsed = OpacityValue::from_str("75%");
38+
/// assert_eq!(parsed.unwrap(), check);
39+
/// ```
40+
fn from_str(unparsed: &str) -> anyhow::Result<Self> {
41+
let units_regex = Regex::new(r"([+-]?)(\d+)(%?)")?;
42+
43+
let err_msg = format!(
44+
"Not a valid opacity value '{}'. Must be of format '255', '100%', '+10%' or '-128'.",
45+
unparsed
46+
);
47+
48+
let captures = units_regex
49+
.captures(unparsed)
50+
.context(err_msg.to_string())?;
51+
52+
let sign_str = captures.get(1).map_or("", |m| m.as_str());
53+
54+
// Interpret value as a delta if it explicitly starts with a sign.
55+
let is_delta = !sign_str.is_empty();
56+
57+
let unit_str = captures.get(3).map_or("", |m| m.as_str());
58+
59+
let amount = captures
60+
.get(2)
61+
.and_then(|amount_str| f32::from_str(amount_str.into()).ok())
62+
// Convert percentages to 0-255 range.
63+
.map(|amount| match unit_str {
64+
"%" => (amount / 100.0 * 255.0).round() as i16,
65+
_ => amount.round() as i16,
66+
})
67+
// Negate the value if it's a negative delta.
68+
// Since an explicit sign tells us it's a delta,
69+
// a negative Alpha value is impossible.
70+
.map(|amount| if sign_str == "-" { -amount } else { amount })
71+
.context(err_msg.to_string())?;
72+
73+
Ok(OpacityValue { amount, is_delta })
74+
}
75+
}
76+
77+
/// Deserialize an `OpacityValue` from either a string or a struct.
78+
impl<'de> Deserialize<'de> for OpacityValue {
79+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80+
where
81+
D: Deserializer<'de>,
82+
{
83+
#[derive(Deserialize)]
84+
#[serde(untagged, rename_all = "camelCase")]
85+
enum OpacityValueDe {
86+
Struct { amount: f32, is_delta: bool },
87+
String(String),
88+
}
89+
90+
match OpacityValueDe::deserialize(deserializer)? {
91+
OpacityValueDe::Struct { amount, is_delta } => Ok(Self {
92+
amount: amount as i16,
93+
is_delta,
94+
}),
95+
OpacityValueDe::String(str) => {
96+
Self::from_str(&str).map_err(serde::de::Error::custom)
97+
}
98+
}
99+
}
100+
}

packages/wm/src/common/platform/native_window.rs

+76-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::Context;
1+
use anyhow::{bail, Context};
22
use tracing::warn;
33
use windows::{
44
core::PWSTR,
@@ -23,17 +23,19 @@ use windows::{
2323
},
2424
Shell::{ITaskbarList, TaskbarList},
2525
WindowsAndMessaging::{
26-
EnumWindows, GetClassNameW, GetWindow, GetWindowLongPtrW,
27-
GetWindowRect, GetWindowTextW, GetWindowThreadProcessId, IsIconic,
28-
IsWindowVisible, IsZoomed, SendNotifyMessageW,
29-
SetForegroundWindow, SetWindowLongPtrW, SetWindowPlacement,
26+
EnumWindows, GetClassNameW, GetLayeredWindowAttributes, GetWindow,
27+
GetWindowLongPtrW, GetWindowRect, GetWindowTextW,
28+
GetWindowThreadProcessId, IsIconic, IsWindowVisible, IsZoomed,
29+
SendNotifyMessageW, SetForegroundWindow,
30+
SetLayeredWindowAttributes, SetWindowLongPtrW, SetWindowPlacement,
3031
SetWindowPos, ShowWindowAsync, GWL_EXSTYLE, GWL_STYLE, GW_OWNER,
31-
HWND_NOTOPMOST, HWND_TOPMOST, SWP_ASYNCWINDOWPOS,
32-
SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOMOVE,
33-
SWP_NOOWNERZORDER, SWP_NOSENDCHANGING, SWP_NOSIZE, SWP_NOZORDER,
34-
SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOWNA,
35-
WINDOWPLACEMENT, WINDOW_EX_STYLE, WINDOW_STYLE, WM_CLOSE,
36-
WPF_ASYNCWINDOWPLACEMENT, WS_CAPTION, WS_CHILD, WS_DLGFRAME,
32+
HWND_NOTOPMOST, HWND_TOPMOST, LAYERED_WINDOW_ATTRIBUTES_FLAGS,
33+
LWA_ALPHA, LWA_COLORKEY, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED,
34+
SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOMOVE, SWP_NOOWNERZORDER,
35+
SWP_NOSENDCHANGING, SWP_NOSIZE, SWP_NOZORDER, SW_HIDE,
36+
SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOWNA, WINDOWPLACEMENT,
37+
WINDOW_EX_STYLE, WINDOW_STYLE, WM_CLOSE, WPF_ASYNCWINDOWPLACEMENT,
38+
WS_CAPTION, WS_CHILD, WS_DLGFRAME, WS_EX_LAYERED,
3739
WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_MAXIMIZEBOX, WS_THICKFRAME,
3840
},
3941
},
@@ -42,7 +44,7 @@ use windows::{
4244

4345
use super::{iapplication_view_collection, iservice_provider, COM_INIT};
4446
use crate::{
45-
common::{Color, LengthValue, Memo, Rect, RectDelta},
47+
common::{Color, LengthValue, Memo, OpacityValue, Rect, RectDelta},
4648
user_config::{CornerStyle, HideMethod},
4749
windows::WindowState,
4850
};
@@ -394,6 +396,68 @@ impl NativeWindow {
394396
Ok(())
395397
}
396398

399+
pub fn set_opacity(
400+
&self,
401+
opacity_value: OpacityValue,
402+
) -> anyhow::Result<()> {
403+
// Make the window layered if it isn't already.
404+
let ex_style =
405+
unsafe { GetWindowLongPtrW(HWND(self.handle), GWL_EXSTYLE) };
406+
407+
if ex_style & WS_EX_LAYERED.0 as isize == 0 {
408+
unsafe {
409+
SetWindowLongPtrW(
410+
HWND(self.handle),
411+
GWL_EXSTYLE,
412+
ex_style | WS_EX_LAYERED.0 as isize,
413+
);
414+
}
415+
}
416+
417+
// Get the window's opacity information.
418+
let mut previous_opacity = u8::MAX; // Use maximum opacity as a default.
419+
let mut flag = LAYERED_WINDOW_ATTRIBUTES_FLAGS::default();
420+
unsafe {
421+
GetLayeredWindowAttributes(
422+
HWND(self.handle),
423+
None,
424+
Some(&mut previous_opacity),
425+
Some(&mut flag),
426+
)?;
427+
}
428+
429+
// Fail if window uses color key.
430+
if flag.contains(LWA_COLORKEY) {
431+
bail!(
432+
"Window uses color key for its transparency. The transparency window effect cannot be applied."
433+
);
434+
}
435+
436+
// Calculate the new opacity value.
437+
let new_opacity = if opacity_value.is_delta {
438+
previous_opacity as i16 + opacity_value.amount
439+
} else {
440+
opacity_value.amount
441+
};
442+
443+
// Clamp new_opacity to a u8.
444+
let new_opacity =
445+
new_opacity.clamp(u8::MIN as i16, u8::MAX as i16) as u8;
446+
447+
// Set the new opacity if needed.
448+
if new_opacity != previous_opacity {
449+
unsafe {
450+
SetLayeredWindowAttributes(
451+
HWND(self.handle),
452+
None,
453+
new_opacity,
454+
LWA_ALPHA,
455+
)?;
456+
}
457+
}
458+
Ok(())
459+
}
460+
397461
/// Gets the window's position, including the window's frame. Excludes
398462
/// the window's shadow borders.
399463
///

packages/wm/src/user_config.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
55

66
use crate::{
77
app_command::InvokeCommand,
8-
common::{Color, LengthValue, RectDelta},
8+
common::{Color, LengthValue, OpacityValue, RectDelta},
99
containers::{traits::CommonGetters, WindowContainer},
1010
monitors::Monitor,
1111
windows::traits::WindowGetters,
@@ -536,6 +536,10 @@ pub struct WindowEffectConfig {
536536
/// Config for optionally changing the corner style.
537537
#[serde(default)]
538538
pub corner_style: CornerEffectConfig,
539+
540+
/// Config for optionally applying transparency.
541+
#[serde(default)]
542+
pub transparency: TransparencyEffectConfig,
539543
}
540544

541545
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -558,6 +562,18 @@ pub struct HideTitleBarEffectConfig {
558562
pub enabled: bool,
559563
}
560564

565+
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
566+
#[serde(rename_all(serialize = "camelCase"))]
567+
pub struct TransparencyEffectConfig {
568+
/// Whether to enable the effect.
569+
#[serde(default = "default_bool::<false>")]
570+
pub enabled: bool,
571+
572+
/// The opacity to apply.
573+
#[serde(default)]
574+
pub opacity: OpacityValue,
575+
}
576+
561577
#[derive(Clone, Debug, Deserialize, Serialize)]
562578
#[serde(rename_all(serialize = "camelCase"))]
563579
pub struct CornerEffectConfig {

resources/assets/sample-config.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ window_effects:
7474
# Allowed values: 'square', 'rounded', 'small_rounded'.
7575
style: 'square'
7676

77+
# Change the transparency of the window.
78+
transparency:
79+
enabled: false
80+
# Can be something like '240' or '95%' for slightly transparent windows
81+
# '0' or '0%' is fully transparent (and, by consequence, unfocusable)
82+
opacity: '95%'
83+
7784
# Visual effects to apply to non-focused windows.
7885
other_windows:
7986
border:
@@ -84,6 +91,9 @@ window_effects:
8491
corner_style:
8592
enabled: false
8693
style: 'square'
94+
transparency:
95+
enabled: false
96+
opacity: '0%'
8797

8898
window_behavior:
8999
# New windows are created in this state whenever possible.

0 commit comments

Comments
 (0)