chore: Sync with Relm4 new-approach branch
This commit is contained in:
parent
c25812a45b
commit
3e4a00a8d8
12 changed files with 123 additions and 463 deletions
|
|
@ -4,8 +4,14 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies.tokio]
|
||||||
async-trait = "0.1.52"
|
version = "1.15"
|
||||||
gtk4 = "0.4.4"
|
features = ["rt", "rt-multi-thread", "sync"]
|
||||||
relm4-macros = "0.4.1"
|
|
||||||
tokio = { version = "1.15.0", features = ["full"]}
|
[dependencies.relm4]
|
||||||
|
git = "https://github.com/AaronErhardt/relm4"
|
||||||
|
branch = "new-approach"
|
||||||
|
|
||||||
|
[dependencies.relm4-macros]
|
||||||
|
git = "https://github.com/AaronErhardt/relm4"
|
||||||
|
branch = "new-approach"
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# COSMIC Component System
|
# COSMIC Component System
|
||||||
|
|
||||||
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 prototyping area for a rewrite of Relm4's component system.
|
||||||
|
|
||||||
## Using a Macro to Define a Component
|
## Using a Macro to Define a Component
|
||||||
|
|
||||||
|
|
@ -12,6 +12,10 @@ pub enum MyCustomInputMessage {
|
||||||
Variant2,
|
Variant2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum MyCustomCommand {
|
||||||
|
Action
|
||||||
|
}
|
||||||
|
|
||||||
component! {
|
component! {
|
||||||
// The model stores the state of this component.
|
// The model stores the state of this component.
|
||||||
pub struct MyCustomModel {
|
pub struct MyCustomModel {
|
||||||
|
|
@ -47,17 +51,14 @@ component! {
|
||||||
|
|
||||||
root.append(&description);
|
root.append(&description);
|
||||||
|
|
||||||
ComponentInner {
|
Fuselage {
|
||||||
model: MyCustomModel { state: String::new() },
|
model: MyCustomModel { state: String::new() },
|
||||||
widgets: MyCustomWidgets { description },
|
widgets: MyCustomWidgets { description },
|
||||||
input,
|
|
||||||
output
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where events are received, with `component` is the `ComponentInner`,
|
// Updates the model and view. `self` is the model.
|
||||||
// and `event` is a `MyCustomInputMessage` which was just received.
|
fn update(&mut self, widgets, event, input, output) {
|
||||||
fn update(component, event) {
|
|
||||||
match event {
|
match event {
|
||||||
MyCustomInputMessage::Variant1 => {
|
MyCustomInputMessage::Variant1 => {
|
||||||
|
|
||||||
|
|
@ -67,6 +68,12 @@ component! {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(MyCustomCommand::Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn command(command: MyCustomCommand, input) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -74,7 +81,8 @@ 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::init("Clicked 0 times".into(), "Click".into())
|
let counter = InfoButton::init()
|
||||||
|
.launch("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
|
||||||
});
|
});
|
||||||
|
|
@ -85,18 +93,5 @@ The handle returned can be used to emit inputs to it, and to get the root widget
|
||||||
```rs
|
```rs
|
||||||
counter.emit(InfoButtonInput::SetDescription(format!("Clicked {} times", count)));
|
counter.emit(InfoButtonInput::SetDescription(format!("Clicked {} times", count)));
|
||||||
|
|
||||||
box.append(counter.widget());
|
box.append(&counter.widget);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
[Relm4](https://github.com/AaronErhardt/relm4) uses a similar approach, but closely follows the Elm model. This library was created as an alternative approach that makes developing reusable components with forwardable events simpler.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/).
|
|
||||||
|
|
||||||
### Contribution
|
|
||||||
|
|
||||||
Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the Mozilla Public License 2.0 (MPL-2.0).
|
|
||||||
|
|
@ -21,20 +21,29 @@ component! {
|
||||||
/// Widgets that are initialized in the view.
|
/// Widgets that are initialized in the view.
|
||||||
pub struct AppWidgets {
|
pub struct AppWidgets {
|
||||||
list: gtk::ListBox,
|
list: gtk::ListBox,
|
||||||
destroyable: Option<Handle<gtk::Box, InfoButtonInput>>,
|
destroyable: Option<Controller<gtk::Box, InfoButtonInput>>,
|
||||||
counter: Handle<gtk::Box, InfoButtonInput>,
|
counter: Controller<gtk::Box, InfoButtonInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Input = AppEvent;
|
type Input = AppEvent;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
type Root = gtk::ApplicationWindow {
|
type Root = gtk::Box {
|
||||||
Default::default()
|
ccs::view! {
|
||||||
|
root = gtk::Box {
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
set_size_request: args!(400, -1),
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(app: gtk::Application, root, input, output) {
|
fn init(window: gtk::ApplicationWindow, root, input, _output) {
|
||||||
let button_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Both);
|
let button_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Both);
|
||||||
|
|
||||||
let destroyable = InfoButton::init((String::new(), "Destroy".into(), button_group.clone()))
|
let destroyable = InfoButton::init()
|
||||||
|
.launch_stateful((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,
|
||||||
});
|
});
|
||||||
|
|
@ -45,64 +54,59 @@ component! {
|
||||||
));
|
));
|
||||||
|
|
||||||
// Create a counter component, too.
|
// Create a counter component, too.
|
||||||
let counter = InfoButton::init(("Click me too".into(), "Click".into(), button_group))
|
let counter = InfoButton::init()
|
||||||
|
.launch_stateful(("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,
|
||||||
});
|
});
|
||||||
|
|
||||||
ccs::view! {
|
ccs::view! {
|
||||||
container = gtk::Box {
|
list = gtk::ListBox {
|
||||||
set_halign: gtk::Align::Center,
|
set_selection_mode: gtk::SelectionMode::None,
|
||||||
set_size_request: args!(400, -1),
|
set_hexpand: true,
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
|
||||||
|
|
||||||
append: list = >k::ListBox {
|
append: &destroyable.widget,
|
||||||
set_selection_mode: gtk::SelectionMode::None,
|
append: &counter.widget,
|
||||||
set_hexpand: true,
|
|
||||||
|
|
||||||
append: destroyable.widget(),
|
|
||||||
append: counter.widget(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.set_application(Some(&app));
|
root.append(&list);
|
||||||
root.set_child(Some(&container));
|
window.set_child(Some(root));
|
||||||
|
|
||||||
root.show();
|
Fuselage {
|
||||||
|
|
||||||
ComponentInner {
|
|
||||||
model: Self::default(),
|
model: Self::default(),
|
||||||
widgets: AppWidgets { list, destroyable: Some(destroyable), counter },
|
widgets: AppWidgets { list, destroyable: Some(destroyable), counter },
|
||||||
input,
|
|
||||||
output
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the view
|
/// Updates the view
|
||||||
fn update(component, event) {
|
fn update(&mut self, widgets, event, _input, _output) {
|
||||||
let &mut ComponentInner { ref mut model, ref mut widgets, .. } = component;
|
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
AppEvent::Increment => {
|
AppEvent::Increment => {
|
||||||
model.counter += 1;
|
self.counter += 1;
|
||||||
|
|
||||||
widgets
|
widgets
|
||||||
.counter
|
.counter
|
||||||
.emit(InfoButtonInput::SetDescription(format!(
|
.emit(InfoButtonInput::SetDescription(format!(
|
||||||
"Clicked {} times",
|
"Clicked {} times",
|
||||||
model.counter
|
self.counter
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
AppEvent::Destroy => {
|
AppEvent::Destroy => {
|
||||||
// Components are kept alive by their root GTK widget.
|
// Components are kept alive by their root GTK widget.
|
||||||
if let Some(handle) = widgets.destroyable.take() {
|
if let Some(handle) = widgets.destroyable.take() {
|
||||||
if let Some(parent) = handle.widget().parent() {
|
if let Some(parent) = handle.widget.parent() {
|
||||||
widgets.list.remove(&parent);
|
widgets.list.remove(&parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn command(_message: (), _input) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ component! {
|
||||||
pub struct InfoButtonWidgets {
|
pub struct InfoButtonWidgets {
|
||||||
description: gtk::Label,
|
description: gtk::Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Input = InfoButtonInput;
|
type Input = InfoButtonInput;
|
||||||
type Output = InfoButtonOutput;
|
type Output = InfoButtonOutput;
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@ component! {
|
||||||
root
|
root
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(args: (String, String, gtk::SizeGroup), root, input, output) {
|
fn init(args: (String, String, gtk::SizeGroup), root, _input, output) {
|
||||||
let (desc, button_label, sg) = args;
|
let (desc, button_label, sg) = args;
|
||||||
|
|
||||||
ccs::view! {
|
ccs::view! {
|
||||||
|
|
@ -67,19 +68,23 @@ component! {
|
||||||
|
|
||||||
sg.add_widget(&button);
|
sg.add_widget(&button);
|
||||||
|
|
||||||
ComponentInner {
|
Fuselage {
|
||||||
model: InfoButton {},
|
model: InfoButton {},
|
||||||
widgets: InfoButtonWidgets { description },
|
widgets: InfoButtonWidgets { description },
|
||||||
input,
|
|
||||||
output,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(component, message) {
|
fn update(&mut self, widgets, message, _input, _output) {
|
||||||
match message {
|
match message {
|
||||||
InfoButtonInput::SetDescription(value) => {
|
InfoButtonInput::SetDescription(value) => {
|
||||||
component.widgets.description.set_text(&value);
|
widgets.description.set_text(&value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn command(_message: (), _input) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ extern crate cosmic_component_system as ccs;
|
||||||
mod components;
|
mod components;
|
||||||
|
|
||||||
use self::components::App;
|
use self::components::App;
|
||||||
use ccs::{AppRunner, Component};
|
use ccs::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
gtk4::builders::ApplicationBuilder::new()
|
gtk::builders::ApplicationBuilder::new()
|
||||||
.application_id("org.pop.CosmicComponentExample")
|
.application_id("org.pop.CosmicComponentExample")
|
||||||
.cosmic_run(|app| {
|
.launch(|_app, window| {
|
||||||
App::init(app);
|
App::init().launch_stateful(window);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
use gtk4::prelude::{ApplicationExt, ApplicationExtManual};
|
|
||||||
|
|
||||||
pub trait AppRunner {
|
|
||||||
/// Convenience method to activates and run a GTK4 application.
|
|
||||||
fn cosmic_run<F: Fn(gtk4::Application) + 'static>(self, func: F);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppRunner for gtk4::Application {
|
|
||||||
fn cosmic_run<F: Fn(gtk4::Application) + 'static>(self, func: F) {
|
|
||||||
self.connect_activate(move |app| func(app.clone()));
|
|
||||||
self.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppRunner for gtk4::builders::ApplicationBuilder {
|
|
||||||
fn cosmic_run<F: Fn(gtk4::Application) + 'static>(self, func: F) {
|
|
||||||
self.build().cosmic_run(func);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// The message type that the component accepts as inputs.
|
|
||||||
type Input: 'static;
|
|
||||||
|
|
||||||
/// The message type that the component provides as outputs.
|
|
||||||
type Output: 'static;
|
|
||||||
|
|
||||||
/// 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, output) = mpsc::unbounded_channel::<Self::Output>();
|
|
||||||
|
|
||||||
let root = Self::init_root();
|
|
||||||
|
|
||||||
let mut component = Self::init_inner(params, &root, sender, out_tx);
|
|
||||||
|
|
||||||
let handle = Handle {
|
|
||||||
widget: root,
|
|
||||||
sender: component.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) => {
|
|
||||||
Self::update(&mut component, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerMessage::Drop => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
forward(in_rx, inner_tx, InnerMessage::Message);
|
|
||||||
|
|
||||||
Registered {
|
|
||||||
handle,
|
|
||||||
receiver: output,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles input messages and enables the programmer to update the model and view.
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
fn update(
|
|
||||||
component: &mut ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>,
|
|
||||||
message: Self::Input,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
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>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
// 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),
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Handle to an active widget component in the system.
|
|
||||||
pub struct Handle<W, I> {
|
|
||||||
/// The widget that this component manages.
|
|
||||||
pub(crate) widget: W,
|
|
||||||
|
|
||||||
/// Used for emitting events to the component.
|
|
||||||
pub(crate) sender: Sender<I>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W, I> Widget<W> for Handle<W, I> {
|
|
||||||
fn widget(&self) -> &W {
|
|
||||||
&self.widget
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W, I> Handle<W, I> {
|
|
||||||
pub fn emit(&self, event: I) {
|
|
||||||
let _ = self.sender.send(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A newly-registered component which supports destructuring the handle
|
|
||||||
/// by forwarding or ignoring outputs from the component.
|
|
||||||
pub struct Registered<W: Clone + AsRef<gtk4::Widget>, I, O> {
|
|
||||||
/// Handle to the component that was registered.
|
|
||||||
pub handle: Handle<W, I>,
|
|
||||||
|
|
||||||
/// The outputs being received by the component.
|
|
||||||
pub receiver: Receiver<O>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: Clone + AsRef<gtk4::Widget>, I: 'static, O: 'static> Registered<W, I, O> {
|
|
||||||
/// Forwards output events to the designated sender.
|
|
||||||
pub fn forward<X: 'static, F: (Fn(O) -> X) + 'static>(
|
|
||||||
self,
|
|
||||||
sender: Sender<X>,
|
|
||||||
transform: F,
|
|
||||||
) -> Handle<W, I> {
|
|
||||||
let Registered { handle, receiver } = self;
|
|
||||||
forward(receiver, sender, transform);
|
|
||||||
handle
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle<F: FnMut(O) + 'static>(self, mut func: F) -> Handle<W, I> {
|
|
||||||
let Registered {
|
|
||||||
handle,
|
|
||||||
mut receiver,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
spawn_local(async move {
|
|
||||||
while let Some(event) = receiver.recv().await {
|
|
||||||
func(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
handle
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ignore outputs from the component and take the handle.
|
|
||||||
pub fn ignore(self) -> Handle<W, I> {
|
|
||||||
self.handle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +1,7 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
mod app_runner;
|
|
||||||
mod component;
|
|
||||||
mod handle;
|
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
use gtk4::prelude::*;
|
pub use relm4::*;
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
pub use self::app_runner::AppRunner;
|
|
||||||
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;
|
pub use relm4_macros::view;
|
||||||
|
|
||||||
pub type Sender<T> = mpsc::UnboundedSender<T>;
|
|
||||||
pub type Receiver<T> = mpsc::UnboundedReceiver<T>;
|
|
||||||
|
|
||||||
/// Provides a convenience function for getting a widget out of a type.
|
|
||||||
pub trait Widget<W> {
|
|
||||||
fn widget(&self) -> &W;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait CosmicWidgetExt<W>: Widget<W>
|
|
||||||
where
|
|
||||||
W: AsRef<gtk::Widget>,
|
|
||||||
{
|
|
||||||
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.
|
|
||||||
pub fn forward<I: 'static, O: 'static, F: (Fn(I) -> O) + 'static>(
|
|
||||||
mut receiver: Receiver<I>,
|
|
||||||
sender: Sender<O>,
|
|
||||||
transformer: F,
|
|
||||||
) {
|
|
||||||
spawn_local(async move {
|
|
||||||
while let Some(event) = receiver.recv().await {
|
|
||||||
if sender.send(transformer(event)).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience function for spawning tasks on the local executor
|
|
||||||
pub fn spawn_local<F: std::future::Future<Output = ()> + 'static>(func: F) {
|
|
||||||
gtk4::glib::MainContext::default().spawn_local(func);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,15 @@ macro_rules! component {
|
||||||
|
|
||||||
$(#[$attr5:meta])*
|
$(#[$attr5:meta])*
|
||||||
fn update(
|
fn update(
|
||||||
$componentv:ident,
|
&mut $modelv:ident,
|
||||||
$messagev:ident
|
$widgetsv:ident,
|
||||||
|
$messagev:ident,
|
||||||
|
$inputv2:ident,
|
||||||
|
$outputv2:ident
|
||||||
) $update:block
|
) $update:block
|
||||||
|
|
||||||
|
$(#[$attr6:meta])*
|
||||||
|
async fn command($cmdv:ident: $cmd:ty, $inputv3:ident) { $($cmd_expr:expr)* }
|
||||||
) => {
|
) => {
|
||||||
$(#[$attr])*
|
$(#[$attr])*
|
||||||
$mvis struct $model {
|
$mvis struct $model {
|
||||||
|
|
@ -46,29 +52,37 @@ macro_rules! component {
|
||||||
$($wpvis $widgets_property: $widgets_type,)*
|
$($wpvis $widgets_property: $widgets_type,)*
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cosmic_component_system::async_trait]
|
impl StatefulComponent for $model {
|
||||||
impl Component for $model {
|
type Payload = $args;
|
||||||
type InitParams = $args;
|
|
||||||
type Input = $input;
|
type Input = $input;
|
||||||
type Output = $output;
|
type Output = $output;
|
||||||
type Root = $root;
|
type Root = $root;
|
||||||
type Widgets = $widgets_;
|
type Widgets = $widgets_;
|
||||||
|
type Command = $cmd;
|
||||||
|
|
||||||
fn init_root() -> Self::Root $init_root
|
fn init_root() -> Self::Root $init_root
|
||||||
|
|
||||||
$(#[$attr4])*
|
$(#[$attr4])*
|
||||||
fn init_inner(
|
fn dock(
|
||||||
$argsv: Self::InitParams,
|
$argsv: Self::Payload,
|
||||||
$rootv: &Self::Root,
|
$rootv: &Self::Root,
|
||||||
$inputv: Sender<Self::Input>,
|
$inputv: &mut Sender<Self::Input>,
|
||||||
$outputv: Sender<Self::Output>
|
$outputv: &mut Sender<Self::Output>
|
||||||
) -> ComponentInner<Self, Self::Widgets, Self::Input, Self::Output> $init_view
|
) -> Fuselage<Self, Self::Widgets> $init_view
|
||||||
|
|
||||||
$(#[$attr5])*
|
$(#[$attr5])*
|
||||||
fn update(
|
fn update(
|
||||||
$componentv: &mut ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>,
|
&mut $modelv,
|
||||||
|
$widgetsv: &mut Self::Widgets,
|
||||||
$messagev: Self::Input,
|
$messagev: Self::Input,
|
||||||
) $update
|
$inputv2: &mut Sender<Self::Input>,
|
||||||
|
$outputv2: &mut Sender<Self::Output>
|
||||||
|
) -> Option<Self::Command> $update
|
||||||
|
|
||||||
|
$(#[$attr6])*
|
||||||
|
fn command($cmdv: $cmd, $inputv3: Sender<Self::Input>) -> CommandFuture {
|
||||||
|
Box::pin(async move { $($cmd_expr)* } )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +106,6 @@ macro_rules! elm_component {
|
||||||
|
|
||||||
type Input = $input:ty;
|
type Input = $input:ty;
|
||||||
type Output = $output:ty;
|
type Output = $output:ty;
|
||||||
type Command = $cmd:ty;
|
|
||||||
type Root = $root:ty $init_root:block;
|
type Root = $root:ty $init_root:block;
|
||||||
|
|
||||||
$(#[$attr4:meta])*
|
$(#[$attr4:meta])*
|
||||||
|
|
@ -105,7 +118,7 @@ macro_rules! elm_component {
|
||||||
|
|
||||||
$(#[$attr5:meta])*
|
$(#[$attr5:meta])*
|
||||||
fn update(
|
fn update(
|
||||||
$modelv:ident,
|
&mut $modelv:ident,
|
||||||
$messagev:ident,
|
$messagev:ident,
|
||||||
$inputv2:ident,
|
$inputv2:ident,
|
||||||
$outputv2:ident
|
$outputv2:ident
|
||||||
|
|
@ -113,14 +126,14 @@ macro_rules! elm_component {
|
||||||
|
|
||||||
$(#[$attr6:meta])*
|
$(#[$attr6:meta])*
|
||||||
fn update_view(
|
fn update_view(
|
||||||
$modelv2:ident,
|
&$modelv2:ident,
|
||||||
$widgetsv:ident,
|
$widgetsv:ident,
|
||||||
$inputv3:ident,
|
$inputv3:ident,
|
||||||
$outputv3:ident
|
$outputv3:ident
|
||||||
) $update_view:block
|
) $update_view:block
|
||||||
|
|
||||||
$(#[$attr7:meta])*
|
$(#[$attr7:meta])*
|
||||||
async fn command($cmdv:ident) $command_block:block
|
async fn command($cmdv:ident: $cmd:ty, $inputv4:ident) { $($cmd_expr:expr)* }
|
||||||
) => {
|
) => {
|
||||||
$(#[$attr])*
|
$(#[$attr])*
|
||||||
$mvis struct $model {
|
$mvis struct $model {
|
||||||
|
|
@ -132,43 +145,44 @@ macro_rules! elm_component {
|
||||||
$($wpvis $widgets_property: $widgets_type,)*
|
$($wpvis $widgets_property: $widgets_type,)*
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cosmic_component_system::async_trait]
|
impl StatefulComponent for $model {
|
||||||
impl ElmComponent for $model {
|
type Payload = $args;
|
||||||
type InitParams = $args;
|
|
||||||
type Input = $input;
|
type Input = $input;
|
||||||
type Output = $output;
|
type Output = $output;
|
||||||
type Command = $cmd;
|
|
||||||
type Root = $root;
|
type Root = $root;
|
||||||
type Widgets = $widgets_;
|
type Widgets = $widgets_;
|
||||||
|
type Command = $cmd;
|
||||||
|
|
||||||
fn init_root() -> Self::Root $init_root
|
fn init_root() -> Self::Root $init_root
|
||||||
|
|
||||||
$(#[$attr4])*
|
$(#[$attr4])*
|
||||||
fn init_inner(
|
fn dock(
|
||||||
$argsv: Self::InitParams,
|
$argsv: Self::Payload,
|
||||||
$rootv: &Self::Root,
|
$rootv: &Self::Root,
|
||||||
$inputv: Sender<Self::Input>,
|
$inputv: &mut Sender<Self::Input>,
|
||||||
$outputv: Sender<Self::Output>
|
$outputv: &mut Sender<Self::Output>
|
||||||
) -> ComponentInner<Self, Self::Widgets, Self::Input, Self::Output> $init_view
|
) -> Fuselage<Self, Self::Widgets> $init_view
|
||||||
|
|
||||||
$(#[$attr5])*
|
$(#[$attr5])*
|
||||||
fn update(
|
fn update(
|
||||||
&mut $modelv,
|
&mut $modelv,
|
||||||
$messagev: Self::Input,
|
$messagev: Self::Input,
|
||||||
$inputv2: &mut Sender<Self::Input>,
|
$inputv2: &mut Sender<Self::Input>,
|
||||||
$outputv2: &mut Sender<Self::Output>,
|
$outputv2: &mut Sender<Self::Output>
|
||||||
) -> Option<Self::Command> $update
|
) -> Option<Self::Command> $update
|
||||||
|
|
||||||
$(#[$attr6])*
|
$(#[$attr6])*
|
||||||
fn update_view(
|
fn update_view(
|
||||||
&mut $modelv2,
|
&$modelv2,
|
||||||
$widgetsv: &mut Self::Widgets,
|
$widgetsv: &mut Self::Widgets,
|
||||||
$inputv3: &mut Sender<Self::Input>,
|
$inputv3: &mut Sender<Self::Input>,
|
||||||
$outputv3: &mut Sender<Self::Output>,
|
$outputsv3: &mut Sender<Self::Output>
|
||||||
) $update_view
|
) $update_view
|
||||||
|
|
||||||
$(#[$attr7])*
|
$(#[$attr7])*
|
||||||
async fn command($cmdv: Self::Command) -> Option<Self::Input> $command_block
|
fn command($cmdv: $cmd, $inputv4: Sender<Self::Input>) -> CommandFuture {
|
||||||
|
Box::pin(async move { $($cmd_expr)* } )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue