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"
|
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"]}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
}
|
}
|
||||||
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;
|
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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue