feat(native): Tier A desktop features (P5-46/36/44/43/49) + window chrome (P5-47)
Adds a `native/` module system (each feature = its own module exposing `#[tauri::command]`s + optional `setup`; `emit_to_web` pushes DOM CustomEvents to the web like `forward_deeplink`). Wired into generate_handler! + native::setup; windows-crate feature union added to Cargo.toml. - power.rs (P5-46): SetThreadExecutionState held on the main thread while a call is active; released on end. Cross-platform (no-op off Windows). - jumplist.rs (P5-36): ICustomDestinationList "Recent Rooms" of IShellLink tasks launching the exe with a matrix: arg (existing deep-link handler opens the room). - thumbbar.rs (P5-44): ITaskbarList3 ThumbBar Mute/Deafen/End (GDI HICONs) + a window subclass catching THBN_CLICKED → emit thumbbar-action. - smtc.rs (P5-43): WinRT SystemMediaTransportControls via GetForWindow; ButtonPressed → smtc-action; call-state command. (Experimental for a non-media app.) - network.rs (P5-49): INetworkListManager poll thread → emit network-changed. - chrome.rs (P5-47): cross-platform window-control commands + set_custom_chrome (set_decorations) for the opt-in TDS titlebar. NOT compile-verified locally (no Rust/Windows toolchain on the dev box) — this is for the CI Windows compile pass (GitHub test.yml / Gitea windows runner). Expect a possible fixup round (windows-crate feature/namespace paths, e.g. subclass APIs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
//! P5-43 — System Media Transport Controls (SMTC) call surface (Windows).
|
||||
//!
|
||||
//! Surfaces the active voice/video call to the Windows media overlay (the
|
||||
//! volume flyout / SMTC card) so the user can mute or hang up from the OS
|
||||
//! media controls. We create the SMTC via the WinRT interop factory
|
||||
//! (`ISystemMediaTransportControlsInterop::GetForWindow`), keep the object in
|
||||
//! managed state, and let the web client drive its state through
|
||||
//! `set_smtc_call_state` as the call-embed atom / mic state changes.
|
||||
//!
|
||||
//! Button mapping: **Play/Pause → mute toggle**, **Stop → end call**. Presses
|
||||
//! are forwarded to the web client as a `smtc-action` DOM CustomEvent (see
|
||||
//! `super::emit_to_web`) with `action` in `"mute" | "end"`; the web hook
|
||||
//! (`useTauriSmtc`) translates them into `CallControl.toggleMicrophone()` /
|
||||
//! `CallEmbed.hangup()`.
|
||||
//!
|
||||
//! RUNTIME NOTE: SMTC is designed for real media apps. For a non-media app the
|
||||
//! card may not actually appear unless the process owns an active audio session
|
||||
//! recognised by the system. This module prioritises a clean compile and
|
||||
//! correct WinRT API usage; visibility of the overlay at runtime is uncertain
|
||||
//! and may depend on the embedded Element Call iframe holding an audio session.
|
||||
//!
|
||||
//! Other platforms are a no-op (SMTC is Windows-only); the command keeps an
|
||||
//! identical cross-platform signature so the web side stays unconditional.
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// Payload for the `smtc-action` DOM event forwarded to the web client.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[derive(serde::Serialize)]
|
||||
struct Ev {
|
||||
action: String,
|
||||
}
|
||||
|
||||
/// Holds the SMTC object (and its `ButtonPressed` registration token) in Tauri
|
||||
/// managed state so `set_smtc_call_state` can update it at runtime. Mirrors the
|
||||
/// `TrayUnreadState` managed-state pattern in lib.rs.
|
||||
#[cfg(target_os = "windows")]
|
||||
struct SmtcState {
|
||||
controls: std::sync::Mutex<Option<windows::Media::SystemMediaTransportControls>>,
|
||||
// Kept alive so the ButtonPressed handler stays registered for the app's
|
||||
// lifetime; never unregistered.
|
||||
_token: windows::Foundation::EventRegistrationToken,
|
||||
}
|
||||
|
||||
/// Called once from `native::setup()`. Creates and configures the SMTC on
|
||||
/// Windows; no-op elsewhere. SMTC init failures are logged and swallowed so a
|
||||
/// missing/unsupported overlay never blocks app startup.
|
||||
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Err(err) = init_smtc(app) {
|
||||
eprintln!("smtc: failed to initialize System Media Transport Controls: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = app;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn init_smtc(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use tauri::Manager;
|
||||
use windows::core::{factory, HSTRING};
|
||||
use windows::Foundation::TypedEventHandler;
|
||||
use windows::Media::{
|
||||
MediaPlaybackStatus, MediaPlaybackType, SystemMediaTransportControls,
|
||||
SystemMediaTransportControlsButton, SystemMediaTransportControlsButtonPressedEventArgs,
|
||||
};
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::System::WinRT::ISystemMediaTransportControlsInterop;
|
||||
|
||||
let window = app
|
||||
.get_webview_window("main")
|
||||
.ok_or("smtc: main window not found")?;
|
||||
// Match the HWND conversion used by set_badge_count in lib.rs.
|
||||
let hwnd = HWND(window.hwnd()?.0 as _);
|
||||
|
||||
// SMTC has no WinRT constructor; it's obtained per-window via the interop
|
||||
// factory. `factory::<C, I>()` fetches the activation factory for the
|
||||
// runtime class `C` cast to the classic COM interop interface `I`.
|
||||
let interop =
|
||||
factory::<SystemMediaTransportControls, ISystemMediaTransportControlsInterop>()?;
|
||||
let controls: SystemMediaTransportControls = unsafe { interop.GetForWindow(hwnd)? };
|
||||
|
||||
controls.SetIsEnabled(true)?;
|
||||
controls.SetIsPlayEnabled(true)?;
|
||||
controls.SetIsPauseEnabled(true)?;
|
||||
controls.SetIsStopEnabled(true)?;
|
||||
|
||||
// Configure the card metadata once ("In call"); the web side only toggles
|
||||
// playback status afterwards.
|
||||
let updater = controls.DisplayUpdater()?;
|
||||
updater.SetType(MediaPlaybackType::Music)?;
|
||||
let music = updater.MusicProperties()?;
|
||||
music.SetTitle(&HSTRING::from("In call"))?;
|
||||
updater.Update()?;
|
||||
|
||||
// Idle until a call becomes active (set_smtc_call_state flips this).
|
||||
controls.SetPlaybackStatus(MediaPlaybackStatus::Closed)?;
|
||||
|
||||
// ButtonPressed → forward a normalized action to the web client.
|
||||
let app_for_handler = app.clone();
|
||||
let handler = TypedEventHandler::<
|
||||
SystemMediaTransportControls,
|
||||
SystemMediaTransportControlsButtonPressedEventArgs,
|
||||
>::new(move |_sender, args| {
|
||||
if let Some(args) = args.as_ref() {
|
||||
let button = args.Button()?;
|
||||
let action = if button == SystemMediaTransportControlsButton::Play
|
||||
|| button == SystemMediaTransportControlsButton::Pause
|
||||
{
|
||||
Some("mute")
|
||||
} else if button == SystemMediaTransportControlsButton::Stop {
|
||||
Some("end")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(action) = action {
|
||||
let payload = serde_json::to_string(&Ev {
|
||||
action: action.to_string(),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
super::emit_to_web(&app_for_handler, "smtc-action", &payload);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
let token = controls.ButtonPressed(&handler)?;
|
||||
|
||||
app.manage(SmtcState {
|
||||
controls: std::sync::Mutex::new(Some(controls)),
|
||||
_token: token,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reflect the call state onto the SMTC. When `active`, enable the controls and
|
||||
/// set playback status to Playing (unmuted) / Paused (muted); when inactive,
|
||||
/// mark the card Closed and disable it. Windows-only; no-op elsewhere.
|
||||
#[tauri::command]
|
||||
pub fn set_smtc_call_state(app: AppHandle, active: bool, muted: bool) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use tauri::Manager;
|
||||
if let Some(state) = app.try_state::<SmtcState>() {
|
||||
if let Ok(guard) = state.controls.lock() {
|
||||
if let Some(controls) = guard.as_ref() {
|
||||
let _ = apply_call_state(controls, active, muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// No-op off Windows; bind args so the signature is identical everywhere
|
||||
// and no unused warnings fire.
|
||||
let _ = (&app, active, muted);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn apply_call_state(
|
||||
controls: &windows::Media::SystemMediaTransportControls,
|
||||
active: bool,
|
||||
muted: bool,
|
||||
) -> windows::core::Result<()> {
|
||||
use windows::Media::MediaPlaybackStatus;
|
||||
|
||||
if active {
|
||||
controls.SetIsEnabled(true)?;
|
||||
let status = if muted {
|
||||
MediaPlaybackStatus::Paused
|
||||
} else {
|
||||
MediaPlaybackStatus::Playing
|
||||
};
|
||||
controls.SetPlaybackStatus(status)?;
|
||||
} else {
|
||||
controls.SetPlaybackStatus(MediaPlaybackStatus::Closed)?;
|
||||
controls.SetIsEnabled(false)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user