Refactor the imp/gui bondary to use real traits #18

Merged
liamwarfield merged 8 commits from gui-platform-boundary-refactor into main 2026-02-18 04:53:41 +00:00
6 changed files with 459 additions and 22 deletions
Showing only changes of commit ff14f577fe - Show all commits
+8 -5
View File
@@ -7,7 +7,10 @@ use std::cell::RefCell;
use std::sync::Arc;
use tracing::{error, info};
use crate::imp;
use crate::imp::{CurrentPlatform, PlatformRuntime, SpawnHandleTrait};
/// The spawn handle type for the current platform.
type SpawnHandle = <CurrentPlatform as PlatformRuntime>::SpawnHandle;
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
// TODO: make this user configurable.
@@ -33,7 +36,7 @@ enum DenoisingModelState {
}
fn with_denoising_model<O>(
spawn: &imp::SpawnHandle,
spawn: &SpawnHandle,
func: impl FnOnce(&mut DfTract) -> O,
) -> Option<O> {
// Using a thread local is super gross, but DfTract is not Send (so it can never leave the current
@@ -89,7 +92,7 @@ fn with_denoising_model<O>(
pub struct AudioProcessor {
denoise: bool,
spawn: imp::SpawnHandle,
spawn: SpawnHandle,
buffer: Vec<f32>,
noise_floor: f32,
/// Whether we were transmitting in the previous frame
@@ -102,7 +105,7 @@ impl AudioProcessor {
pub fn new_plain() -> Self {
AudioProcessor {
denoise: false,
spawn: imp::SpawnHandle::current(),
spawn: SpawnHandle::current(),
buffer: Vec::new(),
noise_floor: DEFAULT_NOISE_FLOOR,
was_transmitting: false,
@@ -113,7 +116,7 @@ impl AudioProcessor {
pub fn new_denoising() -> Self {
AudioProcessor {
denoise: true,
spawn: imp::SpawnHandle::current(),
spawn: SpawnHandle::current(),
buffer: Vec::new(),
noise_floor: DEFAULT_NOISE_FLOOR,
was_transmitting: false,
+105 -4
View File
@@ -1,13 +1,114 @@
use crate::app::Command;
Outdated
Review

Add a doc comment here for what this file is, here and elsewhere in imp

Add a doc comment here for what this file is, here and elsewhere in imp
Outdated
Review

I'm not sure if this is actually needed.

I'm not sure if this is actually needed.
use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver;
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
use mumble_web2_common::ClientConfig;
use mumble_web2_common::{ClientConfig, ServerStatus};
use std::collections::HashMap;
pub use tokio::runtime::Handle as SpawnHandle;
pub use tokio::task::spawn;
pub use tokio::time::sleep;
use std::future::Future;
use std::time::Duration;
liamwarfield marked this conversation as resolved Outdated
Outdated
Review

Delete this bar

Delete this bar
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use super::connect::*;
pub use super::native_audio::*;
pub use tokio::task::spawn;
pub use tokio::time::sleep;
// ============================================================================
// Platform Struct
// ============================================================================
/// Desktop platform implementation using Tokio and native audio.
pub struct DesktopPlatform;
// ============================================================================
// SpawnHandle
// ============================================================================
pub type SpawnHandle = tokio::runtime::Handle;
impl SpawnHandleTrait for SpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::runtime::Handle::spawn(self, future);
}
fn current() -> Self {
tokio::runtime::Handle::current()
}
}
// ============================================================================
// Trait Implementations
// ============================================================================
impl PlatformRuntime for DesktopPlatform {
type SpawnHandle = SpawnHandle;
fn spawn<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::task::spawn(future);
}
async fn sleep(duration: Duration) {
tokio::time::sleep(duration).await;
}
}
impl PlatformConfig for DesktopPlatform {
async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await
}
fn load_username() -> Option<String> {
load_username()
}
fn load_server_url() -> Option<String> {
load_server_url()
}
fn set_default_username(username: &str) -> Option<()> {
set_default_username(username)
}
fn set_default_server(server: &str) -> Option<()> {
set_default_server(server)
}
}
impl PlatformNetwork for DesktopPlatform {
async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> Result<(), Error> {
network_connect(address, username, event_rx, gui_config).await
}
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await
}
}
impl PlatformInit for DesktopPlatform {
fn init_logging() {
init_logging();
}
fn request_permissions() {
// No-op on desktop
}
}
impl Platform for DesktopPlatform {}
fn get_config_path() -> std::path::PathBuf {
let strategy = choose_app_strategy(AppStrategyArgs {
top_level_domain: "com".to_string(),
+105 -6
View File
@@ -1,16 +1,115 @@
use android_permissions::{PermissionManager, RECORD_AUDIO};
use crate::app::Command;
use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver;
use jni::{objects::JObject, JavaVM};
use mumble_web2_common::ClientConfig;
use mumble_web2_common::{ClientConfig, ServerStatus};
use std::future::Future;
use std::time::Duration;
use std::collections::HashMap;
pub use tokio::runtime::Handle as SpawnHandle;
pub use tokio::task::spawn;
pub use tokio::time::sleep;
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use super::connect::*;
pub use super::native_audio::*;
pub fn set_default_username(username: &str) -> Option<()> {
pub use tokio::task::spawn;
pub use tokio::time::sleep;
// ============================================================================
// Platform Struct
// ============================================================================
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
pub struct MobilePlatform;
// ============================================================================
// SpawnHandle
// ============================================================================
pub type SpawnHandle = tokio::runtime::Handle;
impl SpawnHandleTrait for SpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::runtime::Handle::spawn(self, future);
}
fn current() -> Self {
tokio::runtime::Handle::current()
}
}
// ============================================================================
// Trait Implementations
// ============================================================================
impl PlatformRuntime for MobilePlatform {
type SpawnHandle = SpawnHandle;
fn spawn<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::task::spawn(future);
}
async fn sleep(duration: Duration) {
tokio::time::sleep(duration).await;
}
}
impl PlatformConfig for MobilePlatform {
async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await
}
fn load_username() -> Option<String> {
load_username()
}
fn load_server_url() -> Option<String> {
load_server_url()
}
fn set_default_username(username: &str) -> Option<()> {
set_default_username(username)
}
fn set_default_server(server: &str) -> Option<()> {
set_default_server(server)
}
}
impl PlatformNetwork for MobilePlatform {
async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> Result<(), Error> {
network_connect(address, username, event_rx, gui_config).await
}
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await
}
}
impl PlatformInit for MobilePlatform {
fn init_logging() {
init_logging();
}
fn request_permissions() {
request_recording_permission();
}
}
impl Platform for MobilePlatform {}
pub fn set_default_username(_username: &str) -> Option<()> {
None
}
+147 -3
View File
@@ -1,3 +1,126 @@
//! Platform abstraction layer
//!
//! This module defines traits that each platform (web, desktop, mobile) must implement.
//! The traits make the platform boundary explicit and provide compile-time verification.
use crate::app::Command;
use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver;
use mumble_web2_common::{ClientConfig, ServerStatus};
use std::future::Future;
use std::time::Duration;
// ============================================================================
// Trait Definitions
// ============================================================================
/// Trait for spawn handles that can be stored and used to spawn tasks later.
#[cfg(feature = "web")]
liamwarfield marked this conversation as resolved Outdated
Outdated
Review

We need to add a doc comment for the overall trait here.

We need to add a doc comment for the overall trait here.
Outdated
Review

Done

Done
pub trait SpawnHandleTrait: Clone + 'static {
/// Spawn an async task using this handle.
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static;
/// Get a spawn handle for the current context.
fn current() -> Self;
}
/// Trait for spawn handles that can be stored and used to spawn tasks later.
#[cfg(any(feature = "desktop", feature = "mobile"))]
pub trait SpawnHandleTrait: Clone + 'static {
/// Spawn an async task using this handle.
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static;
/// Get a spawn handle for the current context.
fn current() -> Self;
}
/// Runtime primitives: task spawning and async sleep.
#[cfg(feature = "web")]
pub trait PlatformRuntime {
/// The spawn handle type for this platform.
type SpawnHandle: SpawnHandleTrait;
/// Spawn an async task.
fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static;
/// Async sleep for the given duration.
fn sleep(duration: Duration) -> impl Future<Output = ()>;
}
/// Runtime primitives: task spawning and async sleep.
#[cfg(any(feature = "desktop", feature = "mobile"))]
pub trait PlatformRuntime {
/// The spawn handle type for this platform.
type SpawnHandle: SpawnHandleTrait;
/// Spawn an async task.
fn spawn<F>(future: F)
where
F: Future<Output = ()> + Send + 'static;
/// Async sleep for the given duration.
fn sleep(duration: Duration) -> impl Future<Output = ()>;
}
/// Configuration persistence: loading and saving user preferences.
pub trait PlatformConfig {
/// Load the client configuration (proxy URL, cert hash, etc.).
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
/// Load saved username.
fn load_username() -> Option<String>;
/// Load saved server URL.
fn load_server_url() -> Option<String>;
/// Save the default username.
fn set_default_username(username: &str) -> Option<()>;
/// Save the default server URL.
fn set_default_server(server: &str) -> Option<()>;
}
/// Network operations: connecting to servers.
pub trait PlatformNetwork {
/// Establish a connection to the Mumble server and run the network loop.
fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> impl Future<Output = Result<(), Error>>;
/// Get server status (user count, version, etc.).
fn get_status(client: &reqwest::Client)
-> impl Future<Output = color_eyre::Result<ServerStatus>>;
}
/// Platform initialization.
pub trait PlatformInit {
/// Initialize logging for the platform.
fn init_logging();
/// Request runtime permissions (Android audio recording, etc.).
fn request_permissions();
}
/// Combined platform trait.
///
/// This is the main trait that each platform must implement. It combines all
/// platform-specific functionality into a single interface, providing compile-time
/// verification that all platforms implement the required functionality.
pub trait Platform: PlatformRuntime + PlatformConfig + PlatformNetwork + PlatformInit {}
// ============================================================================
// Platform Modules
// ============================================================================
#[cfg(feature = "web")]
mod web;
@@ -11,6 +134,30 @@ mod desktop;
#[cfg(feature = "mobile")]
mod mobile;
// ============================================================================
// Platform Type Alias
// ============================================================================
/// The current platform type, selected at compile time based on features.
#[cfg(feature = "web")]
pub type CurrentPlatform = web::WebPlatform;
#[cfg(feature = "desktop")]
pub type CurrentPlatform = desktop::DesktopPlatform;
#[cfg(feature = "mobile")]
pub type CurrentPlatform = mobile::MobilePlatform;
/// Compile-time assertion that CurrentPlatform implements Platform.
const _: () = {
fn assert_platform<T: Platform>() {}
let _ = assert_platform::<CurrentPlatform>;
};
// ============================================================================
// Platform Re-exports
// ============================================================================
#[cfg(feature = "desktop")]
pub use desktop::*;
#[cfg(feature = "mobile")]
@@ -24,6 +171,3 @@ pub fn request_permissions() {}
#[cfg(all(feature = "web", not(any(feature = "desktop", feature = "mobile"))))]
pub use web::*;
#[cfg(any(feature = "desktop"))]
pub use desktop::*;
2
+91 -1
View File
@@ -29,7 +29,6 @@ use web_sys::AudioWorkletNode;
use web_sys::EncodedAudioChunk;
use web_sys::EncodedAudioChunkInit;
use web_sys::EncodedAudioChunkType;
use web_sys::MediaStream;
use web_sys::MediaStreamConstraints;
use web_sys::MessageEvent;
use web_sys::WebTransport;
@@ -39,6 +38,8 @@ use web_sys::WorkletOptions;
use web_sys::{console, window};
use web_sys::{AudioContext, AudioDataCopyToOptions};
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use wasm_bindgen_futures::spawn_local as spawn;
pub trait ImpRead: AsyncRead + Unpin + 'static {}
@@ -47,10 +48,98 @@ impl<T: AsyncRead + Unpin + 'static> ImpRead for T {}
pub trait ImpWrite: AsyncWrite + Unpin + 'static {}
impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {}
// ============================================================================
// Platform Struct
// ============================================================================
/// Web platform implementation using WebTransport and Web Audio API.
pub struct WebPlatform;
pub async fn sleep(d: Duration) {
TimeoutFuture::new(d.as_millis() as u32).await
}
// ============================================================================
// Trait Implementations
// ============================================================================
impl SpawnHandleTrait for SpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
fn current() -> Self {
SpawnHandle
}
}
impl PlatformRuntime for WebPlatform {
type SpawnHandle = SpawnHandle;
fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
async fn sleep(duration: Duration) {
TimeoutFuture::new(duration.as_millis() as u32).await;
}
}
impl PlatformConfig for WebPlatform {
async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await
}
fn load_username() -> Option<String> {
load_username()
}
fn load_server_url() -> Option<String> {
load_server_url()
}
fn set_default_username(username: &str) -> Option<()> {
set_default_username(username)
}
fn set_default_server(server: &str) -> Option<()> {
set_default_server(server)
}
}
impl PlatformNetwork for WebPlatform {
async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> Result<(), Error> {
network_connect(address, username, event_rx, gui_config).await
}
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await
}
}
impl PlatformInit for WebPlatform {
fn init_logging() {
init_logging();
}
fn request_permissions() {
// No-op on web
}
}
impl Platform for WebPlatform {}
trait ResultExt<T> {
fn ey(self) -> Result<T, Error>;
}
@@ -495,6 +584,7 @@ pub fn init_logging() {
info!("logging initiated");
}
#[derive(Clone)]
pub struct SpawnHandle;
impl SpawnHandle {
+3 -3
View File
@@ -27,7 +27,7 @@ use tracing::error;
use tracing::info;
use crate::effects::AudioProcessor;
use crate::imp::AudioSystem;
use crate::imp::{AudioSystem, CurrentPlatform, Platform, PlatformNetwork, PlatformRuntime};
pub mod app;
mod effects;
@@ -47,7 +47,7 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
*STATE.server.write() = Default::default();
*STATE.status.write() = ConnectionState::Connecting;
if let Err(error) = imp::network_connect(address, username, &mut event_rx, &config).await {
if let Err(error) = CurrentPlatform::network_connect(address, username, &mut event_rx, &config).await {
error!("could not connect {:?}", error);
*STATE.status.write() = ConnectionState::Failed(error.to_string());
} else {
@@ -105,7 +105,7 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
break;
}
imp::sleep(Duration::from_millis(3000)).await;
CurrentPlatform::sleep(Duration::from_millis(3000)).await;
}
});
}