feat: Add ElmComponent
This commit is contained in:
parent
dff9265e35
commit
c25812a45b
6 changed files with 244 additions and 19 deletions
|
|
@ -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"]}
|
||||
tokio = { version = "1.15.0", features = ["full"]}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,14 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
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>,
|
||||
}
|
||||
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<Self, Self::Widgets, Self::Input, Self::Output>,
|
||||
message: Self::Input,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to drop the component's event loop when the managed widget is destroyed.
|
||||
enum InnerMessage<T> {
|
||||
Drop,
|
||||
Message(T),
|
||||
);
|
||||
}
|
||||
112
crates/component-system/src/component/elmlike.rs
Normal file
112
crates/component-system/src/component/elmlike.rs
Normal 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>;
|
||||
}
|
||||
24
crates/component-system/src/component/mod.rs
Normal file
24
crates/component-system/src/component/mod.rs
Normal 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),
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue