Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/winisland-core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ pub struct AppConfig {
pub lyrics_mode: String,
pub lyrics_local_dir: Option<String>,
pub custom_font_path: Option<String>,
pub font_family: Option<String>,
#[educe(Default = "regular")]
#[setting(choice(
"light" => "font_weight_light",
"regular" => "font_weight_regular",
"bold" => "font_weight_bold",
))]
pub font_weight: String,
pub auto_start: bool,
#[setting(toggle)]
pub auto_hide: bool,
Expand Down
9 changes: 9 additions & 0 deletions crates/winisland-core/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ impl ActivityManager {
.max_by_key(|item| (item.priority, item.updated_at, item.id))
}

/// Returns the activity `current` would pick if `id` were absent.
pub fn current_excluding(&self, id: u64) -> Option<&Activity> {
let now = Instant::now();
self.activities
.iter()
.filter(|item| item.id != id && item.visible_at(now))
.max_by_key(|item| (item.priority, item.updated_at, item.id))
}

pub fn keeps_visible(&self) -> bool {
let now = Instant::now();
self.activities
Expand Down
22 changes: 17 additions & 5 deletions crates/winisland-core/src/multitask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use std::time::{Duration, Instant};

use crate::anim::{Curve, Tween};

const SEPARATE_DURATION: Duration = Duration::from_millis(750);
const MERGE_DURATION: Duration = Duration::from_millis(500);
const EXPAND_MERGE_DURATION: Duration = Duration::from_millis(320);
const CONTENT_DURATION: Duration = Duration::from_millis(140);
const SEPARATE_DURATION: Duration = Duration::from_millis(540);
const RESEPARATE_DURATION: Duration = Duration::from_millis(320);
const MERGE_DURATION: Duration = Duration::from_millis(360);
const EXPAND_MERGE_DURATION: Duration = Duration::from_millis(230);
const CONTENT_DURATION: Duration = Duration::from_millis(100);

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct TaskId(pub u64);
Expand Down Expand Up @@ -35,6 +36,7 @@ pub struct Multitask<T> {
current: Option<Task<T>>,
separation: Tween,
content: Tween,
detached: Option<TaskId>,
}

impl<T: Copy + PartialEq> Default for Multitask<T> {
Expand All @@ -50,6 +52,7 @@ impl<T: Copy + PartialEq> Multitask<T> {
current: None,
separation: Tween::new(0.0),
content: Tween::new(0.0),
detached: None,
}
}

Expand All @@ -76,7 +79,11 @@ impl<T: Copy + PartialEq> Multitask<T> {
&& (compact_settled || self.separation.value() > 0.0)
&& !replacing
&& self.current.is_some();
let duration = if detach {
let returning =
self.detached.is_some() && self.detached == self.current.map(|task| task.id);
let duration = if detach && returning {
RESEPARATE_DURATION
} else if detach {
SEPARATE_DURATION
} else if presentation == Presentation::Expanded {
EXPAND_MERGE_DURATION
Expand All @@ -100,6 +107,11 @@ impl<T: Copy + PartialEq> Multitask<T> {
.retarget(1.0, SEPARATE_DURATION, Curve::EaseInRebound, now);
}
}
if self.current.is_none() {
self.detached = None;
} else if self.separation.value() >= 0.99 {
self.detached = self.current.map(|task| task.id);
}
let show_content = self.current.is_some() && self.separation.value() >= 0.55 && detach;
self.content.retarget(
if show_content { 1.0 } else { 0.0 },
Expand Down
2 changes: 1 addition & 1 deletion crates/winisland-platform-windows/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ winisland-platform = { path = "../winisland-platform" }
windows-core = "0.62.2"
windows-collections = "0.3.2"
windows-registry = "0.6.1"
windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_System_Com", "Win32_System_Ole", "Win32_System_Wmi", "Win32_System_Variant", "Win32_Globalization", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_DataExchange", "Win32_System_Memory","Win32_System_SystemInformation", "Win32_System_Performance", "Win32_System_LibraryLoader", "Win32_System_EventLog", "Win32_System_WinRT", "Win32_System_WinRT_Composition", "Win32_Media_MediaFoundation", "Win32_Graphics_Direct2D", "Win32_Graphics_Direct2D_Common", "Win32_System_WinRT_Graphics_Direct2D", "Graphics", "Win32_Media_Audio", "Win32_Media_Audio_Endpoints", "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_Touch", "Win32_Storage_Packaging_Appx", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Dwm", "Win32_UI_Magnification", "Win32_UI_HiDpi", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_System_WinRT_Direct3D11", "Win32_System_WinRT_Graphics_Capture", "Graphics_Capture", "Graphics_DirectX", "Security_Authorization_AppCapabilityAccess", "Graphics_DirectX_Direct3D11", "Graphics_Effects", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_Storage_FileSystem", "ApplicationModel", "Foundation", "Foundation_Metadata", "Globalization", "Foundation_Collections", "Storage_Streams", "UI_Notifications", "UI_Notifications_Management", "UI_Composition", "UI_Composition_Desktop", "Media_Control", "Graphics_Imaging", "Data_Xml_Dom", "System"] }
windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_System_Com", "Win32_System_Ole", "Win32_System_Wmi", "Win32_System_Variant", "Win32_Globalization", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_DataExchange", "Win32_System_Memory","Win32_System_SystemInformation", "Win32_System_Performance", "Win32_System_LibraryLoader", "Win32_System_EventLog", "Win32_System_WinRT", "Win32_System_WinRT_Composition", "Win32_Media_MediaFoundation", "Win32_Graphics_Direct2D", "Win32_Graphics_Direct2D_Common", "Win32_System_WinRT_Graphics_Direct2D", "Graphics", "Win32_Media_Audio", "Win32_Media_Audio_Endpoints", "Win32_Devices_FunctionDiscovery", "Win32_System_Com_StructuredStorage", "Win32_UI_Shell_PropertiesSystem", "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_Touch", "Win32_Storage_Packaging_Appx", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Dwm", "Win32_UI_Magnification", "Win32_UI_HiDpi", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_System_WinRT_Direct3D11", "Win32_System_WinRT_Graphics_Capture", "Graphics_Capture", "Graphics_DirectX", "Security_Authorization_AppCapabilityAccess", "Graphics_DirectX_Direct3D11", "Graphics_Effects", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_Storage_FileSystem", "ApplicationModel", "Foundation", "Foundation_Metadata", "Globalization", "Foundation_Collections", "Storage_Streams", "UI_Notifications", "UI_Notifications_Management", "UI_Composition", "UI_Composition_Desktop", "Media_Control", "Graphics_Imaging", "Data_Xml_Dom", "System"] }
dirs = "6.0"
log = { version = "0.4", features = ["std"] }
parking_lot = "0.12"
Expand Down
12 changes: 11 additions & 1 deletion crates/winisland-platform-windows/src/audio.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod capture;
mod outputs;
mod volume;

use std::cell::RefCell;

use winisland_platform::{
AudioMeter, AudioProvider, PlatformError, ProcessCapture, VolumeEndpoint, VolumeState,
AudioMeter, AudioOutputDevice, AudioProvider, PlatformError, ProcessCapture, VolumeEndpoint,
VolumeState,
};

pub struct WindowsAudio;
Expand Down Expand Up @@ -76,4 +78,12 @@ impl AudioProvider for WindowsAudio {
.is_some_and(VolumeEndpoint::take_device_change))
})
}

fn output_devices(&self) -> Result<Vec<AudioOutputDevice>, PlatformError> {
outputs::output_devices()
}

fn set_default_output(&self, id: &str) -> Result<(), PlatformError> {
outputs::set_default_output(id)
}
}
116 changes: 116 additions & 0 deletions crates/winisland-platform-windows/src/audio/outputs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::ffi::c_void;

use windows::Win32::Devices::FunctionDiscovery::PKEY_Device_FriendlyName;
use windows::Win32::Media::Audio::{
DEVICE_STATE_ACTIVE, DigitalAudioDisplayDevice, ERole, Headphones, Headset, IMMDevice,
IMMDeviceEnumerator, MMDeviceEnumerator, PKEY_AudioEndpoint_FormFactor, eCommunications,
eConsole, eMultimedia, eRender,
};
use windows::Win32::System::Com::StructuredStorage::{PropVariantToBSTR, PropVariantToUInt32};
use windows::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance, CoTaskMemFree, STGM_READ};
use windows::core::{GUID, HRESULT, HSTRING, IUnknown, IUnknown_Vtbl, Interface, PCWSTR, PWSTR};
use winisland_platform::{AudioOutputDevice, AudioOutputKind, PlatformError};

use crate::com::ComGuard;

const CLSID_POLICY_CONFIG: GUID = GUID::from_u128(0x870af99c_171d_4f9e_af0d_e63df40c2bc9);
const IID_POLICY_CONFIG: GUID = GUID::from_u128(0xf8679f50_850a_41cf_9c72_430f290290c8);

/// Leading slots of the undocumented `IPolicyConfig` interface Windows uses to switch outputs.
#[repr(C)]
struct PolicyConfigVtbl {
base: IUnknown_Vtbl,
unused: [usize; 10],
set_default_endpoint: unsafe extern "system" fn(*mut c_void, PCWSTR, ERole) -> HRESULT,
}

fn take_string(value: PWSTR) -> String {
// SAFETY: The string was allocated by COM for the caller and is freed exactly once.
unsafe {
let text = value.to_string().unwrap_or_default();
CoTaskMemFree(Some(value.0 as *const c_void));
text
}
}

fn device_id(device: &IMMDevice) -> Option<String> {
// SAFETY: device is used on the COM thread that created it.
unsafe { device.GetId().ok().map(take_string) }
}

fn describe(device: &IMMDevice, default_id: Option<&str>) -> Option<AudioOutputDevice> {
let id = device_id(device)?;
// SAFETY: The property store and its values stay on the COM thread that opened them.
let (name, form_factor) = unsafe {
let store = device.OpenPropertyStore(STGM_READ).ok()?;
let name = store
.GetValue(&PKEY_Device_FriendlyName)
.and_then(|value| PropVariantToBSTR(&value))
.map(|name| name.to_string())
.unwrap_or_default();
let form_factor = store
.GetValue(&PKEY_AudioEndpoint_FormFactor)
.and_then(|value| PropVariantToUInt32(&value))
.map_or(-1, |value| value as i32);
(name, form_factor)
};
let kind = if form_factor == Headphones.0 || form_factor == Headset.0 {
AudioOutputKind::Headphones
} else if form_factor == DigitalAudioDisplayDevice.0 {
AudioOutputKind::Display
} else {
AudioOutputKind::Speakers
};
Some(AudioOutputDevice {
is_default: default_id == Some(id.as_str()),
id,
name,
kind,
})
}

pub(super) fn output_devices() -> Result<Vec<AudioOutputDevice>, PlatformError> {
let _com = ComGuard::mta()?;
// SAFETY: The enumerator and collection are used on this COM-initialized thread.
unsafe {
let enumerator: IMMDeviceEnumerator =
CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)
.map_err(PlatformError::backend)?;
let default_id = enumerator
.GetDefaultAudioEndpoint(eRender, eConsole)
.ok()
.and_then(|device| device_id(&device));
let collection = enumerator
.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)
.map_err(PlatformError::backend)?;
let count = collection.GetCount().map_err(PlatformError::backend)?;
Ok((0..count)
.filter_map(|index| collection.Item(index).ok())
.filter_map(|device| describe(&device, default_id.as_deref()))
.collect())
}
}

pub(super) fn set_default_output(id: &str) -> Result<(), PlatformError> {
let _com = ComGuard::mta()?;
let id = HSTRING::from(id);
// SAFETY: The policy object is queried for its documented-by-convention IID and released
// when `policy` drops; the vtable layout matches IPolicyConfig on Windows 10 and later.
unsafe {
let unknown: IUnknown = CoCreateInstance(&CLSID_POLICY_CONFIG, None, CLSCTX_ALL)
.map_err(PlatformError::backend)?;
let mut raw = std::ptr::null_mut();
unknown
.query(&IID_POLICY_CONFIG, &mut raw)
.ok()
.map_err(PlatformError::backend)?;
let policy = IUnknown::from_raw(raw);
let vtable = *(policy.as_raw() as *const *const PolicyConfigVtbl);
for role in [eConsole, eMultimedia, eCommunications] {
((*vtable).set_default_endpoint)(policy.as_raw(), PCWSTR(id.as_ptr()), role)
.ok()
.map_err(PlatformError::backend)?;
}
}
Ok(())
}
7 changes: 4 additions & 3 deletions crates/winisland-platform-windows/src/backdrop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,11 @@ impl WindowBackdrop {
})
}

pub(crate) fn update(&self, width: f32, height: f32, radius: f32) -> windows::core::Result<()> {
pub(crate) fn update(&self, rect: [f32; 4], radius: f32) -> windows::core::Result<()> {
let [x, y, width, height] = rect;
let shape = (width > 0.0 && height > 0.0).then(|| ShapeGeometry {
x: 0.0,
y: 0.0,
x: quantize(x),
y: quantize(y),
width: quantize(width),
height: quantize(height),
radius: quantize(radius.max(0.0)),
Expand Down
19 changes: 17 additions & 2 deletions crates/winisland-platform-windows/src/window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,21 @@ impl WindowSystem for WindowsWindowSystem {
}
}

fn set_text_input(&self, id: WindowId, area: Option<winisland_platform::Rect>) {
let _ = with_window(id, |window| {
window.set_ime_allowed(area.is_some());
if let Some(area) = area {
window.set_ime_cursor_area(
PhysicalPosition::new(area.left, area.top),
PhysicalSize::new(
(area.right - area.left).max(1) as u32,
(area.bottom - area.top).max(1) as u32,
),
);
}
});
}

fn set_titlebar_theme(&self, id: WindowId, is_light: bool) {
let _ = with_window(id, |window| {
if let Some(hwnd) = window_hwnd(window) {
Expand Down Expand Up @@ -678,7 +693,7 @@ impl WindowSystem for WindowsWindowSystem {
Ok(())
}

fn update_window_backdrop(&self, id: WindowId, width: f32, height: f32, radius: f32) -> bool {
fn update_window_backdrop(&self, id: WindowId, rect: [f32; 4], radius: f32) -> bool {
let window_backdrop = WINDOWS.with(|windows| {
windows
.borrow()
Expand All @@ -688,7 +703,7 @@ impl WindowSystem for WindowsWindowSystem {
let Some(window_backdrop) = window_backdrop else {
return false;
};
if let Err(error) = window_backdrop.update(width, height, radius) {
if let Err(error) = window_backdrop.update(rect, radius) {
log::warn!("Window backdrop update failed: {error}");
let released = WINDOWS.with(|windows| {
windows
Expand Down
6 changes: 5 additions & 1 deletion crates/winisland-platform/src/audio.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{PlatformError, VolumeCommand, VolumeState};
use crate::{AudioOutputDevice, PlatformError, VolumeCommand, VolumeState};

/// Device-scoped meter. Use on an audio worker; calls return empty or zero when unavailable.
pub trait AudioMeter {
Expand Down Expand Up @@ -60,4 +60,8 @@ pub trait AudioProvider {
fn toggle_mute(&self) -> Result<(), PlatformError>;
/// Polls for a default device change, or returns a subscription error.
fn poll_device_events(&self) -> Result<bool, PlatformError>;
/// Lists active playback devices, marking the default one.
fn output_devices(&self) -> Result<Vec<AudioOutputDevice>, PlatformError>;
/// Makes `id` the default playback device for every role.
fn set_default_output(&self, id: &str) -> Result<(), PlatformError>;
}
15 changes: 15 additions & 0 deletions crates/winisland-platform/src/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,21 @@ pub struct VolumeState {
pub muted: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AudioOutputKind {
Speakers,
Headphones,
Display,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AudioOutputDevice {
pub id: String,
pub name: String,
pub kind: AudioOutputKind,
pub is_default: bool,
}

#[derive(Clone, Copy, Debug)]
pub enum VolumeCommand {
StepUp,
Expand Down
9 changes: 6 additions & 3 deletions crates/winisland-platform/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ pub trait WindowSystem {
/// Sets the cursor icon; a missing window is ignored.
fn set_cursor(&self, id: WindowId, cursor: CursorKind);
fn set_plugin_input(&self, id: WindowId, capture: bool, keyboard: bool);
/// Enables IME text entry with candidates placed at `area` in client physical pixels, or
/// disables it with `None`; a missing window is ignored.
fn set_text_input(&self, id: WindowId, area: Option<crate::Rect>);
/// Applies the titlebar theme; unsupported or missing windows are ignored.
fn set_titlebar_theme(&self, id: WindowId, is_light: bool);
/// Starts composition backdrop for a live overlay; unavailable support returns an error.
Expand All @@ -127,9 +130,9 @@ pub trait WindowSystem {
/// Starts composition backdrop beneath a settings window's own content; disabled transparency
/// or unavailable support returns an error.
fn start_window_backdrop(&self, id: WindowId) -> Result<(), PlatformError>;
/// Shapes the window backdrop from the client origin in physical pixels; false means absent,
/// and an update error releases it.
fn update_window_backdrop(&self, id: WindowId, width: f32, height: f32, radius: f32) -> bool;
/// Shapes the window backdrop as a rounded rectangle in client physical pixels; false means
/// absent, and an update error releases it.
fn update_window_backdrop(&self, id: WindowId, rect: [f32; 4], radius: f32) -> bool;
/// Starts screen sampling beneath an overlay on the adapter with `adapter_luid`, excluding the
/// overlay from all screen captures until stopped; unavailable support returns an error.
fn start_backdrop_capture(
Expand Down
Loading
Loading