nrelm 0.1.0

An idiomatic GUI library inspired by Elm and based on gtk3-rs
#![doc(html_logo_url = "https://nrelm.org/icons/nrelm_logo.svg")]
#![doc(html_favicon_url = "https://nrelm.org/icons/nrelm_org.svg")]
#![doc = "An idiomatic GUI library inspired by Elm and based on gtk3-rs"]

//! Relm4 is an idiomatic GUI library inspired by Elm, built on top of
//! [gtk3-rs](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/).
//!
//! The crate is organized around components: a [`SimpleComponent`] owns a
//! model and a view built with the [`view!`](crate::view) macro. The
//! [`RelmApp`] ties everything together by running the GTK main loop.
//!
//! # Example
//!
//! ```no_run
//! use gtk::prelude::*;
//! use nrelm::{gtk, ComponentParts, ComponentSender, RelmApp, SimpleComponent};
//!
//! struct AppModel;
//!
//! #[nrelm::component]
//! impl SimpleComponent for AppModel {
//!     type Init = ();
//!     type Input = ();
//!     type Output = ();
//!
//!     fn init(
//!         _init: Self::Init,
//!         root: Self::Root,
//!         _sender: ComponentSender<Self>,
//!     ) -> ComponentParts<Self> {
//!         let widgets = view_output!();
//!         ComponentParts { model: AppModel, widgets }
//!     }
//!
//!     view! {
//!         root = gtk::Window {
//!             set_title: "Simple app",
//!             gtk::Label {
//!                 set_label: "Hello world!",
//!             },
//!         }
//!     }
//! }
//!
//! fn main() {
//!     let app = RelmApp::new("nrelm.example.simple");
//!     app.run::<AppModel>(());
//! }
//! ```
#![warn(
    missing_debug_implementations,
    missing_docs,
    rust_2018_idioms,
    unreachable_pub,
    unused_qualifications,
    clippy::cargo,
    clippy::must_use_candidate,
    clippy::used_underscore_binding
)]
#![allow(clippy::multiple_crate_versions)]
#![cfg_attr(docsrs, feature(doc_cfg))]

mod app;
mod channel;
mod extensions;
pub(crate) mod late_initialization;
mod runtime_util;

/// Abstractions around low-level GTK drawing and event handling.
pub mod abstractions;
pub mod actions;
pub mod binding;
pub mod component;
pub mod factory;
pub mod loading_widgets;
#[doc(hidden)]
pub mod macro_helper;
pub mod shared_state;

pub use channel::ComponentSender;
pub use channel::*;
pub use component::worker::{Worker, WorkerController, WorkerHandle};
pub use component::{
    Component, ComponentBuilder, ComponentController, ComponentParts, Controller, MessageBroker,
    SimpleComponent,
};
pub use extensions::*;
pub use shared_state::{AsyncReducer, AsyncReducible, Reducer, Reducible, SharedState};
pub use shutdown::ShutdownReceiver;

pub use app::RelmApp;
pub use tokio::task::JoinHandle;

use gtk::prelude::{Cast, CssProviderExt, IsA};
use once_cell::sync::{Lazy, OnceCell};
use runtime_util::{GuardedReceiver, RuntimeSenders, ShutdownOnDrop};
use std::cell::Cell;
use std::future::Future;
use tokio::runtime::Runtime;

/// The number of threads used by the internal tokio runtime.
///
/// Set this before the first call to [`spawn`] (for example at the start of
/// `main`) to configure how many worker threads the runtime may use. The
/// default is 1.
pub static RELM_THREADS: OnceCell<usize> = OnceCell::new();

/// The number of threads used for blocking operations.
///
/// Set this before the first call to [`spawn_blocking`] to configure how many
/// threads may be used for blocking tasks. The default is 512.
pub static RELM_BLOCKING_THREADS: OnceCell<usize> = OnceCell::new();

/// Commonly used re-exports of nrelm types.
pub mod prelude;

pub use gtk;

#[cfg(feature = "css")]
#[cfg_attr(docsrs, doc(cfg(feature = "css")))]
pub use nrelm_css as css;

#[cfg(feature = "macros")]
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
pub use nrelm_macros::*;

pub use once_cell;
pub use tokio;

thread_local! {
    static MAIN_APPLICATION: Cell<Option<gtk::Application>> = Cell::default();
}

fn set_main_application(app: impl IsA<gtk::Application>) {
    MAIN_APPLICATION.with(move |cell| cell.set(Some(app.upcast())));
}

fn init() {
    gtk::init().unwrap();
}

/// Returns the main application that was created by [`RelmApp::new`].
///
/// If no application exists yet, a new one with no application ID is created
/// and registered.
#[must_use]
/// Returns the main application, creating one if it doesn't exist yet.
pub fn main_application() -> gtk::Application {
    fn new_application() -> gtk::Application {
        gtk::Application::new(None, gtk::gio::ApplicationFlags::FLAGS_NONE)
    }

    MAIN_APPLICATION.with(|cell| {
        let app = cell.take().unwrap_or_else(new_application);
        cell.set(Some(app.clone()));
        app
    })
}

/// Spawns a future on the default main context of the current thread.
///
/// The returned handle can be used to await the result or to cancel the
/// future. This is useful for futures that must run on the GTK main thread.
pub fn spawn_local<F, Out>(func: F) -> gtk::glib::JoinHandle<Out>
where
    F: Future<Output = Out> + 'static,
    Out: 'static,
{
    gtk::glib::MainContext::ref_thread_default().spawn_local(func)
}

/// Spawns a future on the default main context of the current thread with a
/// custom priority.
///
/// See [`spawn_local`] for details.
pub fn spawn_local_with_priority<F, Out>(
    priority: gtk::glib::Priority,
    func: F,
) -> gtk::glib::JoinHandle<Out>
where
    F: Future<Output = Out> + 'static,
    Out: 'static,
{
    gtk::glib::MainContext::ref_thread_default().spawn_local_with_priority(priority, func)
}

static RUNTIME: Lazy<Runtime> = Lazy::new(|| {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .worker_threads(*RELM_THREADS.get_or_init(|| 1))
        .max_blocking_threads(*RELM_BLOCKING_THREADS.get_or_init(|| 512))
        .build()
        .unwrap()
});

/// Spawns a future on the internal multi-threaded tokio runtime.
///
/// The future must be `Send`, so it cannot access widgets or other types that
/// are not thread safe. Use [`spawn_local`] for futures that need access to
/// the main thread.
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
    F: Future + Send + 'static,
    F::Output: Send + 'static,
{
    RUNTIME.spawn(future)
}

/// Runs a blocking function on a separate thread of the internal runtime and
/// returns a handle to await the result.
///
/// This is useful for long-running operations like file or network I/O that
/// should not block the GTK main thread.
pub fn spawn_blocking<F, R>(func: F) -> JoinHandle<R>
where
    F: FnOnce() -> R + Send + 'static,
    R: Send + 'static,
{
    RUNTIME.spawn_blocking(func)
}

/// Sets global CSS styles with a custom priority.
///
/// The given style data is loaded into a new [`gtk::CssProvider`] that is
/// added to the default screen. See [`gtk::StyleContext::add_provider_for_screen`].
pub fn set_global_css_with_priority(style_data: &str, priority: u32) {
    let screen = gtk::gdk::Screen::default().unwrap();
    let provider = gtk::CssProvider::new();
    provider.load_from_data(style_data.as_bytes()).unwrap();

    gtk::StyleContext::add_provider_for_screen(&screen, &provider, priority);
}

/// Sets global CSS styles with the default application priority.
///
/// See [`set_global_css_with_priority`] for details.
pub fn set_global_css(style_data: &str) {
    set_global_css_with_priority(style_data, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
}

/// Reads CSS styles from a file and applies them with a custom priority.
///
/// Returns an error if the file could not be read.
pub fn set_global_css_from_file_with_priority<P: AsRef<std::path::Path>>(
    path: P,
    priority: u32,
) -> Result<(), std::io::Error> {
    std::fs::read_to_string(path)
        .map(|bytes| set_global_css_with_priority(&bytes, priority))
        .map_err(|err| {
            tracing::error!("Couldn't load global CSS from file: {}", err);
            err
        })
}

/// Reads CSS styles from a file and applies them with the default application
/// priority.
///
/// See [`set_global_css_from_file_with_priority`] for details.
pub fn set_global_css_from_file<P: AsRef<std::path::Path>>(path: P) -> Result<(), std::io::Error> {
    set_global_css_from_file_with_priority(path, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION)
}

/// Runs a GTK test body on the single thread that initialized GTK.
///
/// GTK3 may only be used from the thread that called `gtk::init`. Since tests
/// run on parallel threads by default, all GTK-related test bodies are
/// serialized on one worker thread via a single-threaded thread pool. Panics
/// from the test body are propagated to the calling test thread.
#[cfg(test)]
pub(crate) fn run_gtk_test<F>(f: F)
where
    F: FnOnce() + Send + 'static,
{
    static TEST_THREAD_WORKER: Lazy<gtk::glib::ThreadPool> = Lazy::new(|| {
        let pool = gtk::glib::ThreadPool::exclusive(1).unwrap();
        pool.push(|| {
            gtk::init().expect("failed to initialize gtk");
        })
        .expect("failed to schedule gtk initialization");
        pool
    });

    let (tx, rx) = std::sync::mpsc::channel();
    TEST_THREAD_WORKER
        .push(move || {
            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
            tx.send(result).unwrap();
        })
        .expect("failed to schedule test closure");
    if let Err(payload) = rx.recv().unwrap() {
        std::panic::resume_unwind(payload);
    }
}