diff --git a/crates/component-system/Cargo.toml b/crates/component-system/Cargo.toml index 8a96dded..c00daa15 100644 --- a/crates/component-system/Cargo.toml +++ b/crates/component-system/Cargo.toml @@ -4,8 +4,14 @@ version = "0.1.0" 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 = ["full"]} +[dependencies.tokio] +version = "1.15" +features = ["rt", "rt-multi-thread", "sync"] + +[dependencies.relm4] +git = "https://github.com/AaronErhardt/relm4" +branch = "new-approach" + +[dependencies.relm4-macros] +git = "https://github.com/AaronErhardt/relm4" +branch = "new-approach" \ No newline at end of file diff --git a/crates/component-system/README.md b/crates/component-system/README.md index 1b9577f5..5b676fd6 100644 --- a/crates/component-system/README.md +++ b/crates/component-system/README.md @@ -1,6 +1,6 @@ # COSMIC Component System -This library is a GTK4 GUI framework inspired by [Relm](https://github.com/antoyo/relm), which is inspired by [Elm](https://guide.elm-lang.org/architecture/). The philosophy for this framework is to isolate custom widgets into reusable components. You start with a custom `Model` type that implements `Component`, which is used to register a component with an optional argument. On registration, the model is used to construct the view and its widgets in the `init_view()` function. An event-handler is also spawned to handle events from both the component and any component emitting events to it. Those events are received and handled in the `update()` function. Both the `init_view()` and `update()` methods also have access to an outbound sender, which the caller can forward and consume however desired. See the examples directory for a demonstration of how to create a component. +This library is a prototyping area for a rewrite of Relm4's component system. ## Using a Macro to Define a Component @@ -12,6 +12,10 @@ pub enum MyCustomInputMessage { Variant2, } +pub enum MyCustomCommand { + Action +} + component! { // The model stores the state of this component. pub struct MyCustomModel { @@ -47,17 +51,14 @@ component! { root.append(&description); - ComponentInner { + Fuselage { model: MyCustomModel { state: String::new() }, widgets: MyCustomWidgets { description }, - input, - output } } - // Where events are received, with `component` is the `ComponentInner`, - // and `event` is a `MyCustomInputMessage` which was just received. - fn update(component, event) { + // Updates the model and view. `self` is the model. + fn update(&mut self, widgets, event, input, output) { match event { MyCustomInputMessage::Variant1 => { @@ -67,6 +68,12 @@ component! { } } + + Some(MyCustomCommand::Action) + } + + async fn command(command: MyCustomCommand, input) { + } } ``` @@ -74,7 +81,8 @@ component! { Components can be created and have their output events forwarded: ```rs -let counter = InfoButton::init("Clicked 0 times".into(), "Click".into()) +let counter = InfoButton::init() + .launch("Clicked 0 times".into(), "Click".into()) .forward(input.clone(), |event| match event { InfoButtonOutput::Clicked => AppEvent::Increment }); @@ -85,18 +93,5 @@ The handle returned can be used to emit inputs to it, and to get the root widget ```rs counter.emit(InfoButtonInput::SetDescription(format!("Clicked {} times", count))); -box.append(counter.widget()); -``` - - -## See Also - -[Relm4](https://github.com/AaronErhardt/relm4) uses a similar approach, but closely follows the Elm model. This library was created as an alternative approach that makes developing reusable components with forwardable events simpler. - -## License - -Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/). - -### Contribution - -Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the Mozilla Public License 2.0 (MPL-2.0). \ No newline at end of file +box.append(&counter.widget); +``` \ No newline at end of file diff --git a/crates/component-system/examples/basic/components/app.rs b/crates/component-system/examples/basic/components/app.rs index 0b7b3138..c9c3e2a6 100644 --- a/crates/component-system/examples/basic/components/app.rs +++ b/crates/component-system/examples/basic/components/app.rs @@ -21,20 +21,29 @@ component! { /// Widgets that are initialized in the view. pub struct AppWidgets { list: gtk::ListBox, - destroyable: Option>, - counter: Handle, + destroyable: Option>, + counter: Controller, } type Input = AppEvent; type Output = (); - type Root = gtk::ApplicationWindow { - Default::default() + type Root = gtk::Box { + ccs::view! { + root = gtk::Box { + set_halign: gtk::Align::Center, + set_size_request: args!(400, -1), + set_orientation: gtk::Orientation::Vertical, + } + } + + root }; - fn init(app: gtk::Application, root, input, output) { + fn init(window: gtk::ApplicationWindow, root, input, _output) { let button_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Both); - let destroyable = InfoButton::init((String::new(), "Destroy".into(), button_group.clone())) + let destroyable = InfoButton::init() + .launch_stateful((String::new(), "Destroy".into(), button_group.clone())) .forward(input.clone(), |event| match event { InfoButtonOutput::Clicked => AppEvent::Destroy, }); @@ -45,64 +54,59 @@ component! { )); // Create a counter component, too. - let counter = InfoButton::init(("Click me too".into(), "Click".into(), button_group)) + let counter = InfoButton::init() + .launch_stateful(("Click me too".into(), "Click".into(), button_group)) .forward(input.clone(), |event| match event { InfoButtonOutput::Clicked => AppEvent::Increment, }); ccs::view! { - container = gtk::Box { - set_halign: gtk::Align::Center, - set_size_request: args!(400, -1), - set_orientation: gtk::Orientation::Vertical, + list = gtk::ListBox { + set_selection_mode: gtk::SelectionMode::None, + set_hexpand: true, - append: list = >k::ListBox { - set_selection_mode: gtk::SelectionMode::None, - set_hexpand: true, - - append: destroyable.widget(), - append: counter.widget(), - } + append: &destroyable.widget, + append: &counter.widget, } } - root.set_application(Some(&app)); - root.set_child(Some(&container)); + root.append(&list); + window.set_child(Some(root)); - root.show(); - - ComponentInner { + Fuselage { model: Self::default(), widgets: AppWidgets { list, destroyable: Some(destroyable), counter }, - input, - output } } /// Updates the view - fn update(component, event) { - let &mut ComponentInner { ref mut model, ref mut widgets, .. } = component; - + fn update(&mut self, widgets, event, _input, _output) { match event { AppEvent::Increment => { - model.counter += 1; + self.counter += 1; widgets .counter .emit(InfoButtonInput::SetDescription(format!( "Clicked {} times", - model.counter + self.counter ))); } AppEvent::Destroy => { // Components are kept alive by their root GTK widget. if let Some(handle) = widgets.destroyable.take() { - if let Some(parent) = handle.widget().parent() { + if let Some(parent) = handle.widget.parent() { widgets.list.remove(&parent); } } } } + + None + } + + async fn command(_message: (), _input) { + } } diff --git a/crates/component-system/examples/basic/components/info_button.rs b/crates/component-system/examples/basic/components/info_button.rs index 2ae3f806..2da02207 100644 --- a/crates/component-system/examples/basic/components/info_button.rs +++ b/crates/component-system/examples/basic/components/info_button.rs @@ -21,6 +21,7 @@ component! { pub struct InfoButtonWidgets { description: gtk::Label, } + type Input = InfoButtonInput; type Output = InfoButtonOutput; @@ -39,7 +40,7 @@ component! { root }; - fn init(args: (String, String, gtk::SizeGroup), root, input, output) { + fn init(args: (String, String, gtk::SizeGroup), root, _input, output) { let (desc, button_label, sg) = args; ccs::view! { @@ -67,19 +68,23 @@ component! { sg.add_widget(&button); - ComponentInner { + Fuselage { model: InfoButton {}, widgets: InfoButtonWidgets { description }, - input, - output, } } - fn update(component, message) { + fn update(&mut self, widgets, message, _input, _output) { match message { InfoButtonInput::SetDescription(value) => { - component.widgets.description.set_text(&value); + widgets.description.set_text(&value); } } + + None + } + + async fn command(_message: (), _input) { + } } diff --git a/crates/component-system/examples/basic/main.rs b/crates/component-system/examples/basic/main.rs index efbe5328..f941f702 100644 --- a/crates/component-system/examples/basic/main.rs +++ b/crates/component-system/examples/basic/main.rs @@ -6,12 +6,12 @@ extern crate cosmic_component_system as ccs; mod components; use self::components::App; -use ccs::{AppRunner, Component}; +use ccs::*; fn main() { - gtk4::builders::ApplicationBuilder::new() + gtk::builders::ApplicationBuilder::new() .application_id("org.pop.CosmicComponentExample") - .cosmic_run(|app| { - App::init(app); + .launch(|_app, window| { + App::init().launch_stateful(window); }); } diff --git a/crates/component-system/src/app_runner.rs b/crates/component-system/src/app_runner.rs deleted file mode 100644 index 7b75a2ad..00000000 --- a/crates/component-system/src/app_runner.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use gtk4::prelude::{ApplicationExt, ApplicationExtManual}; - -pub trait AppRunner { - /// Convenience method to activates and run a GTK4 application. - fn cosmic_run(self, func: F); -} - -impl AppRunner for gtk4::Application { - fn cosmic_run(self, func: F) { - self.connect_activate(move |app| func(app.clone())); - self.run(); - } -} - -impl AppRunner for gtk4::builders::ApplicationBuilder { - fn cosmic_run(self, func: F) { - self.build().cosmic_run(func); - } -} diff --git a/crates/component-system/src/component/elmless.rs b/crates/component-system/src/component/elmless.rs deleted file mode 100644 index 300f0579..00000000 --- a/crates/component-system/src/component/elmless.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -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; - - /// The message type that the component accepts as inputs. - type Input: 'static; - - /// The message type that the component provides as outputs. - type Output: 'static; - - /// 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, output) = mpsc::unbounded_channel::(); - - let root = Self::init_root(); - - let mut component = Self::init_inner(params, &root, sender, out_tx); - - let handle = Handle { - widget: root, - sender: component.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) => { - Self::update(&mut component, event); - } - - InnerMessage::Drop => break, - } - } - }); - - forward(in_rx, inner_tx, InnerMessage::Message); - - Registered { - handle, - receiver: output, - } - } - - /// Handles input messages and enables the programmer to update the model and view. - #[allow(unused_variables)] - fn update( - component: &mut ComponentInner, - message: Self::Input, - ); -} diff --git a/crates/component-system/src/component/elmlike.rs b/crates/component-system/src/component/elmlike.rs deleted file mode 100644 index d6aa44a3..00000000 --- a/crates/component-system/src/component/elmlike.rs +++ /dev/null @@ -1,112 +0,0 @@ -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 deleted file mode 100644 index 1e8a2cb9..00000000 --- a/crates/component-system/src/component/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -// 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/handle.rs b/crates/component-system/src/handle.rs deleted file mode 100644 index 832313a3..00000000 --- a/crates/component-system/src/handle.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::*; - -/// Handle to an active widget component in the system. -pub struct Handle { - /// The widget that this component manages. - pub(crate) widget: W, - - /// Used for emitting events to the component. - pub(crate) sender: Sender, -} - -impl Widget for Handle { - fn widget(&self) -> &W { - &self.widget - } -} - -impl Handle { - pub fn emit(&self, event: I) { - let _ = self.sender.send(event); - } -} - -/// A newly-registered component which supports destructuring the handle -/// by forwarding or ignoring outputs from the component. -pub struct Registered, I, O> { - /// Handle to the component that was registered. - pub handle: Handle, - - /// The outputs being received by the component. - pub receiver: Receiver, -} - -impl, I: 'static, O: 'static> Registered { - /// Forwards output events to the designated sender. - pub fn forward X) + 'static>( - self, - sender: Sender, - transform: F, - ) -> Handle { - let Registered { handle, receiver } = self; - forward(receiver, sender, transform); - handle - } - - pub fn handle(self, mut func: F) -> Handle { - let Registered { - handle, - mut receiver, - } = self; - - spawn_local(async move { - while let Some(event) = receiver.recv().await { - func(event); - } - }); - - handle - } - - /// Ignore outputs from the component and take the handle. - pub fn ignore(self) -> Handle { - self.handle - } -} diff --git a/crates/component-system/src/lib.rs b/crates/component-system/src/lib.rs index a879ac34..7e96613b 100644 --- a/crates/component-system/src/lib.rs +++ b/crates/component-system/src/lib.rs @@ -1,55 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -mod app_runner; -mod component; -mod handle; mod macros; -use gtk4::prelude::*; -use tokio::sync::mpsc; - -pub use self::app_runner::AppRunner; -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::*; pub use relm4_macros::view; - -pub type Sender = mpsc::UnboundedSender; -pub type Receiver = mpsc::UnboundedReceiver; - -/// Provides a convenience function for getting a widget out of a type. -pub trait Widget { - fn widget(&self) -> &W; -} - -pub trait CosmicWidgetExt: Widget -where - W: AsRef, -{ - fn attach_size_group(&self, sg: >k::SizeGroup) -> &Self { - sg.add_widget(self.widget().as_ref()); - self - } -} - -/// Convenience function for forwarding events from a receiver to different sender. -pub fn forward O) + 'static>( - mut receiver: Receiver, - sender: Sender, - transformer: F, -) { - spawn_local(async move { - while let Some(event) = receiver.recv().await { - if sender.send(transformer(event)).is_err() { - break; - } - } - }) -} - -/// Convenience function for spawning tasks on the local executor -pub fn spawn_local + 'static>(func: F) { - gtk4::glib::MainContext::default().spawn_local(func); -} diff --git a/crates/component-system/src/macros.rs b/crates/component-system/src/macros.rs index aae2c7b7..d1f6accb 100644 --- a/crates/component-system/src/macros.rs +++ b/crates/component-system/src/macros.rs @@ -32,9 +32,15 @@ macro_rules! component { $(#[$attr5:meta])* fn update( - $componentv:ident, - $messagev:ident + &mut $modelv:ident, + $widgetsv:ident, + $messagev:ident, + $inputv2:ident, + $outputv2:ident ) $update:block + + $(#[$attr6:meta])* + async fn command($cmdv:ident: $cmd:ty, $inputv3:ident) { $($cmd_expr:expr)* } ) => { $(#[$attr])* $mvis struct $model { @@ -46,29 +52,37 @@ macro_rules! component { $($wpvis $widgets_property: $widgets_type,)* } - #[cosmic_component_system::async_trait] - impl Component for $model { - type InitParams = $args; + impl StatefulComponent for $model { + type Payload = $args; type Input = $input; type Output = $output; type Root = $root; type Widgets = $widgets_; + type Command = $cmd; fn init_root() -> Self::Root $init_root $(#[$attr4])* - fn init_inner( - $argsv: Self::InitParams, + fn dock( + $argsv: Self::Payload, $rootv: &Self::Root, - $inputv: Sender, - $outputv: Sender - ) -> ComponentInner $init_view + $inputv: &mut Sender, + $outputv: &mut Sender + ) -> Fuselage $init_view $(#[$attr5])* fn update( - $componentv: &mut ComponentInner, + &mut $modelv, + $widgetsv: &mut Self::Widgets, $messagev: Self::Input, - ) $update + $inputv2: &mut Sender, + $outputv2: &mut Sender + ) -> Option $update + + $(#[$attr6])* + fn command($cmdv: $cmd, $inputv3: Sender) -> CommandFuture { + Box::pin(async move { $($cmd_expr)* } ) + } } } } @@ -92,7 +106,6 @@ macro_rules! elm_component { type Input = $input:ty; type Output = $output:ty; - type Command = $cmd:ty; type Root = $root:ty $init_root:block; $(#[$attr4:meta])* @@ -105,7 +118,7 @@ macro_rules! elm_component { $(#[$attr5:meta])* fn update( - $modelv:ident, + &mut $modelv:ident, $messagev:ident, $inputv2:ident, $outputv2:ident @@ -113,14 +126,14 @@ macro_rules! elm_component { $(#[$attr6:meta])* fn update_view( - $modelv2:ident, + &$modelv2:ident, $widgetsv:ident, $inputv3:ident, $outputv3:ident ) $update_view:block $(#[$attr7:meta])* - async fn command($cmdv:ident) $command_block:block + async fn command($cmdv:ident: $cmd:ty, $inputv4:ident) { $($cmd_expr:expr)* } ) => { $(#[$attr])* $mvis struct $model { @@ -132,43 +145,44 @@ macro_rules! elm_component { $($wpvis $widgets_property: $widgets_type,)* } - #[cosmic_component_system::async_trait] - impl ElmComponent for $model { - type InitParams = $args; + impl StatefulComponent for $model { + type Payload = $args; type Input = $input; type Output = $output; - type Command = $cmd; type Root = $root; type Widgets = $widgets_; + type Command = $cmd; fn init_root() -> Self::Root $init_root $(#[$attr4])* - fn init_inner( - $argsv: Self::InitParams, + fn dock( + $argsv: Self::Payload, $rootv: &Self::Root, - $inputv: Sender, - $outputv: Sender - ) -> ComponentInner $init_view + $inputv: &mut Sender, + $outputv: &mut Sender + ) -> Fuselage $init_view $(#[$attr5])* fn update( &mut $modelv, $messagev: Self::Input, $inputv2: &mut Sender, - $outputv2: &mut Sender, + $outputv2: &mut Sender ) -> Option $update $(#[$attr6])* fn update_view( - &mut $modelv2, + &$modelv2, $widgetsv: &mut Self::Widgets, $inputv3: &mut Sender, - $outputv3: &mut Sender, + $outputsv3: &mut Sender ) $update_view $(#[$attr7])* - async fn command($cmdv: Self::Command) -> Option $command_block + fn command($cmdv: $cmd, $inputv4: Sender) -> CommandFuture { + Box::pin(async move { $($cmd_expr)* } ) + } } } }