feat: Add ElmComponent

This commit is contained in:
Michael Aaron Murphy 2022-01-26 12:58:06 +01:00
parent dff9265e35
commit c25812a45b
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
6 changed files with 244 additions and 19 deletions

View file

@ -5,6 +5,7 @@ edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
[dependencies] [dependencies]
async-trait = "0.1.52"
gtk4 = "0.4.4" gtk4 = "0.4.4"
relm4-macros = "0.4.1" relm4-macros = "0.4.1"
tokio = { version = "1.15.0", features = ["sync"]} tokio = { version = "1.15.0", features = ["full"]}

View file

@ -1,21 +1,14 @@
// Copyright 2022 System76 <info@system76.com> // Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::*; use super::*;
/// 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
/// which handles signals from within the widget, and supports forwarding messages to /// which handles signals from within the widget, and supports forwarding messages to
/// the consumer of the component. /// the consumer of the component.
#[async_trait::async_trait]
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 InitParams; type InitParams;
@ -93,12 +86,5 @@ pub trait Component: Sized + 'static {
fn update( fn update(
component: &mut ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>, component: &mut ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>,
message: Self::Input, message: Self::Input,
) { );
}
}
/// Used to drop the component's event loop when the managed widget is destroyed.
enum InnerMessage<T> {
Drop,
Message(T),
} }

View file

@ -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<gtk4::Widget>;
/// 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<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.
///
/// 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<Self::Root, Self::Input, Self::Output> {
let (sender, in_rx) = mpsc::unbounded_channel::<Self::Input>();
let (out_tx, out_rx) = mpsc::unbounded_channel::<Self::Output>();
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::<InnerMessage<Self::Input>>();
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<Self::Input>,
output: &mut Sender<Self::Output>,
) -> Option<Self::Command>;
/// Update the UI
fn update_view(
&mut self,
widgets: &mut Self::Widgets,
input: &mut Sender<Self::Input>,
output: &mut Sender<Self::Output>,
);
/// A command to perform in a background thread.
async fn command(message: Self::Command) -> Option<Self::Input>;
}

View file

@ -0,0 +1,24 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Model, Widgets, Input, Output> {
pub model: Model,
pub widgets: Widgets,
pub input: Sender<Input>,
pub output: Sender<Output>,
}
/// Used to drop the component's event loop when the managed widget is destroyed.
enum InnerMessage<T> {
Drop,
Message(T),
}

View file

@ -10,8 +10,9 @@ 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, ComponentInner}; pub use self::component::{Component, ComponentInner, ElmComponent};
pub use self::handle::{Handle, Registered}; pub use self::handle::{Handle, Registered};
pub use async_trait::async_trait;
pub use gtk4 as gtk; pub use gtk4 as gtk;
pub use relm4_macros::view; pub use relm4_macros::view;

View file

@ -46,6 +46,7 @@ macro_rules! component {
$($wpvis $widgets_property: $widgets_type,)* $($wpvis $widgets_property: $widgets_type,)*
} }
#[cosmic_component_system::async_trait]
impl Component for $model { impl Component for $model {
type InitParams = $args; type InitParams = $args;
type Input = $input; 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<Self::Input>,
$outputv: Sender<Self::Output>
) -> ComponentInner<Self, Self::Widgets, Self::Input, Self::Output> $init_view
$(#[$attr5])*
fn update(
&mut $modelv,
$messagev: Self::Input,
$inputv2: &mut Sender<Self::Input>,
$outputv2: &mut Sender<Self::Output>,
) -> Option<Self::Command> $update
$(#[$attr6])*
fn update_view(
&mut $modelv2,
$widgetsv: &mut Self::Widgets,
$inputv3: &mut Sender<Self::Input>,
$outputv3: &mut Sender<Self::Output>,
) $update_view
$(#[$attr7])*
async fn command($cmdv: Self::Command) -> Option<Self::Input> $command_block
}
}
}