Tried implementing a prototype to explain this alternative approach. Here's a rough draft of some of the differences:
- No differentiation between app components and widget components
- Combines the
MicroComponents
and Components
API together into a more flexible Component
trait
- Doesn't require a
Model
trait -- implementing Component
is enough to have a fully functioning component
- Uses channels for sending events to and from the component, so components do not need to know any type information about a parent type, which makes them reusable
- Zero use of
Rc
and RefCell
, because all state is owned by the component's inner event loop.
- Watches for the destroy event on the root widget to hang up the component's event loop.
The implementation:
// Copyright 2022 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0
use gtk4::prelude::*;
use tokio::sync::mpsc;
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
}
/// 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: Clone + AsRef<gtk4::Widget>, I> {
/// The widget that this component manages.
pub widget: W,
/// Used for emitting events to the component.
pub sender: Sender<I>,
}
impl<W: Clone + AsRef<gtk4::Widget>, I> Widget<W> for Handle<W, I> {
fn widget(&self) -> &W {
&self.widget
}
}
impl<W: Clone + AsRef<gtk4::Widget>, 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: Clone + AsRef<gtk4::Widget>> {
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,
sender: &mut Sender<Self::Input>,
out_sender: &mut Sender<Self::Output>,
) -> (Self::Widgets, Self::RootWidget);
/// Handles input messages and enables the programmer to update the model and view.
fn update(
&mut self,
widgets: &mut Self::Widgets,
event: Self::Input,
sender: &mut Sender<Self::Input>,
outbound: &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);
}
Creation of an InfoButton component:
// Copyright 2022 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0
use ccs::*;
use gtk::prelude::*;
use gtk4 as gtk;
pub enum InfoButtonInput {
SetDescription(String),
}
pub enum InfoButtonOutput {
Clicked,
}
pub struct InfoButtonWidgets {
description: gtk::Label,
}
#[derive(Default)]
pub struct InfoButton;
impl Component for InfoButton {
type InitialArgs = (String, String, gtk::SizeGroup);
type RootWidget = gtk::Box;
type Input = InfoButtonInput;
type Output = InfoButtonOutput;
type Widgets = InfoButtonWidgets;
fn init_view(
&mut self,
(desc, button_label, sg): Self::InitialArgs,
_sender: &mut Sender<InfoButtonInput>,
out_sender: &mut Sender<InfoButtonOutput>,
) -> (InfoButtonWidgets, gtk::Box) {
relm4_macros::view! {
root = info_box() -> gtk::Box {
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(out_sender) => move |_| {
let _ = out_sender.send(InfoButtonOutput::Clicked);
}
}
}
}
sg.add_widget(&button);
(InfoButtonWidgets { description }, root)
}
fn update(
&mut self,
widgets: &mut InfoButtonWidgets,
event: InfoButtonInput,
_sender: &mut Sender<InfoButtonInput>,
_out_sender: &mut Sender<InfoButtonOutput>,
) {
match event {
InfoButtonInput::SetDescription(value) => {
widgets.description.set_text(&value);
}
}
}
}
pub fn info_box() -> gtk::Box {
relm4_macros::view! {
container = 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
}
}
container
}
Creating and maintaining InfoButton components inside of an App component
// Copyright 2022 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0
use crate::components::{InfoButton, InfoButtonInput, InfoButtonOutput};
use ccs::*;
use gtk::prelude::*;
use gtk4 as gtk;
/// The model where component state is stored.
#[derive(Default)]
pub struct App {
pub counter: usize,
}
/// Widgets that are initialized in the view.
pub struct AppWidgets {
list: gtk::ListBox,
destroyable: Option<Handle<gtk::Box, InfoButtonInput>>,
counter: Handle<gtk::Box, InfoButtonInput>,
}
/// An input event that is used to update the model.
pub enum AppEvent {
Destroy,
Increment,
}
/// Components are the glue that wrap everything together.
impl Component for App {
type InitialArgs = gtk::Application;
type Input = AppEvent;
type Output = ();
type Widgets = AppWidgets;
type RootWidget = gtk::ApplicationWindow;
fn init_view(
&mut self,
app: gtk::Application,
sender: &mut Sender<AppEvent>,
_out_sender: &mut Sender<()>,
) -> (AppWidgets, gtk::ApplicationWindow) {
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(sender.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(sender.clone(), |event| match event {
InfoButtonOutput::Clicked => AppEvent::Increment,
});
// Construct the view for this component, attaching the component's widget.
relm4_macros::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(
&mut self,
widgets: &mut AppWidgets,
event: AppEvent,
_sender: &mut Sender<AppEvent>,
_outbound: &mut Sender<()>,
) {
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);
}
}
}
}
}
}
With application launched via
// Copyright 2022 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0
extern crate cosmic_component_system as ccs;
mod components;
use self::components::App;
use ccs::Component;
use gtk4 as gtk;
fn main() {
ccs::run(|app| {
App::default().register(app.clone());
});
}