Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add window transparency effect #864

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
15 changes: 14 additions & 1 deletion packages/wm/src/app_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
cycle_focus, disable_binding_mode, enable_binding_mode,
reload_config, shell_exec, toggle_pause,
},
Direction, LengthValue, RectDelta, TilingDirection,
Direction, LengthValue, OpacityValue, RectDelta, TilingDirection,
},
containers::{
commands::{
Expand Down Expand Up @@ -226,6 +226,10 @@ pub enum InvokeCommand {
#[clap(required = true, value_enum)]
visibility: TitleBarVisibility,
},
SetOpacity {
#[clap(required = true, allow_hyphen_values = true)]
opacity: OpacityValue,
},
ShellExec {
#[clap(long, action)]
hide_window: bool,
Expand Down Expand Up @@ -602,6 +606,15 @@ impl InvokeCommand {
_ => Ok(()),
}
}
InvokeCommand::SetOpacity { opacity } => {
match subject_container.as_window_container() {
Ok(window) => {
_ = window.native().set_opacity(opacity.clone());
Ok(())
}
_ => Ok(()),
}
}
InvokeCommand::ShellExec {
hide_window,
command,
Expand Down
29 changes: 26 additions & 3 deletions packages/wm/src/common/commands/platform_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use tokio::task;
use tracing::warn;

use crate::{
common::{platform::Platform, DisplayState},
common::{platform::Platform, DisplayState, OpacityValue},
containers::{
traits::{CommonGetters, PositionGetters},
Container, WindowContainer,
Expand Down Expand Up @@ -211,8 +211,6 @@ fn apply_window_effects(
is_focused: bool,
config: &UserConfig,
) {
// TODO: Be able to add transparency to windows.

let window_effects = &config.value.window_effects;

let effect_config = match is_focused {
Expand All @@ -238,6 +236,12 @@ fn apply_window_effects(
{
apply_corner_effect(&window, effect_config);
}

if window_effects.focused_window.transparency.enabled
|| window_effects.other_windows.transparency.enabled
{
apply_transparency_effect(&window, effect_config);
}
}

fn apply_border_effect(
Expand Down Expand Up @@ -282,3 +286,22 @@ fn apply_corner_effect(

_ = window.native().set_corner_style(corner_style);
}

fn apply_transparency_effect(
window: &WindowContainer,
effect_config: &WindowEffectConfig,
) {
_ = window
.native()
.set_opacity(if effect_config.transparency.enabled {
effect_config.transparency.opacity.clone()
} else {
// This code is only reached if the transparency effect is only
// enabled in one of the two window effect configurations. In
// this case, reset the opacity to default.
OpacityValue {
amount: 255,
is_delta: false,
}
})
}
2 changes: 2 additions & 0 deletions packages/wm/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod display_state;
pub mod events;
mod length_value;
mod memo;
mod opacity_value;
pub mod platform;
mod point;
mod rect;
Expand All @@ -18,6 +19,7 @@ pub use direction::*;
pub use display_state::*;
pub use length_value::*;
pub use memo::*;
pub use opacity_value::*;
pub use point::*;
pub use rect::*;
pub use rect_delta::*;
Expand Down
100 changes: 100 additions & 0 deletions packages/wm/src/common/opacity_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::str::FromStr;

use anyhow::Context;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct OpacityValue {
pub amount: i16,
pub is_delta: bool,
}

impl Default for OpacityValue {
fn default() -> Self {
Self {
amount: 255,
is_delta: false,
}
}
}

impl FromStr for OpacityValue {
type Err = anyhow::Error;

/// Parses a string for an opacity value. The string can be a number
/// or a percentage. If the string starts with a sign, the value is
/// interpreted as a delta.
///
/// Example:
/// ```
/// # use wm::common::{OpacityValue};
/// # use std::str::FromStr;
/// let check = OpacityValue {
/// amount: 191,
/// is_delta: false,
/// };
/// let parsed = OpacityValue::from_str("75%");
/// assert_eq!(parsed.unwrap(), check);
/// ```
fn from_str(unparsed: &str) -> anyhow::Result<Self> {
let units_regex = Regex::new(r"([+-]?)(\d+)(%?)")?;

let err_msg = format!(
"Not a valid opacity value '{}'. Must be of format '255', '100%', '+10%' or '-128'.",
unparsed
);

let captures = units_regex
.captures(unparsed)
.context(err_msg.to_string())?;

let sign_str = captures.get(1).map_or("", |m| m.as_str());

// Interpret value as a delta if it explicitly starts with a sign.
let is_delta = !sign_str.is_empty();

let unit_str = captures.get(3).map_or("", |m| m.as_str());

let amount = captures
.get(2)
.and_then(|amount_str| f32::from_str(amount_str.into()).ok())
// Convert percentages to 0-255 range.
.map(|amount| match unit_str {
"%" => (amount / 100.0 * 255.0).round() as i16,
_ => amount.round() as i16,
})
// Negate the value if it's a negative delta.
// Since an explicit sign tells us it's a delta,
// a negative Alpha value is impossible.
.map(|amount| if sign_str == "-" { -amount } else { amount })
.context(err_msg.to_string())?;

Ok(OpacityValue { amount, is_delta })
}
}

/// Deserialize an `OpacityValue` from either a string or a struct.
impl<'de> Deserialize<'de> for OpacityValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged, rename_all = "camelCase")]
enum OpacityValueDe {
Struct { amount: f32, is_delta: bool },
String(String),
}

match OpacityValueDe::deserialize(deserializer)? {
OpacityValueDe::Struct { amount, is_delta } => Ok(Self {
amount: amount as i16,
is_delta,
}),
OpacityValueDe::String(str) => {
Self::from_str(&str).map_err(serde::de::Error::custom)
}
}
}
}
88 changes: 76 additions & 12 deletions packages/wm/src/common/platform/native_window.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Context;
use anyhow::{bail, Context};
use tracing::warn;
use windows::{
core::PWSTR,
Expand All @@ -23,17 +23,19 @@ use windows::{
},
Shell::{ITaskbarList, TaskbarList},
WindowsAndMessaging::{
EnumWindows, GetClassNameW, GetWindow, GetWindowLongPtrW,
GetWindowRect, GetWindowTextW, GetWindowThreadProcessId, IsIconic,
IsWindowVisible, IsZoomed, SendNotifyMessageW,
SetForegroundWindow, SetWindowLongPtrW, SetWindowPlacement,
EnumWindows, GetClassNameW, GetLayeredWindowAttributes, GetWindow,
GetWindowLongPtrW, GetWindowRect, GetWindowTextW,
GetWindowThreadProcessId, IsIconic, IsWindowVisible, IsZoomed,
SendNotifyMessageW, SetForegroundWindow,
SetLayeredWindowAttributes, SetWindowLongPtrW, SetWindowPlacement,
SetWindowPos, ShowWindowAsync, GWL_EXSTYLE, GWL_STYLE, GW_OWNER,
HWND_NOTOPMOST, HWND_TOPMOST, SWP_ASYNCWINDOWPOS,
SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOMOVE,
SWP_NOOWNERZORDER, SWP_NOSENDCHANGING, SWP_NOSIZE, SWP_NOZORDER,
SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOWNA,
WINDOWPLACEMENT, WINDOW_EX_STYLE, WINDOW_STYLE, WM_CLOSE,
WPF_ASYNCWINDOWPLACEMENT, WS_CAPTION, WS_CHILD, WS_DLGFRAME,
HWND_NOTOPMOST, HWND_TOPMOST, LAYERED_WINDOW_ATTRIBUTES_FLAGS,
LWA_ALPHA, LWA_COLORKEY, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED,
SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOMOVE, SWP_NOOWNERZORDER,
SWP_NOSENDCHANGING, SWP_NOSIZE, SWP_NOZORDER, SW_HIDE,
SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOWNA, WINDOWPLACEMENT,
WINDOW_EX_STYLE, WINDOW_STYLE, WM_CLOSE, WPF_ASYNCWINDOWPLACEMENT,
WS_CAPTION, WS_CHILD, WS_DLGFRAME, WS_EX_LAYERED,
WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_MAXIMIZEBOX, WS_THICKFRAME,
},
},
Expand All @@ -42,7 +44,7 @@ use windows::{

use super::{iapplication_view_collection, iservice_provider, COM_INIT};
use crate::{
common::{Color, LengthValue, Memo, Rect, RectDelta},
common::{Color, LengthValue, Memo, OpacityValue, Rect, RectDelta},
user_config::{CornerStyle, HideMethod},
windows::WindowState,
};
Expand Down Expand Up @@ -394,6 +396,68 @@ impl NativeWindow {
Ok(())
}

pub fn set_opacity(
&self,
opacity_value: OpacityValue,
) -> anyhow::Result<()> {
// Make the window layered if it isn't already.
jackssrt marked this conversation as resolved.
Show resolved Hide resolved
let ex_style =
unsafe { GetWindowLongPtrW(HWND(self.handle), GWL_EXSTYLE) };

if ex_style & WS_EX_LAYERED.0 as isize == 0 {
unsafe {
SetWindowLongPtrW(
HWND(self.handle),
GWL_EXSTYLE,
ex_style | WS_EX_LAYERED.0 as isize,
);
}
}

// Get the window's opacity information.
let mut previous_opacity = u8::MAX; // Use maximum opacity as a default.
let mut flag = LAYERED_WINDOW_ATTRIBUTES_FLAGS::default();
unsafe {
GetLayeredWindowAttributes(
HWND(self.handle),
None,
Some(&mut previous_opacity),
Some(&mut flag),
)?;
}

// Fail if window uses color key.
if flag.contains(LWA_COLORKEY) {
bail!(
"Window uses color key for its transparency. The transparency window effect cannot be applied."
);
}

// Calculate the new opacity value.
let new_opacity = if opacity_value.is_delta {
previous_opacity as i16 + opacity_value.amount
} else {
opacity_value.amount
};

// Clamp new_opacity to a u8.
let new_opacity =
new_opacity.clamp(u8::MIN as i16, u8::MAX as i16) as u8;

// Set the new opacity if needed.
if new_opacity != previous_opacity {
unsafe {
SetLayeredWindowAttributes(
HWND(self.handle),
None,
new_opacity,
LWA_ALPHA,
)?;
}
}
Ok(())
}

/// Gets the window's position, including the window's frame. Excludes
/// the window's shadow borders.
///
Expand Down
18 changes: 17 additions & 1 deletion packages/wm/src/user_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};

use crate::{
app_command::InvokeCommand,
common::{Color, LengthValue, RectDelta},
common::{Color, LengthValue, OpacityValue, RectDelta},
containers::{traits::CommonGetters, WindowContainer},
monitors::Monitor,
windows::traits::WindowGetters,
Expand Down Expand Up @@ -536,6 +536,10 @@ pub struct WindowEffectConfig {
/// Config for optionally changing the corner style.
#[serde(default)]
pub corner_style: CornerEffectConfig,

/// Config for optionally applying transparency.
#[serde(default)]
pub transparency: TransparencyEffectConfig,
}

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

#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all(serialize = "camelCase"))]
pub struct TransparencyEffectConfig {
/// Whether to enable the effect.
#[serde(default = "default_bool::<false>")]
pub enabled: bool,

/// The opacity to apply.
#[serde(default)]
pub opacity: OpacityValue,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all(serialize = "camelCase"))]
pub struct CornerEffectConfig {
Expand Down
10 changes: 10 additions & 0 deletions resources/assets/sample-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ window_effects:
# Allowed values: 'square', 'rounded', 'small_rounded'.
style: 'square'

# Change the transparency of the window.
transparency:
enabled: false
# Can be something like '240' or '95%' for slightly transparent windows
# '0' or '0%' is fully transparent (and, by consequence, unfocusable)
opacity: '95%'

# Visual effects to apply to non-focused windows.
other_windows:
border:
Expand All @@ -84,6 +91,9 @@ window_effects:
corner_style:
enabled: false
style: 'square'
transparency:
enabled: false
opacity: '0%'

window_behavior:
# New windows are created in this state whenever possible.
Expand Down