drteeth 0.1.0

Low-complexity web technology user interface library for desktop apps
//! Drteeth is a library that directly translates web application
//! architecture into a desktop app. A Drteeth app is litarally a web
//! application server and client bundled into a single binary
//! file. As such, all web application tools and skills translate
//! directly to use in Drteeth.
//!
//! Drteeth will help you quickly build an app that uses web
//! techologies for a slick user interface and runs on the desktop in
//! its own window, while the application logic is written in Rust and
//! has a normal application's access to operating system services.
//!
//! Drteeth works in concert with tokio, async-std, or smol, and has
//! `tokio`, `asyncstd`, and `smol` features to select which of the
//! three you wish to use. The `examples` feature is required to
//! successfully compile any of the examples.

use std::{future::Future, net::SocketAddr};
use wry::{
    application::{
        event::{Event, StartCause, WindowEvent},
        event_loop::{ControlFlow, EventLoop},
    },
    webview::WebViewBuilder,
};

pub use wry::application::window::{self, WindowBuilder};

/// Types which implement this trait can be passed to the launch
/// functions as the initial address to load in the webview.
///
/// It is strongly suggested to set up your server to only listen on a
/// loopback address. If you choose to have the server listen on a
/// routable address, take care to employ an authetication and
/// authorization mechanism in the server's request handlers. Exposing
/// the server on a routable address without authorization checks
/// would create an enormous security hole.
pub trait EntryPoint {
    fn url(&self) -> String;
}

impl EntryPoint for SocketAddr {
    fn url(&self) -> String {
        format!("http://{}:{}/", self.ip(), self.port())
    }
}

impl EntryPoint for &SocketAddr {
    fn url(&self) -> String {
        format!("http://{}:{}/", self.ip(), self.port())
    }
}

impl EntryPoint for String {
    fn url(&self) -> String {
        self.clone()
    }
}

impl EntryPoint for &str {
    fn url(&self) -> String {
        String::from(*self)
    }
}

trait DoneServing {
    fn done_serving(&self) -> bool;
}

#[cfg(feature = "tokio")]
impl<T> DoneServing for (tokio::runtime::Runtime, tokio::task::JoinHandle<T>) {
    fn done_serving(&self) -> bool {
        self.1.is_finished()
    }
}

#[cfg(feature = "asyncstd")]
impl<T> DoneServing
    for (
        std::sync::Arc<std::sync::Mutex<bool>>,
        async_std::task::JoinHandle<T>,
    )
{
    fn done_serving(&self) -> bool {
        let guard = self.0.lock().unwrap();
        *guard
    }
}

#[cfg(feature = "smol")]
impl<T> DoneServing for smol::Task<T> {
    fn done_serving(&self) -> bool {
        self.is_finished()
    }
}

#[cfg(all(feature = "tokio", not(feature = "asyncstd"), not(feature = "smol")))]
fn run_server<F>(serve: F) -> impl DoneServing
where
    F: Future<Output = ()> + Send + 'static,
{
    let runtime = tokio::runtime::Runtime::new().unwrap();
    let serving = runtime.spawn(serve);
    (runtime, serving)
}

#[cfg(all(not(feature = "tokio"), feature = "asyncstd", not(feature = "smol")))]
fn run_server<F>(serve: F) -> impl DoneServing
where
    F: Future<Output = ()> + Send + 'static,
{
    use std::sync::{Arc, Mutex};

    let finished = Arc::new(Mutex::new(false));

    (
        finished.clone(),
        async_std::task::spawn(async move {
            serve.await;
            let mut guard = finished.lock().unwrap();
            *guard = true;
        }),
    )
}

#[cfg(all(not(feature = "tokio"), not(feature = "asyncstd"), feature = "smol"))]
fn run_server<F>(serve: F) -> impl DoneServing
where
    F: Future<Output = ()> + Send + 'static,
{
    if let Err(_) = std::env::var("SMOL_THREADS") {
        std::env::set_var(
            "SMOL_THREADS",
            std::thread::available_parallelism()
                .unwrap_or(std::num::NonZeroUsize::new(1).unwrap())
                .to_string(),
        );
    }
    smol::spawn(serve)
}

#[cfg(any(
    all(feature = "tokio", feature = "asyncstd"),
    all(feature = "tokio", feature = "smol"),
    all(feature = "asyncstd", feature = "smol"),
    not(any(feature = "tokio", feature = "asyncstd", feature = "smol"))
))]
fn run_server<F>(serve: F) -> impl DoneServing {
    compile_error!("Exactly one of the tokio, asyncstd, or smol features must be enabled.")
}

/// Start the webview window, the windowing system event loop, and the
/// async event loop running. This function accepts a Wry
/// [`WindowBuilder`] (re-exported here for convenience) allowing for
/// detailed configuration of the properties of the window. Use this
/// version if you need to set window traits other than the title,
/// such as default size, position, or icon.
/// [`WindowBuilder`]: https://docs.rs/wry/latest/wry/application/window/struct.WindowBuilder.html
pub fn launch_with_windowbuilder<F>(
    window_builder: WindowBuilder,
    entrypoint: impl EntryPoint,
    serve: F,
) -> wry::Result<()>
where
    F: Future<Output = ()> + Send + 'static,
{
    let serving = run_server(serve);

    let entrypoint = entrypoint.url();

    let event_loop = EventLoop::new();
    let window = window_builder.build(&event_loop)?;
    let _webview = WebViewBuilder::new(window)?
        .with_url(&entrypoint)?
        .build()?;

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        if serving.done_serving() {
            *control_flow = ControlFlow::Exit;
        } else {
            match event {
                Event::NewEvents(StartCause::Init) => {}
                Event::WindowEvent {
                    event: WindowEvent::CloseRequested,
                    ..
                } => *control_flow = ControlFlow::Exit,
                _ => {}
            }
        }
    });
}

/// Start the webview window, the windowing system event loop, and the
/// async event loop running.
pub fn launch<F>(title: impl AsRef<str>, entrypoint: impl EntryPoint, serve: F) -> wry::Result<()>
where
    F: Future<Output = ()> + Send + 'static,
{
    launch_with_windowbuilder(
        WindowBuilder::new().with_title(title.as_ref()),
        entrypoint,
        serve,
    )
}