diff --git a/crates/component-system/Cargo.toml b/crates/component-system/Cargo.toml index 3ecfd18b..8a96dded 100644 --- a/crates/component-system/Cargo.toml +++ b/crates/component-system/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" license = "MPL-2.0" [dependencies] +async-trait = "0.1.52" gtk4 = "0.4.4" relm4-macros = "0.4.1" -tokio = { version = "1.15.0", features = ["sync"]} \ No newline at end of file +tokio = { version = "1.15.0", features = ["full"]} diff --git a/crates/component-system/src/component.rs b/crates/component-system/src/component/elmless.rs similarity index 88% rename from crates/component-system/src/component.rs rename to crates/component-system/src/component/elmless.rs index 459d30e0..300f0579 100644 --- a/crates/component-system/src/component.rs +++ b/crates/component-system/src/component/elmless.rs @@ -1,21 +1,14 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use crate::*; - -/// The pieces that make up the state of the component. -pub struct ComponentInner { - pub model: Model, - pub widgets: Widgets, - pub input: Sender, - pub output: Sender, -} +use super::*; /// The basis of a COSMIC widget. /// /// A component takes care of constructing the UI of a widget, managing an event-loop /// which handles signals from within the widget, and supports forwarding messages to /// the consumer of the component. +#[async_trait::async_trait] pub trait Component: Sized + 'static { /// The arguments that are passed to the init_view method. type InitParams; @@ -93,12 +86,5 @@ pub trait Component: Sized + 'static { fn update( component: &mut ComponentInner, message: Self::Input, - ) { - } -} - -/// Used to drop the component's event loop when the managed widget is destroyed. -enum InnerMessage { - Drop, - Message(T), + ); } diff --git a/crates/component-system/src/component/elmlike.rs b/crates/component-system/src/component/elmlike.rs new file mode 100644 index 00000000..d6aa44a3 --- /dev/null +++ b/crates/component-system/src/component/elmlike.rs @@ -0,0 +1,112 @@ +use super::*; + +#[async_trait::async_trait] +/// Elm-style variant of a Component with view updates separated from input updates +pub trait ElmComponent: Sized + 'static { + /// The arguments that are passed to the init_view method. + type InitParams; + + /// The message type that the component accepts as inputs. + type Input: 'static + Send; + + /// The message type that the component provides as outputs. + type Output: 'static; + + /// Internal commands to perform + type Command: 'static + Send; + + /// The widget that was constructed by the component. + type Root: Clone + AsRef; + + /// The type that's used for storing widgets created for this component. + type Widgets: 'static; + + /// Initializes the root widget + fn init_root() -> Self::Root; + + fn init_inner( + params: Self::InitParams, + root_widget: &Self::Root, + input: Sender, + output: Sender, + ) -> ComponentInner; + + /// Initializes the component and attaches it to the default local executor. + /// + /// Spawns an event loop on `glib::MainContext::default()`, which exists + /// for as long as the root widget remains alive. + fn init(params: Self::InitParams) -> Registered { + let (sender, in_rx) = mpsc::unbounded_channel::(); + let (out_tx, out_rx) = mpsc::unbounded_channel::(); + + let root = Self::init_root(); + + let ComponentInner { + mut model, + mut widgets, + mut input, + mut output, + } = Self::init_inner(params, &root, sender, out_tx); + + let handle = Handle { + widget: root, + sender: input.clone(), + }; + + let (inner_tx, mut inner_rx) = mpsc::unbounded_channel::>(); + + handle.widget.as_ref().connect_destroy({ + let sender = inner_tx.clone(); + move |_| { + let _ = sender.send(InnerMessage::Drop); + } + }); + + spawn_local(async move { + while let Some(event) = inner_rx.recv().await { + match event { + InnerMessage::Message(event) => { + if let Some(command) = model.update(event, &mut input, &mut output) { + let input = input.clone(); + tokio::spawn(async move { + if let Some(event) = Self::command(command).await { + let _ = input.send(event); + } + }); + } + + model.update_view(&mut widgets, &mut input, &mut output); + } + + InnerMessage::Drop => break, + } + } + }); + + forward(in_rx, inner_tx, InnerMessage::Message); + + Registered { + handle, + receiver: out_rx, + } + } + + /// Handles input messages and enables the programmer to update the model and view. + fn update( + &mut self, + message: Self::Input, + input: &mut Sender, + output: &mut Sender, + ) -> Option; + + /// Update the UI + fn update_view( + &mut self, + widgets: &mut Self::Widgets, + input: &mut Sender, + output: &mut Sender, + ); + + /// A command to perform in a background thread. + async fn command(message: Self::Command) -> Option; +} diff --git a/crates/component-system/src/component/mod.rs b/crates/component-system/src/component/mod.rs new file mode 100644 index 00000000..1e8a2cb9 --- /dev/null +++ b/crates/component-system/src/component/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::*; + +mod elmless; +mod elmlike; + +pub use self::elmless::*; +pub use self::elmlike::*; + +/// The pieces that make up the state of the component. +pub struct ComponentInner { + pub model: Model, + pub widgets: Widgets, + pub input: Sender, + pub output: Sender, +} + +/// Used to drop the component's event loop when the managed widget is destroyed. +enum InnerMessage { + Drop, + Message(T), +} diff --git a/crates/component-system/src/lib.rs b/crates/component-system/src/lib.rs index 3385c6ce..a879ac34 100644 --- a/crates/component-system/src/lib.rs +++ b/crates/component-system/src/lib.rs @@ -10,8 +10,9 @@ use gtk4::prelude::*; use tokio::sync::mpsc; pub use self::app_runner::AppRunner; -pub use self::component::{Component, ComponentInner}; +pub use self::component::{Component, ComponentInner, ElmComponent}; pub use self::handle::{Handle, Registered}; +pub use async_trait::async_trait; pub use gtk4 as gtk; pub use relm4_macros::view; diff --git a/crates/component-system/src/macros.rs b/crates/component-system/src/macros.rs index 52f23069..aae2c7b7 100644 --- a/crates/component-system/src/macros.rs +++ b/crates/component-system/src/macros.rs @@ -46,6 +46,7 @@ macro_rules! component { $($wpvis $widgets_property: $widgets_type,)* } + #[cosmic_component_system::async_trait] impl Component for $model { type InitParams = $args; type Input = $input; @@ -71,3 +72,103 @@ macro_rules! component { } } } + +#[macro_export] +macro_rules! elm_component { + ( + $(#[$attr:meta])* + $mvis:vis struct $model:ident { + $( + $mpvis:vis $property:ident : $type:ty, + )* + } + + $(#[$attr2:meta])* + $wvis:vis struct $widgets_:ident { + $( + $wpvis:vis $widgets_property:ident : $widgets_type:ty, + )* + } + + type Input = $input:ty; + type Output = $output:ty; + type Command = $cmd:ty; + type Root = $root:ty $init_root:block; + + $(#[$attr4:meta])* + fn init( + $argsv:ident: $args:ty, + $rootv:ident, + $inputv:ident, + $outputv:ident + ) $init_view:block + + $(#[$attr5:meta])* + fn update( + $modelv:ident, + $messagev:ident, + $inputv2:ident, + $outputv2:ident + ) $update:block + + $(#[$attr6:meta])* + fn update_view( + $modelv2:ident, + $widgetsv:ident, + $inputv3:ident, + $outputv3:ident + ) $update_view:block + + $(#[$attr7:meta])* + async fn command($cmdv:ident) $command_block:block + ) => { + $(#[$attr])* + $mvis struct $model { + $($mpvis $property: $type,)* + } + + $(#[$attr2])* + $wvis struct $widgets_ { + $($wpvis $widgets_property: $widgets_type,)* + } + + #[cosmic_component_system::async_trait] + impl ElmComponent for $model { + type InitParams = $args; + type Input = $input; + type Output = $output; + type Command = $cmd; + type Root = $root; + type Widgets = $widgets_; + + fn init_root() -> Self::Root $init_root + + $(#[$attr4])* + fn init_inner( + $argsv: Self::InitParams, + $rootv: &Self::Root, + $inputv: Sender, + $outputv: Sender + ) -> ComponentInner $init_view + + $(#[$attr5])* + fn update( + &mut $modelv, + $messagev: Self::Input, + $inputv2: &mut Sender, + $outputv2: &mut Sender, + ) -> Option $update + + $(#[$attr6])* + fn update_view( + &mut $modelv2, + $widgetsv: &mut Self::Widgets, + $inputv3: &mut Sender, + $outputv3: &mut Sender, + ) $update_view + + $(#[$attr7])* + async fn command($cmdv: Self::Command) -> Option $command_block + } + } +}