refactor!(ccs): Improvements to design

This commit is contained in:
Michael Aaron Murphy 2022-01-26 11:30:58 +01:00
parent 148ffc6dae
commit dff9265e35
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
7 changed files with 157 additions and 128 deletions

View file

@ -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. 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. The simplest way to define a component is to use the `component!()` macro.
@ -13,14 +13,13 @@ pub enum MyCustomInputMessage {
} }
component! { component! {
// The `()` is the args parameter accepted by `init_view()` // The model stores the state of this component.
// and `Component::register()`. pub struct MyCustomModel {
pub struct MyCustomModel(()) {
pub state: String, pub state: String,
} }
// The `gtk::Box` is the root widget returned in `init_view()`. // Widgets managed by the view are stored here.
pub struct MyCustomWidgets(gtk::Box) { pub struct MyCustomWidgets {
description: gtk::Label, description: gtk::Label,
} }
@ -30,24 +29,35 @@ component! {
// The type of the output sender // The type of the output sender
type Output = (); type Output = ();
// `self` is `MyCustomModel`, and must return `(MyCustomWidget, RootWidget)` // Declares the root widget and how it should be constructed.
fn init_view(self, args, input, output) { type Root = gtk::Box {
ccs::view! { ccs::view! {
root = gtk::Box { root = gtk::Box {
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Vertical,
append: description = &gtk::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 // Where events are received, with `component` is the `ComponentInner`,
// `widgets` is `MyCustomInputMessage`. `event` is `MyCustonInputMessage`. // and `event` is a `MyCustomInputMessage` which was just received.
fn update(self, widgets, event, input, output) { fn update(component, event) {
match event { match event {
MyCustomInputMessage::Variant1 => { MyCustomInputMessage::Variant1 => {
@ -64,8 +74,7 @@ component! {
Components can be created and have their output events forwarded: Components can be created and have their output events forwarded:
```rs ```rs
let counter = InfoButton::default() let counter = InfoButton::init("Clicked 0 times".into(), "Click".into())
.register("Clicked 0 times".into(), "Click".into())
.forward(input.clone(), |event| match event { .forward(input.clone(), |event| match event {
InfoButtonOutput::Clicked => AppEvent::Increment InfoButtonOutput::Clicked => AppEvent::Increment
}); });

View file

@ -14,12 +14,12 @@ pub enum AppEvent {
component! { component! {
/// The model where component state is stored. /// The model where component state is stored.
#[derive(Default)] #[derive(Default)]
pub struct App(gtk::Application) { pub struct App {
pub counter: usize, pub counter: usize,
} }
/// Widgets that are initialized in the view. /// Widgets that are initialized in the view.
pub struct AppWidgets(gtk::ApplicationWindow) { pub struct AppWidgets {
list: gtk::ListBox, list: gtk::ListBox,
destroyable: Option<Handle<gtk::Box, InfoButtonInput>>, destroyable: Option<Handle<gtk::Box, InfoButtonInput>>,
counter: Handle<gtk::Box, InfoButtonInput>, counter: Handle<gtk::Box, InfoButtonInput>,
@ -27,13 +27,14 @@ component! {
type Input = AppEvent; type Input = AppEvent;
type Output = (); 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); let button_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Both);
// Create an `InfoButton` component. let destroyable = InfoButton::init((String::new(), "Destroy".into(), button_group.clone()))
let destroyable = InfoButton::default()
.register((String::new(), "Destroy".into(), button_group.clone()))
.forward(input.clone(), |event| match event { .forward(input.clone(), |event| match event {
InfoButtonOutput::Clicked => AppEvent::Destroy, InfoButtonOutput::Clicked => AppEvent::Destroy,
}); });
@ -44,17 +45,13 @@ component! {
)); ));
// Create a counter component, too. // Create a counter component, too.
let counter = InfoButton::default() let counter = InfoButton::init(("Click me too".into(), "Click".into(), button_group))
.register(("Click me too".into(), "Click".into(), button_group))
.forward(input.clone(), |event| match event { .forward(input.clone(), |event| match event {
InfoButtonOutput::Clicked => AppEvent::Increment, InfoButtonOutput::Clicked => AppEvent::Increment,
}); });
// Construct the view for this component, attaching the component's widget.
ccs::view! { ccs::view! {
window = gtk::ApplicationWindow { container = gtk::Box {
set_application: Some(&app),
set_child = Some(&gtk::Box) {
set_halign: gtk::Align::Center, set_halign: gtk::Align::Center,
set_size_request: args!(400, -1), set_size_request: args!(400, -1),
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Vertical,
@ -65,34 +62,36 @@ component! {
append: destroyable.widget(), append: destroyable.widget(),
append: counter.widget(), append: counter.widget(),
},
} }
} }
} }
window.show(); root.set_application(Some(&app));
root.set_child(Some(&container));
( root.show();
AppWidgets {
list, ComponentInner {
counter, model: Self::default(),
destroyable: Some(destroyable), widgets: AppWidgets { list, destroyable: Some(destroyable), counter },
}, input,
window, output
) }
} }
/// Updates the view /// 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 { match event {
AppEvent::Increment => { AppEvent::Increment => {
self.counter += 1; model.counter += 1;
widgets widgets
.counter .counter
.emit(InfoButtonInput::SetDescription(format!( .emit(InfoButtonInput::SetDescription(format!(
"Clicked {} times", "Clicked {} times",
self.counter model.counter
))); )));
} }

View file

@ -14,19 +14,17 @@ pub enum InfoButtonOutput {
component! { component! {
#[derive(Default)] #[derive(Default)]
pub struct InfoButton((String, String, gtk::SizeGroup)) { pub struct InfoButton {
} }
pub struct InfoButtonWidgets(gtk::Box) { pub struct InfoButtonWidgets {
description: gtk::Label, description: gtk::Label,
} }
type Input = InfoButtonInput; type Input = InfoButtonInput;
type Output = InfoButtonOutput; type Output = InfoButtonOutput;
fn init_view(self, args, _input, output) { type Root = gtk::Box {
let (desc, button_label, sg) = args;
ccs::view! { ccs::view! {
root = gtk::Box { root = gtk::Box {
set_orientation: gtk::Orientation::Horizontal, set_orientation: gtk::Orientation::Horizontal,
@ -35,16 +33,27 @@ component! {
set_margin_top: 8, set_margin_top: 8,
set_margin_bottom: 8, set_margin_bottom: 8,
set_spacing: 24, set_spacing: 24,
}
}
append: description = &gtk::Label { root
};
fn init(args: (String, String, gtk::SizeGroup), root, input, output) {
let (desc, button_label, sg) = args;
ccs::view! {
description = gtk::Label {
set_label: &desc, set_label: &desc,
set_halign: gtk::Align::Start, set_halign: gtk::Align::Start,
set_hexpand: true, set_hexpand: true,
set_valign: gtk::Align::Center, set_valign: gtk::Align::Center,
set_ellipsize: gtk::pango::EllipsizeMode::End, set_ellipsize: gtk::pango::EllipsizeMode::End,
}, }
}
append: button = &gtk::Button { ccs::view! {
button = gtk::Button {
set_label: &button_label, set_label: &button_label,
connect_clicked(output) => move |_| { connect_clicked(output) => move |_| {
@ -52,17 +61,24 @@ component! {
} }
} }
} }
}
root.append(&description);
root.append(&button);
sg.add_widget(&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 { match message {
InfoButtonInput::SetDescription(value) => { InfoButtonInput::SetDescription(value) => {
widgets.description.set_text(&value); component.widgets.description.set_text(&value);
} }
} }
} }

View file

@ -12,6 +12,6 @@ fn main() {
gtk4::builders::ApplicationBuilder::new() gtk4::builders::ApplicationBuilder::new()
.application_id("org.pop.CosmicComponentExample") .application_id("org.pop.CosmicComponentExample")
.cosmic_run(|app| { .cosmic_run(|app| {
App::default().register(app); App::init(app);
}); });
} }

View file

@ -3,6 +3,14 @@
use crate::*; use crate::*;
/// The pieces that make up the state of the component.
pub struct ComponentInner<Model, Widgets, Input, Output> {
pub model: Model,
pub widgets: Widgets,
pub input: Sender<Input>,
pub output: Sender<Output>,
}
/// The basis of a COSMIC widget. /// The basis of a COSMIC widget.
/// ///
/// A component takes care of constructing the UI of a widget, managing an event-loop /// 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. /// the consumer of the component.
pub trait Component: Sized + 'static { pub trait Component: Sized + 'static {
/// The arguments that are passed to the init_view method. /// The arguments that are passed to the init_view method.
type InitialArgs; type InitParams;
/// The message type that the component accepts as inputs. /// The message type that the component accepts as inputs.
type Input: 'static; type Input: 'static;
@ -19,27 +27,36 @@ pub trait Component: Sized + 'static {
type Output: 'static; type Output: 'static;
/// The widget that was constructed by the component. /// The widget that was constructed by the component.
type RootWidget: Clone + AsRef<gtk4::Widget>; type Root: Clone + AsRef<gtk4::Widget>;
/// The type that's used for storing widgets created for this component. /// The type that's used for storing widgets created for this component.
type Widgets: 'static; type Widgets: 'static;
/// Initializes the root widget
fn init_root() -> Self::Root;
fn init_inner(
params: Self::InitParams,
root_widget: &Self::Root,
input: Sender<Self::Input>,
output: Sender<Self::Output>,
) -> ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>;
/// Initializes the component and attaches it to the default local executor. /// Initializes the component and attaches it to the default local executor.
/// ///
/// Spawns an event loop on `glib::MainContext::default()`, which exists /// Spawns an event loop on `glib::MainContext::default()`, which exists
/// for as long as the root widget remains alive. /// for as long as the root widget remains alive.
fn register( fn init(params: Self::InitParams) -> Registered<Self::Root, Self::Input, Self::Output> {
mut self, let (sender, in_rx) = mpsc::unbounded_channel::<Self::Input>();
args: Self::InitialArgs, let (out_tx, output) = mpsc::unbounded_channel::<Self::Output>();
) -> Registered<Self::RootWidget, Self::Input, Self::Output> {
let (mut sender, in_rx) = mpsc::unbounded_channel::<Self::Input>();
let (mut out_tx, output) = mpsc::unbounded_channel::<Self::Output>();
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 { let handle = Handle {
widget, widget: root,
sender: sender.clone(), sender: component.input.clone(),
}; };
let (inner_tx, mut inner_rx) = mpsc::unbounded_channel::<InnerMessage<Self::Input>>(); let (inner_tx, mut inner_rx) = mpsc::unbounded_channel::<InnerMessage<Self::Input>>();
@ -55,7 +72,7 @@ pub trait Component: Sized + 'static {
while let Some(event) = inner_rx.recv().await { while let Some(event) = inner_rx.recv().await {
match event { match event {
InnerMessage::Message(event) => { InnerMessage::Message(event) => {
self.update(&mut widgets, event, &mut sender, &mut out_tx); Self::update(&mut component, event);
} }
InnerMessage::Drop => break, 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<Self::Input>,
output: &mut Sender<Self::Output>,
) -> (Self::Widgets, Self::RootWidget);
/// Handles input messages and enables the programmer to update the model and view. /// Handles input messages and enables the programmer to update the model and view.
#[allow(unused_variables)] #[allow(unused_variables)]
fn update( fn update(
&mut self, component: &mut ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>,
widgets: &mut Self::Widgets,
message: Self::Input, message: Self::Input,
input: &mut Sender<Self::Input>,
output: &mut Sender<Self::Output>,
) { ) {
} }
} }

View file

@ -10,7 +10,7 @@ use gtk4::prelude::*;
use tokio::sync::mpsc; use tokio::sync::mpsc;
pub use self::app_runner::AppRunner; 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 self::handle::{Handle, Registered};
pub use gtk4 as gtk; pub use gtk4 as gtk;
pub use relm4_macros::view; pub use relm4_macros::view;
@ -23,12 +23,14 @@ pub trait Widget<W> {
fn widget(&self) -> &W; fn widget(&self) -> &W;
} }
/// Convenience function for `Component::register()`. pub trait CosmicWidgetExt<W>: Widget<W>
pub fn register<C: Component>( where
model: C, W: AsRef<gtk::Widget>,
args: C::InitialArgs, {
) -> Registered<C::RootWidget, C::Input, C::Output> { fn attach_size_group(&self, sg: &gtk::SizeGroup) -> &Self {
model.register(args) sg.add_widget(self.widget().as_ref());
self
}
} }
/// Convenience function for forwarding events from a receiver to different sender. /// Convenience function for forwarding events from a receiver to different sender.

View file

@ -5,14 +5,14 @@
macro_rules! component { macro_rules! component {
( (
$(#[$attr:meta])* $(#[$attr:meta])*
$mvis:vis struct $model:ident ($args:ty) { $mvis:vis struct $model:ident {
$( $(
$mpvis:vis $property:ident : $type:ty, $mpvis:vis $property:ident : $type:ty,
)* )*
} }
$(#[$attr2:meta])* $(#[$attr2:meta])*
$wvis:vis struct $widgets_:ident($root:ty) { $wvis:vis struct $widgets_:ident {
$( $(
$wpvis:vis $widgets_property:ident : $widgets_type:ty, $wpvis:vis $widgets_property:ident : $widgets_type:ty,
)* )*
@ -20,22 +20,20 @@ macro_rules! component {
type Input = $input:ty; type Input = $input:ty;
type Output = $output:ty; type Output = $output:ty;
type Root = $root:ty $init_root:block;
$(#[$attr3:meta])* $(#[$attr4:meta])*
fn init_view( fn init(
$selfv:ident, $argsv:ident: $args:ty,
$argsv:ident, $rootv:ident,
$inputv:ident, $inputv:ident,
$outputv:ident $outputv:ident
) $init_view:block ) $init_view:block
$(#[$attr4:meta])* $(#[$attr5:meta])*
fn update( fn update(
$selfv2:ident, $componentv:ident,
$widgetsv:ident, $messagev:ident
$messagev:ident,
$inputv2:ident,
$outputv2:ident
) $update:block ) $update:block
) => { ) => {
$(#[$attr])* $(#[$attr])*
@ -49,27 +47,26 @@ macro_rules! component {
} }
impl Component for $model { impl Component for $model {
type InitialArgs = $args; type InitParams = $args;
type Input = $input; type Input = $input;
type Output = $output; type Output = $output;
type RootWidget = $root; type Root = $root;
type Widgets = $widgets_; type Widgets = $widgets_;
$(#[$attr3])* fn init_root() -> Self::Root $init_root
fn init_view(
&mut $selfv2,
$argsv: Self::InitialArgs,
$inputv: &mut Sender<Self::Input>,
$outputv: &mut Sender<Self::Output>
) -> (Self::Widgets, Self::RootWidget) $init_view
$(#[$attr4])* $(#[$attr4])*
fn init_inner(
$argsv: Self::InitParams,
$rootv: &Self::Root,
$inputv: Sender<Self::Input>,
$outputv: Sender<Self::Output>
) -> ComponentInner<Self, Self::Widgets, Self::Input, Self::Output> $init_view
$(#[$attr5])*
fn update( fn update(
&mut $selfv2, $componentv: &mut ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>,
$widgetsv: &mut Self::Widgets,
$messagev: Self::Input, $messagev: Self::Input,
$inputv2: &mut Sender<Self::Input>,
$outputv2: &mut Sender<Self::Output>
) $update ) $update
} }
} }