Gleam + Relm4 foreign node POC over Erlang distribution
1//! Relm4 + libadwaita counter window, driven by distribution messages.
2
3use relm4::gtk;
4use relm4::gtk::glib;
5use relm4::gtk::prelude::*;
6use relm4::prelude::*;
7use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent};
8
9// libadwaita widget traits (AdwApplicationWindowExt, etc.)
10use relm4::adw::prelude::*;
11
12use crate::dist::Outbound;
13use crate::OUTBOUND;
14
15#[derive(Debug)]
16pub enum AppMsg {
17 /// Controller handshake / first message received.
18 Connected,
19 SetTitle(String),
20 SetLabel(String),
21 SetCount(i64),
22 /// UI-local: increment and notify controller.
23 Increment,
24 Decrement,
25 /// Dist asked for pong.
26 PongRequested,
27 DistDied(String),
28 Quit,
29}
30
31pub struct AppModel {
32 title: String,
33 label: String,
34 count: i64,
35 status: String,
36}
37
38#[relm4::component(pub)]
39impl SimpleComponent for AppModel {
40 type Init = ();
41 type Input = AppMsg;
42 type Output = ();
43
44 view! {
45 #[root]
46 adw::ApplicationWindow {
47 #[watch]
48 set_title: Some(&model.title),
49 set_default_width: 420,
50 set_default_height: 280,
51
52 connect_close_request[sender] => move |_| {
53 emit_outbound(Outbound::WindowClosed);
54 sender.input(AppMsg::Quit);
55 glib::Propagation::Proceed
56 },
57
58 #[wrap(Some)]
59 set_content = &adw::ToolbarView {
60 add_top_bar = &adw::HeaderBar {},
61
62 #[wrap(Some)]
63 set_content = >k::Box {
64 set_orientation: gtk::Orientation::Vertical,
65 set_spacing: 16,
66 set_margin_all: 24,
67 set_halign: gtk::Align::Center,
68 set_valign: gtk::Align::Center,
69
70 gtk::Label {
71 #[watch]
72 set_label: &model.label,
73 add_css_class: "title-1",
74 },
75
76 gtk::Label {
77 #[watch]
78 set_label: &format!("Count: {}", model.count),
79 add_css_class: "title-2",
80 },
81
82 gtk::Box {
83 set_orientation: gtk::Orientation::Horizontal,
84 set_spacing: 12,
85 set_halign: gtk::Align::Center,
86
87 gtk::Button {
88 set_label: "−",
89 add_css_class: "circular",
90 connect_clicked => AppMsg::Decrement,
91 },
92
93 gtk::Button {
94 set_label: "Click me",
95 add_css_class: "suggested-action",
96 add_css_class: "pill",
97 connect_clicked => AppMsg::Increment,
98 },
99
100 gtk::Button {
101 set_label: "+",
102 add_css_class: "circular",
103 connect_clicked => AppMsg::Increment,
104 },
105 },
106
107 gtk::Label {
108 #[watch]
109 set_label: &model.status,
110 add_css_class: "dim-label",
111 },
112 },
113 },
114 }
115 }
116
117 fn init(
118 _init: Self::Init,
119 root: Self::Root,
120 sender: ComponentSender<Self>,
121 ) -> ComponentParts<Self> {
122 let _ = &sender;
123 let model = AppModel {
124 title: "gleamtk".into(),
125 label: "Waiting for Gleam controller…".into(),
126 count: 0,
127 status: "foreign node up · registered as gui".into(),
128 };
129
130 let widgets = view_output!();
131 ComponentParts { model, widgets }
132 }
133
134 fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
135 match msg {
136 AppMsg::Connected => {
137 self.status = "controller connected".into();
138 }
139 AppMsg::SetTitle(t) => {
140 self.title = t;
141 }
142 AppMsg::SetLabel(l) => {
143 self.label = l;
144 }
145 AppMsg::SetCount(n) => {
146 self.count = n;
147 }
148 AppMsg::Increment => {
149 self.count += 1;
150 emit_outbound(Outbound::Clicked(self.count));
151 }
152 AppMsg::Decrement => {
153 self.count -= 1;
154 emit_outbound(Outbound::CountChanged(self.count));
155 }
156 AppMsg::PongRequested => {
157 emit_outbound(Outbound::Pong);
158 }
159 AppMsg::DistDied(err) => {
160 self.status = format!("dist error: {err}");
161 }
162 AppMsg::Quit => {
163 relm4::main_application().quit();
164 }
165 }
166 }
167}
168
169fn emit_outbound(msg: Outbound) {
170 if let Some(tx) = OUTBOUND.get() {
171 if let Err(e) = tx.try_send(msg) {
172 eprintln!("[app] outbound send failed: {e}");
173 }
174 }
175}