From dff9265e358bc10e6505a71638f13f9c40b87f92 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 26 Jan 2022 11:30:58 +0100 Subject: [PATCH] refactor!(ccs): Improvements to design --- crates/component-system/README.md | 45 ++++++++----- .../examples/basic/components/app.rs | 65 +++++++++---------- .../examples/basic/components/info_button.rs | 56 ++++++++++------ .../component-system/examples/basic/main.rs | 2 +- crates/component-system/src/component.rs | 54 ++++++++------- crates/component-system/src/lib.rs | 16 +++-- crates/component-system/src/macros.rs | 47 +++++++------- 7 files changed, 157 insertions(+), 128 deletions(-) diff --git a/crates/component-system/README.md b/crates/component-system/README.md index 0bd304de..1b9577f5 100644 --- a/crates/component-system/README.md +++ b/crates/component-system/README.md @@ -2,7 +2,7 @@ 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. -## Defining a Component +## Using a Macro to Define a Component The simplest way to define a component is to use the `component!()` macro. @@ -13,14 +13,13 @@ pub enum MyCustomInputMessage { } component! { - // The `()` is the args parameter accepted by `init_view()` - // and `Component::register()`. - pub struct MyCustomModel(()) { + // The model stores the state of this component. + pub struct MyCustomModel { pub state: String, } - // The `gtk::Box` is the root widget returned in `init_view()`. - pub struct MyCustomWidgets(gtk::Box) { + // Widgets managed by the view are stored here. + pub struct MyCustomWidgets { description: gtk::Label, } @@ -30,24 +29,35 @@ component! { // The type of the output sender type Output = (); - // `self` is `MyCustomModel`, and must return `(MyCustomWidget, RootWidget)` - fn init_view(self, args, input, output) { + // Declares the root widget and how it should be constructed. + type Root = gtk::Box { ccs::view! { root = gtk::Box { set_orientation: gtk::Orientation::Vertical, - - append: description = >k::Label { - - } } } - (MyCustomWidgets { description }, root) + root + }; + + // Constructs the inner component's model and widgets, using the + // initial parameter given by `args`. + fn init(args: (), root, input, output) { + let description = gtk::Label::new(); + + root.append(&description); + + ComponentInner { + model: MyCustomModel { state: String::new() }, + widgets: MyCustomWidgets { description }, + input, + output + } } - // Where events are received, with `self` also being `MyCustomModel`, and - // `widgets` is `MyCustomInputMessage`. `event` is `MyCustonInputMessage`. - fn update(self, widgets, event, input, output) { + // Where events are received, with `component` is the `ComponentInner`, + // and `event` is a `MyCustomInputMessage` which was just received. + fn update(component, event) { match event { MyCustomInputMessage::Variant1 => { @@ -64,8 +74,7 @@ component! { Components can be created and have their output events forwarded: ```rs -let counter = InfoButton::default() - .register("Clicked 0 times".into(), "Click".into()) +let counter = InfoButton::init("Clicked 0 times".into(), "Click".into()) .forward(input.clone(), |event| match event { InfoButtonOutput::Clicked => AppEvent::Increment }); diff --git a/crates/component-system/examples/basic/components/app.rs b/crates/component-system/examples/basic/components/app.rs index 71b1b264..0b7b3138 100644 --- a/crates/component-system/examples/basic/components/app.rs +++ b/crates/component-system/examples/basic/components/app.rs @@ -14,12 +14,12 @@ pub enum AppEvent { component! { /// The model where component state is stored. #[derive(Default)] - pub struct App(gtk::Application) { + pub struct App { pub counter: usize, } /// Widgets that are initialized in the view. - pub struct AppWidgets(gtk::ApplicationWindow) { + pub struct AppWidgets { list: gtk::ListBox, destroyable: Option>, counter: Handle, @@ -27,13 +27,14 @@ component! { type Input = AppEvent; type Output = (); + type Root = gtk::ApplicationWindow { + Default::default() + }; - fn init_view(self, app, input, _output) { + fn init(app: gtk::Application, root, input, output) { let button_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Both); - // Create an `InfoButton` component. - let destroyable = InfoButton::default() - .register((String::new(), "Destroy".into(), button_group.clone())) + let destroyable = InfoButton::init((String::new(), "Destroy".into(), button_group.clone())) .forward(input.clone(), |event| match event { InfoButtonOutput::Clicked => AppEvent::Destroy, }); @@ -44,55 +45,53 @@ component! { )); // Create a counter component, too. - let counter = InfoButton::default() - .register(("Click me too".into(), "Click".into(), button_group)) + let counter = InfoButton::init(("Click me too".into(), "Click".into(), button_group)) .forward(input.clone(), |event| match event { InfoButtonOutput::Clicked => AppEvent::Increment, }); - // Construct the view for this component, attaching the component's widget. ccs::view! { - window = gtk::ApplicationWindow { - set_application: Some(&app), - set_child = Some(>k::Box) { - set_halign: gtk::Align::Center, - set_size_request: args!(400, -1), - set_orientation: gtk::Orientation::Vertical, + container = gtk::Box { + set_halign: gtk::Align::Center, + set_size_request: args!(400, -1), + set_orientation: gtk::Orientation::Vertical, - append: list = >k::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(), } } } - window.show(); + root.set_application(Some(&app)); + root.set_child(Some(&container)); - ( - AppWidgets { - list, - counter, - destroyable: Some(destroyable), - }, - window, - ) + root.show(); + + ComponentInner { + model: Self::default(), + widgets: AppWidgets { list, destroyable: Some(destroyable), counter }, + input, + output + } } /// Updates the view - fn update(self, widgets, event, _input, _output) { + fn update(component, event) { + let &mut ComponentInner { ref mut model, ref mut widgets, .. } = component; + match event { AppEvent::Increment => { - self.counter += 1; + model.counter += 1; widgets .counter .emit(InfoButtonInput::SetDescription(format!( "Clicked {} times", - self.counter + model.counter ))); } diff --git a/crates/component-system/examples/basic/components/info_button.rs b/crates/component-system/examples/basic/components/info_button.rs index 7c6282d4..2ae3f806 100644 --- a/crates/component-system/examples/basic/components/info_button.rs +++ b/crates/component-system/examples/basic/components/info_button.rs @@ -14,19 +14,17 @@ pub enum InfoButtonOutput { component! { #[derive(Default)] - pub struct InfoButton((String, String, gtk::SizeGroup)) { + pub struct InfoButton { } - pub struct InfoButtonWidgets(gtk::Box) { + pub struct InfoButtonWidgets { description: gtk::Label, } - type Input = InfoButtonInput; type Output = InfoButtonOutput; - fn init_view(self, args, _input, output) { - let (desc, button_label, sg) = args; + type Root = gtk::Box { ccs::view! { root = gtk::Box { set_orientation: gtk::Orientation::Horizontal, @@ -35,34 +33,52 @@ component! { set_margin_top: 8, set_margin_bottom: 8, set_spacing: 24, + } + } - append: description = >k::Label { - set_label: &desc, - set_halign: gtk::Align::Start, - set_hexpand: true, - set_valign: gtk::Align::Center, - set_ellipsize: gtk::pango::EllipsizeMode::End, - }, + root + }; - append: button = >k::Button { - set_label: &button_label, + fn init(args: (String, String, gtk::SizeGroup), root, input, output) { + let (desc, button_label, sg) = args; - connect_clicked(output) => move |_| { - let _ = output.send(InfoButtonOutput::Clicked); - } + ccs::view! { + description = gtk::Label { + set_label: &desc, + set_halign: gtk::Align::Start, + set_hexpand: true, + set_valign: gtk::Align::Center, + set_ellipsize: gtk::pango::EllipsizeMode::End, + } + } + + ccs::view! { + button = gtk::Button { + set_label: &button_label, + + connect_clicked(output) => move |_| { + let _ = output.send(InfoButtonOutput::Clicked); } } } + root.append(&description); + root.append(&button); + sg.add_widget(&button); - (InfoButtonWidgets { description }, root) + ComponentInner { + model: InfoButton {}, + widgets: InfoButtonWidgets { description }, + input, + output, + } } - fn update(self, widgets, message, _input, _output) { + fn update(component, message) { match message { InfoButtonInput::SetDescription(value) => { - widgets.description.set_text(&value); + component.widgets.description.set_text(&value); } } } diff --git a/crates/component-system/examples/basic/main.rs b/crates/component-system/examples/basic/main.rs index ca8582f7..efbe5328 100644 --- a/crates/component-system/examples/basic/main.rs +++ b/crates/component-system/examples/basic/main.rs @@ -12,6 +12,6 @@ fn main() { gtk4::builders::ApplicationBuilder::new() .application_id("org.pop.CosmicComponentExample") .cosmic_run(|app| { - App::default().register(app); + App::init(app); }); } diff --git a/crates/component-system/src/component.rs b/crates/component-system/src/component.rs index e6d16660..459d30e0 100644 --- a/crates/component-system/src/component.rs +++ b/crates/component-system/src/component.rs @@ -3,6 +3,14 @@ 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, +} + /// The basis of a COSMIC widget. /// /// A component takes care of constructing the UI of a widget, managing an event-loop @@ -10,7 +18,7 @@ use crate::*; /// the consumer of the component. pub trait Component: Sized + 'static { /// The arguments that are passed to the init_view method. - type InitialArgs; + type InitParams; /// The message type that the component accepts as inputs. type Input: 'static; @@ -19,27 +27,36 @@ pub trait Component: Sized + 'static { type Output: 'static; /// The widget that was constructed by the component. - type RootWidget: Clone + AsRef; + 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 register( - mut self, - args: Self::InitialArgs, - ) -> Registered { - let (mut sender, in_rx) = mpsc::unbounded_channel::(); - let (mut out_tx, output) = mpsc::unbounded_channel::(); + fn init(params: Self::InitParams) -> Registered { + let (sender, in_rx) = mpsc::unbounded_channel::(); + let (out_tx, output) = mpsc::unbounded_channel::(); - let (mut widgets, widget) = self.init_view(args, &mut sender, &mut out_tx); + let root = Self::init_root(); + + let mut component = Self::init_inner(params, &root, sender, out_tx); let handle = Handle { - widget, - sender: sender.clone(), + widget: root, + sender: component.input.clone(), }; let (inner_tx, mut inner_rx) = mpsc::unbounded_channel::>(); @@ -55,7 +72,7 @@ pub trait Component: Sized + 'static { while let Some(event) = inner_rx.recv().await { match event { InnerMessage::Message(event) => { - self.update(&mut widgets, event, &mut sender, &mut out_tx); + Self::update(&mut component, event); } InnerMessage::Drop => break, @@ -71,22 +88,11 @@ pub trait Component: Sized + 'static { } } - /// Creates the initial view and root widget. - fn init_view( - &mut self, - args: Self::InitialArgs, - input: &mut Sender, - output: &mut Sender, - ) -> (Self::Widgets, Self::RootWidget); - /// Handles input messages and enables the programmer to update the model and view. #[allow(unused_variables)] fn update( - &mut self, - widgets: &mut Self::Widgets, + component: &mut ComponentInner, message: Self::Input, - input: &mut Sender, - output: &mut Sender, ) { } } diff --git a/crates/component-system/src/lib.rs b/crates/component-system/src/lib.rs index ec46fb76..3385c6ce 100644 --- a/crates/component-system/src/lib.rs +++ b/crates/component-system/src/lib.rs @@ -10,7 +10,7 @@ use gtk4::prelude::*; use tokio::sync::mpsc; pub use self::app_runner::AppRunner; -pub use self::component::Component; +pub use self::component::{Component, ComponentInner}; pub use self::handle::{Handle, Registered}; pub use gtk4 as gtk; pub use relm4_macros::view; @@ -23,12 +23,14 @@ pub trait Widget { fn widget(&self) -> &W; } -/// Convenience function for `Component::register()`. -pub fn register( - model: C, - args: C::InitialArgs, -) -> Registered { - model.register(args) +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. diff --git a/crates/component-system/src/macros.rs b/crates/component-system/src/macros.rs index 39d9aec0..52f23069 100644 --- a/crates/component-system/src/macros.rs +++ b/crates/component-system/src/macros.rs @@ -5,14 +5,14 @@ macro_rules! component { ( $(#[$attr:meta])* - $mvis:vis struct $model:ident ($args:ty) { + $mvis:vis struct $model:ident { $( $mpvis:vis $property:ident : $type:ty, )* } $(#[$attr2:meta])* - $wvis:vis struct $widgets_:ident($root:ty) { + $wvis:vis struct $widgets_:ident { $( $wpvis:vis $widgets_property:ident : $widgets_type:ty, )* @@ -20,22 +20,20 @@ macro_rules! component { type Input = $input:ty; type Output = $output:ty; + type Root = $root:ty $init_root:block; - $(#[$attr3:meta])* - fn init_view( - $selfv:ident, - $argsv:ident, + $(#[$attr4:meta])* + fn init( + $argsv:ident: $args:ty, + $rootv:ident, $inputv:ident, $outputv:ident ) $init_view:block - $(#[$attr4:meta])* + $(#[$attr5:meta])* fn update( - $selfv2:ident, - $widgetsv:ident, - $messagev:ident, - $inputv2:ident, - $outputv2:ident + $componentv:ident, + $messagev:ident ) $update:block ) => { $(#[$attr])* @@ -49,27 +47,26 @@ macro_rules! component { } impl Component for $model { - type InitialArgs = $args; + type InitParams = $args; type Input = $input; type Output = $output; - type RootWidget = $root; + type Root = $root; type Widgets = $widgets_; - $(#[$attr3])* - fn init_view( - &mut $selfv2, - $argsv: Self::InitialArgs, - $inputv: &mut Sender, - $outputv: &mut Sender - ) -> (Self::Widgets, Self::RootWidget) $init_view + 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 $selfv2, - $widgetsv: &mut Self::Widgets, + $componentv: &mut ComponentInner, $messagev: Self::Input, - $inputv2: &mut Sender, - $outputv2: &mut Sender ) $update } }