Skip to content

Commit

Permalink
feat: separate commands for changing transparency by value and delta (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-berger authored Jan 23, 2025
1 parent f564ed5 commit 110ed89
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 114 deletions.
19 changes: 14 additions & 5 deletions packages/wm-common/src/app_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use serde::{Deserialize, Deserializer, Serialize};
use tracing::Level;
use uuid::Uuid;

use crate::{Direction, LengthValue, OpacityValue, TilingDirection};
use crate::{
Delta, Direction, LengthValue, OpacityValue, TilingDirection,
};

const VERSION: &str = env!("VERSION_NUMBER");

Expand Down Expand Up @@ -197,10 +199,7 @@ pub enum InvokeCommand {
#[clap(required = true, value_enum)]
visibility: TitleBarVisibility,
},
SetOpacity {
#[clap(required = true, allow_hyphen_values = true)]
opacity: OpacityValue,
},
SetTransparency(SetTransparencyCommand),
ShellExec {
#[clap(long, action)]
hide_window: bool,
Expand Down Expand Up @@ -362,6 +361,16 @@ pub struct InvokeResizeCommand {
pub height: Option<LengthValue>,
}

#[derive(Args, Clone, Debug, PartialEq, Serialize)]
#[group(required = true, multiple = true)]
pub struct SetTransparencyCommand {
#[clap(long)]
pub opacity: Option<OpacityValue>,

#[clap(long, allow_hyphen_values = true)]
pub opacity_delta: Option<Delta<OpacityValue>>,
}

#[derive(Args, Clone, Debug, PartialEq, Serialize)]
#[group(required = true, multiple = true)]
pub struct InvokePositionCommand {
Expand Down
35 changes: 35 additions & 0 deletions packages/wm-common/src/delta.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use std::str::FromStr;

use anyhow::bail;
use serde::Serialize;

/// A wrapper that indicates a value should be interpreted as a delta
/// (relative change).
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct Delta<T> {
pub inner: T,
pub is_negative: bool,
}

impl<T: FromStr<Err = anyhow::Error>> FromStr for Delta<T> {
type Err = anyhow::Error;

fn from_str(unparsed: &str) -> anyhow::Result<Self> {
let unparsed = unparsed.trim();

let (raw, is_negative) = match unparsed.chars().next() {
Some('+') => (&unparsed[1..], false),
Some('-') => (&unparsed[1..], true),
// No sign means positive.
_ => (unparsed, false),
};

if raw.is_empty() {
bail!("Empty value.");
}

let inner = T::from_str(raw)?;

Ok(Self { inner, is_negative })
}
}
2 changes: 2 additions & 0 deletions packages/wm-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
mod active_drag;
mod app_command;
mod color;
mod delta;
mod direction;
mod display_state;
mod dtos;
Expand All @@ -22,6 +23,7 @@ mod wm_event;
pub use active_drag::*;
pub use app_command::*;
pub use color::*;
pub use delta::*;
pub use direction::*;
pub use display_state::*;
pub use dtos::*;
Expand Down
88 changes: 34 additions & 54 deletions packages/wm-common/src/opacity_value.rs
Original file line number Diff line number Diff line change
@@ -1,80 +1,65 @@
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,
pub struct OpacityValue(f32);

impl OpacityValue {
#[must_use]
pub fn to_alpha(&self) -> u8 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let alpha = (self.0 * 255.0).round() as u8;
alpha
}

#[must_use]
pub fn from_alpha(alpha: u8) -> Self {
Self(f32::from(alpha) / 255.0)
}
}

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

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.
/// Parses a string for an opacity value. The string must be a percentage
/// or a decimal number.
///
/// Example:
/// ```
/// # use wm::common::{OpacityValue};
/// # use std::str::FromStr;
/// let check = OpacityValue {
/// amount: 191,
/// is_delta: false,
/// };
/// let check = OpacityValue(0.75);
/// 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 '{unparsed}'. Must be of format '255', '100%', '+10%' or '-128'."
);
let unparsed = unparsed.trim();

let captures = units_regex
.captures(unparsed)
.context(err_msg.to_string())?;
if unparsed.ends_with('%') {
let percentage = unparsed
.trim_end_matches('%')
.parse::<f32>()
.context("Invalid percentage format.")?;

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());

#[allow(clippy::cast_possible_truncation)]
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 })
Ok(Self(percentage / 100.0))
} else {
unparsed
.parse::<f32>()
.map(Self)
.context("Invalid decimal format.")
}
}
}

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

match OpacityValueDe::deserialize(deserializer)? {
OpacityValueDe::Struct { amount, is_delta } => Ok(Self {
#[allow(clippy::cast_possible_truncation)]
amount: amount as i16,
is_delta,
}),

OpacityValueDe::Number(num) => Ok(Self(num)),
OpacityValueDe::String(str) => {
Self::from_str(&str).map_err(serde::de::Error::custom)
}
Expand Down
77 changes: 39 additions & 38 deletions packages/wm-platform/src/native_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ use windows::{
},
};
use wm_common::{
Color, CornerStyle, HideMethod, LengthValue, Memo, OpacityValue, Rect,
RectDelta, WindowState,
Color, CornerStyle, Delta, HideMethod, LengthValue, Memo, OpacityValue,
Rect, RectDelta, WindowState,
};

use super::{iapplication_view_collection, iservice_provider, COM_INIT};
Expand Down Expand Up @@ -419,67 +419,67 @@ impl NativeWindow {
Ok(())
}

pub fn set_opacity(
&self,
opacity_value: &OpacityValue,
) -> anyhow::Result<()> {
// Make the window layered if it isn't already.
let ex_style =
fn add_window_style_ex(&self, style: WINDOW_EX_STYLE) {
let current_style =
unsafe { GetWindowLongPtrW(HWND(self.handle), GWL_EXSTYLE) };

#[allow(clippy::cast_possible_wrap)]
if ex_style & WS_EX_LAYERED.0 as isize == 0 {
if current_style & style.0 as isize == 0 {
let new_style = current_style | style.0 as isize;

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

// Get the window's opacity information.
let mut previous_opacity = u8::MAX; // Use maximum opacity as a default.
pub fn adjust_transparency(
&self,
opacity_delta: &Delta<OpacityValue>,
) -> anyhow::Result<()> {
let mut alpha = u8::MAX;
let mut flag = LAYERED_WINDOW_ATTRIBUTES_FLAGS::default();

unsafe {
GetLayeredWindowAttributes(
HWND(self.handle),
None,
Some(&mut previous_opacity),
Some(&mut alpha),
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."
"Window uses color key for its transparency and cannot be adjusted."
);
}

// Calculate the new opacity value.
let new_opacity = if opacity_value.is_delta {
i16::from(previous_opacity) + opacity_value.amount
let target_alpha = if opacity_delta.is_negative {
alpha.saturating_sub(opacity_delta.inner.to_alpha())
} else {
opacity_value.amount
alpha.saturating_add(opacity_delta.inner.to_alpha())
};

// Clamp new_opacity to a u8.
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let new_opacity =
new_opacity.clamp(i16::from(u8::MIN), i16::from(u8::MAX)) as u8;
self.set_transparency(&OpacityValue::from_alpha(target_alpha))
}

// Set the new opacity if needed.
if new_opacity != previous_opacity {
unsafe {
SetLayeredWindowAttributes(
HWND(self.handle),
None,
new_opacity,
LWA_ALPHA,
)?;
}
pub fn set_transparency(
&self,
opacity_value: &OpacityValue,
) -> anyhow::Result<()> {
// Make the window layered if it isn't already.
self.add_window_style_ex(WS_EX_LAYERED);

unsafe {
SetLayeredWindowAttributes(
HWND(self.handle),
None,
opacity_value.to_alpha(),
LWA_ALPHA,
)?;
}

Ok(())
}

Expand Down Expand Up @@ -867,6 +867,7 @@ impl NativeWindow {

_ = self.set_taskbar_visibility(true);
_ = self.set_border_color(None);
_ = self.set_transparency(&OpacityValue::from_alpha(u8::MAX));
}
}

Expand Down
21 changes: 8 additions & 13 deletions packages/wm/src/commands/general/platform_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,17 +421,12 @@ fn apply_transparency_effect(
window: &WindowContainer,
effect_config: &WindowEffectConfig,
) {
_ = window
.native()
.set_opacity(if effect_config.transparency.enabled {
&effect_config.transparency.opacity
} 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,
}
});
let transparency = if effect_config.transparency.enabled {
&effect_config.transparency.opacity
} else {
// Reset the transparency to default.
&OpacityValue::from_alpha(u8::MAX)
};

_ = window.native().set_transparency(transparency);
}
11 changes: 9 additions & 2 deletions packages/wm/src/wm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,17 @@ impl WindowManager {
_ => Ok(()),
}
}
InvokeCommand::SetOpacity { opacity } => {
InvokeCommand::SetTransparency(args) => {
match subject_container.as_window_container() {
Ok(window) => {
_ = window.native().set_opacity(opacity);
if let Some(opacity) = &args.opacity {
_ = window.native().set_transparency(opacity);
}

if let Some(opacity_delta) = &args.opacity_delta {
_ = window.native().adjust_transparency(opacity_delta);
}

Ok(())
}
_ => Ok(()),
Expand Down
Loading

0 comments on commit 110ed89

Please sign in to comment.