feat: COSMIC Component System Prototype
This commit is contained in:
parent
08e70faffd
commit
52df3570ec
9 changed files with 597 additions and 0 deletions
|
|
@ -3,6 +3,9 @@ name = "libcosmic"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/*"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cascade = "1.0.0"
|
cascade = "1.0.0"
|
||||||
gtk4 = { version = "0.4.4", features = ["v4_4"] }
|
gtk4 = { version = "0.4.4", features = ["v4_4"] }
|
||||||
|
|
|
||||||
10
crates/component-system/Cargo.toml
Normal file
10
crates/component-system/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "cosmic-component-system"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gtk4 = "0.4.4"
|
||||||
|
relm4-macros = "0.4.1"
|
||||||
|
tokio = { version = "1.15.0", features = ["sync"]}
|
||||||
93
crates/component-system/README.md
Normal file
93
crates/component-system/README.md
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
## Defining a Component
|
||||||
|
|
||||||
|
The simplest way to define a component is to use the `component!()` macro.
|
||||||
|
|
||||||
|
```rs
|
||||||
|
pub enum MyCustomInputMessage {
|
||||||
|
Variant1,
|
||||||
|
Variant2,
|
||||||
|
}
|
||||||
|
|
||||||
|
component! {
|
||||||
|
// The `()` is the args parameter accepted by `init_view()`
|
||||||
|
// and `Component::register()`.
|
||||||
|
pub struct MyCustomModel(()) {
|
||||||
|
pub state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `gtk::Box` is the root widget returned in `init_view()`.
|
||||||
|
pub struct MyCustomWidgets(gtk::Box) {
|
||||||
|
description: gtk::Label,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The type of the input sender
|
||||||
|
type Input = MyCustomInputMessage;
|
||||||
|
|
||||||
|
// The type of the output sender
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
// `self` is `MyCustomModel`, and must return `(MyCustomWidget, RootWidget)`
|
||||||
|
fn init_view(self, args, input, output) {
|
||||||
|
ccs::view! {
|
||||||
|
root = gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
|
||||||
|
append: description = >k::Label {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(MyCustomWidgets { description }, root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where events are received, with `self` also being `MyCustomModel`, and
|
||||||
|
// `widgets` is `MyCustomInputMessage`. `event` is `MyCustonInputMessage`.
|
||||||
|
fn update(self, widgets, event, input, output) {
|
||||||
|
match event {
|
||||||
|
MyCustomInputMessage::Variant1 => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MyCustomInputMessage::Variant2 => {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Components can be created and have their output events forwarded:
|
||||||
|
|
||||||
|
```rs
|
||||||
|
let counter = InfoButton::default()
|
||||||
|
.register("Clicked 0 times".into(), "Click".into())
|
||||||
|
.forward(input.clone(), |event| match event {
|
||||||
|
InfoButtonOutput::Clicked => AppEvent::Increment
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The handle returned can be used to emit inputs to it, and to get the root widget.
|
||||||
|
|
||||||
|
```rs
|
||||||
|
counter.emit(InfoButtonInput::SetDescription(format!("Clicked {} times", count)));
|
||||||
|
|
||||||
|
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).
|
||||||
109
crates/component-system/examples/basic/components/app.rs
Normal file
109
crates/component-system/examples/basic/components/app.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use crate::components::{InfoButton, InfoButtonInput, InfoButtonOutput};
|
||||||
|
use ccs::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
|
/// An input event that is used to update the model.
|
||||||
|
pub enum AppEvent {
|
||||||
|
Destroy,
|
||||||
|
Increment,
|
||||||
|
}
|
||||||
|
|
||||||
|
component! {
|
||||||
|
/// The model where component state is stored.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct App(gtk::Application) {
|
||||||
|
pub counter: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widgets that are initialized in the view.
|
||||||
|
pub struct AppWidgets(gtk::ApplicationWindow) {
|
||||||
|
list: gtk::ListBox,
|
||||||
|
destroyable: Option<Handle<gtk::Box, InfoButtonInput>>,
|
||||||
|
counter: Handle<gtk::Box, InfoButtonInput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Input = AppEvent;
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
fn init_view(self, app, input, _output) {
|
||||||
|
let button_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Both);
|
||||||
|
|
||||||
|
// Create an `InfoButton` component.
|
||||||
|
let destroyable = InfoButton::default()
|
||||||
|
.register((String::new(), "Destroy".into(), button_group.clone()))
|
||||||
|
.forward(input.clone(), |event| match event {
|
||||||
|
InfoButtonOutput::Clicked => AppEvent::Destroy,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Instruct the component to update its description.
|
||||||
|
let _ = destroyable.emit(InfoButtonInput::SetDescription(
|
||||||
|
"Click this button to destroy me".into(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Create a counter component, too.
|
||||||
|
let counter = InfoButton::default()
|
||||||
|
.register(("Click me too".into(), "Click".into(), button_group))
|
||||||
|
.forward(input.clone(), |event| match event {
|
||||||
|
InfoButtonOutput::Clicked => AppEvent::Increment,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Construct the view for this component, attaching the component's widget.
|
||||||
|
ccs::view! {
|
||||||
|
window = gtk::ApplicationWindow {
|
||||||
|
set_application: Some(&app),
|
||||||
|
set_child = Some(>k::Box) {
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
set_size_request: args!(400, -1),
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
|
||||||
|
append: list = >k::ListBox {
|
||||||
|
set_selection_mode: gtk::SelectionMode::None,
|
||||||
|
set_hexpand: true,
|
||||||
|
|
||||||
|
append: destroyable.widget(),
|
||||||
|
append: counter.widget(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.show();
|
||||||
|
|
||||||
|
(
|
||||||
|
AppWidgets {
|
||||||
|
list,
|
||||||
|
counter,
|
||||||
|
destroyable: Some(destroyable),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the view
|
||||||
|
fn update(self, widgets, event, _input, _output) {
|
||||||
|
match event {
|
||||||
|
AppEvent::Increment => {
|
||||||
|
self.counter += 1;
|
||||||
|
|
||||||
|
widgets
|
||||||
|
.counter
|
||||||
|
.emit(InfoButtonInput::SetDescription(format!(
|
||||||
|
"Clicked {} times",
|
||||||
|
self.counter
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
AppEvent::Destroy => {
|
||||||
|
// Components are kept alive by their root GTK widget.
|
||||||
|
if let Some(handle) = widgets.destroyable.take() {
|
||||||
|
if let Some(parent) = handle.widget().parent() {
|
||||||
|
widgets.list.remove(&parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use ccs::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
|
pub enum InfoButtonInput {
|
||||||
|
SetDescription(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InfoButtonOutput {
|
||||||
|
Clicked,
|
||||||
|
}
|
||||||
|
|
||||||
|
component! {
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct InfoButton((String, String, gtk::SizeGroup)) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InfoButtonWidgets(gtk::Box) {
|
||||||
|
description: gtk::Label,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Input = InfoButtonInput;
|
||||||
|
type Output = InfoButtonOutput;
|
||||||
|
|
||||||
|
fn init_view(self, args, _input, output) {
|
||||||
|
let (desc, button_label, sg) = args;
|
||||||
|
ccs::view! {
|
||||||
|
root = gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_margin_start: 20,
|
||||||
|
set_margin_end: 20,
|
||||||
|
set_margin_top: 8,
|
||||||
|
set_margin_bottom: 8,
|
||||||
|
set_spacing: 24,
|
||||||
|
|
||||||
|
append: description = >k::Label {
|
||||||
|
set_label: &desc,
|
||||||
|
set_halign: gtk::Align::Start,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
set_ellipsize: gtk::pango::EllipsizeMode::End,
|
||||||
|
},
|
||||||
|
|
||||||
|
append: button = >k::Button {
|
||||||
|
set_label: &button_label,
|
||||||
|
|
||||||
|
connect_clicked(output) => move |_| {
|
||||||
|
let _ = output.send(InfoButtonOutput::Clicked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sg.add_widget(&button);
|
||||||
|
|
||||||
|
(InfoButtonWidgets { description }, root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(self, widgets, message, _input, _output) {
|
||||||
|
match message {
|
||||||
|
InfoButtonInput::SetDescription(value) => {
|
||||||
|
widgets.description.set_text(&value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
crates/component-system/examples/basic/components/mod.rs
Normal file
8
crates/component-system/examples/basic/components/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod info_button;
|
||||||
|
|
||||||
|
pub use self::app::*;
|
||||||
|
pub use self::info_button::*;
|
||||||
15
crates/component-system/examples/basic/main.rs
Normal file
15
crates/component-system/examples/basic/main.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
extern crate cosmic_component_system as ccs;
|
||||||
|
|
||||||
|
mod components;
|
||||||
|
|
||||||
|
use self::components::App;
|
||||||
|
use ccs::Component;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
ccs::run(|app| {
|
||||||
|
App::default().register(app);
|
||||||
|
});
|
||||||
|
}
|
||||||
214
crates/component-system/src/lib.rs
Normal file
214
crates/component-system/src/lib.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
mod macros;
|
||||||
|
|
||||||
|
use gtk4::prelude::*;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
pub use gtk4 as gtk;
|
||||||
|
pub use relm4_macros::view;
|
||||||
|
|
||||||
|
pub type Sender<T> = mpsc::UnboundedSender<T>;
|
||||||
|
pub type Receiver<T> = mpsc::UnboundedReceiver<T>;
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle to an active widget component in the system.
|
||||||
|
pub struct Handle<W, I> {
|
||||||
|
/// The widget that this component manages.
|
||||||
|
widget: W,
|
||||||
|
|
||||||
|
/// Used for emitting events to the component.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to drop the component's event loop when the managed widget is destroyed.
|
||||||
|
enum InnerMessage<T> {
|
||||||
|
Drop,
|
||||||
|
Message(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides a convenience function for getting a widget out of a type.
|
||||||
|
pub trait Widget<W> {
|
||||||
|
fn widget(&self) -> &W;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub trait Component: Sized + 'static {
|
||||||
|
/// The arguments that are passed to the init_view method.
|
||||||
|
type InitialArgs;
|
||||||
|
|
||||||
|
/// 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 RootWidget: Clone + AsRef<gtk4::Widget>;
|
||||||
|
|
||||||
|
/// The type that's used for storing widgets created for this component.
|
||||||
|
type Widgets: 'static;
|
||||||
|
|
||||||
|
/// 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 register(
|
||||||
|
mut self,
|
||||||
|
args: Self::InitialArgs,
|
||||||
|
) -> 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 handle = Handle {
|
||||||
|
widget,
|
||||||
|
sender: sender.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 widgets, event, &mut sender, &mut out_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerMessage::Drop => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
forward(in_rx, inner_tx, |event| InnerMessage::Message(event));
|
||||||
|
|
||||||
|
Registered {
|
||||||
|
handle,
|
||||||
|
receiver: output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
widgets: &mut Self::Widgets,
|
||||||
|
message: Self::Input,
|
||||||
|
input: &mut Sender<Self::Input>,
|
||||||
|
output: &mut Sender<Self::Output>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function for `Component::register()`.
|
||||||
|
pub fn register<C: Component>(
|
||||||
|
model: C,
|
||||||
|
args: C::InitialArgs,
|
||||||
|
) -> Registered<C::RootWidget, C::Input, C::Output> {
|
||||||
|
model.register(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 launching an application.
|
||||||
|
pub fn run<F: Fn(gtk4::Application) + 'static>(func: F) {
|
||||||
|
use gtk4::prelude::*;
|
||||||
|
let app = gtk4::Application::new(None, Default::default());
|
||||||
|
|
||||||
|
app.connect_activate(move |app| func(app.clone()));
|
||||||
|
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
76
crates/component-system/src/macros.rs
Normal file
76
crates/component-system/src/macros.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! component {
|
||||||
|
(
|
||||||
|
$(#[$attr:meta])*
|
||||||
|
$mvis:vis struct $model:ident ($args:ty) {
|
||||||
|
$(
|
||||||
|
$mpvis:vis $property:ident : $type:ty,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
$(#[$attr2:meta])*
|
||||||
|
$wvis:vis struct $widgets_:ident($root:ty) {
|
||||||
|
$(
|
||||||
|
$wpvis:vis $widgets_property:ident : $widgets_type:ty,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
type Input = $input:ty;
|
||||||
|
type Output = $output:ty;
|
||||||
|
|
||||||
|
$(#[$attr3:meta])*
|
||||||
|
fn init_view(
|
||||||
|
$selfv:ident,
|
||||||
|
$argsv:ident,
|
||||||
|
$inputv:ident,
|
||||||
|
$outputv:ident
|
||||||
|
) $init_view:block
|
||||||
|
|
||||||
|
$(#[$attr4:meta])*
|
||||||
|
fn update(
|
||||||
|
$selfv2:ident,
|
||||||
|
$widgetsv:ident,
|
||||||
|
$messagev:ident,
|
||||||
|
$inputv2:ident,
|
||||||
|
$outputv2:ident
|
||||||
|
) $update:block
|
||||||
|
) => {
|
||||||
|
$(#[$attr])*
|
||||||
|
$mvis struct $model {
|
||||||
|
$($mpvis $property: $type,)*
|
||||||
|
}
|
||||||
|
|
||||||
|
$(#[$attr2])*
|
||||||
|
$wvis struct $widgets_ {
|
||||||
|
$($wpvis $widgets_property: $widgets_type,)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for $model {
|
||||||
|
type InitialArgs = $args;
|
||||||
|
type Input = $input;
|
||||||
|
type Output = $output;
|
||||||
|
type RootWidget = $root;
|
||||||
|
type Widgets = $widgets_;
|
||||||
|
|
||||||
|
$(#[$attr3])*
|
||||||
|
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])*
|
||||||
|
fn update(
|
||||||
|
&mut $selfv2,
|
||||||
|
$widgetsv: &mut Self::Widgets,
|
||||||
|
$messagev: Self::Input,
|
||||||
|
$inputv2: &mut Sender<Self::Input>,
|
||||||
|
$outputv2: &mut Sender<Self::Output>
|
||||||
|
) $update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue