1use std::collections::{HashMap, HashSet};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::mpsc;
4use std::sync::{Arc, Mutex};
5use std::thread::JoinHandle;
6
7use crate::element::{ElementDecl, ElementId, Value};
8use crate::protocol::ServerMsg;
9use crate::server;
10use crate::window::Window;
11
12pub struct ContextOptions {
14 pub start_port: u16,
16 pub title: String,
18 pub favicon: Option<Vec<u8>>,
20 pub public: bool,
23}
24
25impl Default for ContextOptions {
26 fn default() -> Self {
27 Self {
28 start_port: 9080,
29 title: "wgui".to_string(),
30 favicon: None,
31 public: false,
32 }
33 }
34}
35
36pub struct Context {
37 ws_tx: mpsc::SyncSender<Vec<ServerMsg>>,
39 edit_rx: Mutex<mpsc::Receiver<(ElementId, Value)>>,
41 incoming_edits: HashMap<ElementId, Value>,
43 shutdown: Arc<AtomicBool>,
45
46 prev_frame: Vec<ElementDecl>,
47 current_frame: Vec<ElementDecl>,
48 http_port: u16,
49 ws_port: u16,
50 _http_handle: JoinHandle<()>,
51 _ws_handle: JoinHandle<()>,
52}
53
54impl Context {
55 pub fn new() -> Self {
57 Self::with_options(ContextOptions::default())
58 }
59
60 pub fn with_port(start_port: u16) -> Self {
62 Self::with_options(ContextOptions {
63 start_port,
64 ..Default::default()
65 })
66 }
67
68 pub fn with_options(opts: ContextOptions) -> Self {
70 let bind_addr = if opts.public { "0.0.0.0" } else { "127.0.0.1" };
71 let (http_port, ws_port) = server::find_port_pair(opts.start_port, bind_addr);
72
73 let (ws_tx, ws_rx) = mpsc::sync_channel::<Vec<ServerMsg>>(2);
75 let (edit_tx, edit_rx) = mpsc::channel::<(ElementId, Value)>();
76 let shutdown = Arc::new(AtomicBool::new(false));
77
78 let http_handle =
79 server::spawn_http(shutdown.clone(), http_port, bind_addr, &opts.title, opts.favicon);
80 let ws_handle = server::spawn_ws(ws_rx, edit_tx, ws_port, bind_addr);
81
82 println!("wgui: UI available at http://{bind_addr}:{http_port}");
83
84 Self {
85 ws_tx,
86 edit_rx: Mutex::new(edit_rx),
87 incoming_edits: HashMap::new(),
88 shutdown,
89 prev_frame: Vec::new(),
90 current_frame: Vec::new(),
91 http_port,
92 ws_port,
93 _http_handle: http_handle,
94 _ws_handle: ws_handle,
95 }
96 }
97
98 pub fn http_port(&self) -> u16 {
100 self.http_port
101 }
102
103 pub fn ws_port(&self) -> u16 {
105 self.ws_port
106 }
107
108 pub fn window(&mut self, name: &str) -> Window<'_> {
110 Window::new(name.to_string(), self)
111 }
112
113 pub(crate) fn consume_edit(&mut self, id: &str) -> Option<Value> {
115 let rx = self.edit_rx.lock().unwrap();
117 while let Ok((elem_id, value)) = rx.try_recv() {
118 self.incoming_edits.insert(elem_id, value);
119 }
120 drop(rx);
121 self.incoming_edits.remove(id)
122 }
123
124 pub(crate) fn declare(&mut self, decl: ElementDecl) {
126 self.current_frame.push(decl);
127 }
128
129 pub fn end_frame(&mut self) {
131 let outgoing = reconcile(&self.prev_frame, &self.current_frame);
132
133 if !outgoing.is_empty() {
134 match self.ws_tx.try_send(outgoing) {
135 Ok(()) => {}
136 Err(mpsc::TrySendError::Full(_)) => {
137 log::warn!("wgui: WS channel backpressure, skipping frame update");
138 }
139 Err(mpsc::TrySendError::Disconnected(_)) => {
140 log::warn!("wgui: WS thread disconnected");
141 }
142 }
143 }
144
145 self.prev_frame = std::mem::take(&mut self.current_frame);
147 }
148}
149
150impl Default for Context {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl Drop for Context {
157 fn drop(&mut self) {
158 self.shutdown.store(true, Ordering::Release);
159 }
160}
161
162fn reconcile(prev: &[ElementDecl], current: &[ElementDecl]) -> Vec<ServerMsg> {
165 let mut outgoing = Vec::new();
166
167 let prev_index: HashMap<&str, usize> = prev
169 .iter()
170 .enumerate()
171 .map(|(i, d)| (d.id.as_str(), i))
172 .collect();
173
174 for decl in current {
176 match prev_index.get(decl.id.as_str()) {
177 None => {
178 outgoing.push(ServerMsg::Add {
179 element: decl.clone(),
180 });
181 }
182 Some(&idx) => {
183 let prev_decl = &prev[idx];
184 let value_changed = prev_decl.value != decl.value || prev_decl.kind != decl.kind || prev_decl.label != decl.label;
185 let meta_changed = prev_decl.meta != decl.meta;
186 let label_changed = prev_decl.label != decl.label;
187 if value_changed || meta_changed || label_changed {
188 outgoing.push(ServerMsg::Update {
189 id: decl.id.clone(),
190 value: decl.value.clone(),
191 label: if label_changed {
192 Some(decl.label.clone())
193 } else {
194 None
195 },
196 meta: if meta_changed {
197 Some(decl.meta.clone())
198 } else {
199 None
200 },
201 });
202 }
203 }
204 }
205 }
206
207 let current_ids: HashSet<&str> = current.iter().map(|d| d.id.as_str()).collect();
209 for prev_decl in prev {
210 if !current_ids.contains(prev_decl.id.as_str()) {
211 outgoing.push(ServerMsg::Remove {
212 id: prev_decl.id.clone(),
213 });
214 }
215 }
216
217 let has_structural = outgoing.iter().any(|m| matches!(m, ServerMsg::Add { .. } | ServerMsg::Remove { .. }));
220 if !has_structural && !prev.is_empty() {
221 let mut prev_order: HashMap<&str, Vec<&str>> = HashMap::new();
223 let mut curr_order: HashMap<&str, Vec<&str>> = HashMap::new();
224 for d in prev {
225 prev_order.entry(d.window.as_ref()).or_default().push(&d.id);
226 }
227 for d in current {
228 curr_order.entry(d.window.as_ref()).or_default().push(&d.id);
229 }
230 for (win, curr_ids) in &curr_order {
231 if let Some(prev_ids) = prev_order.get(win) {
232 if prev_ids.len() == curr_ids.len() && prev_ids != curr_ids {
233 outgoing.push(ServerMsg::Reorder {
234 window: win.to_string(),
235 ids: curr_ids.iter().map(|s| s.to_string()).collect(),
236 });
237 }
238 }
239 }
240 }
241
242 outgoing
243}
244
245const _: () = {
246 fn _assert_send_sync<T: Send + Sync>() {}
247 fn _check() { _assert_send_sync::<Context>(); }
248};
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::element::{ElementKind, ElementMeta, Value};
254 use std::sync::Arc;
255
256 fn make_decl(id: &str, value: Value) -> ElementDecl {
257 ElementDecl {
258 id: id.to_string(),
259 kind: ElementKind::Label,
260 label: id.to_string(),
261 value,
262 meta: ElementMeta::default(),
263 window: Arc::from("test"),
264 }
265 }
266
267 #[test]
268 fn reconcile_detects_additions() {
269 let msgs = reconcile(&[], &[make_decl("a", Value::Bool(true))]);
270 assert_eq!(msgs.len(), 1);
271 assert!(matches!(&msgs[0], ServerMsg::Add { element } if element.id == "a"));
272 }
273
274 #[test]
275 fn reconcile_detects_removals() {
276 let msgs = reconcile(&[make_decl("a", Value::Bool(true))], &[]);
277 assert_eq!(msgs.len(), 1);
278 assert!(matches!(&msgs[0], ServerMsg::Remove { id } if id == "a"));
279 }
280
281 #[test]
282 fn reconcile_detects_updates() {
283 let prev = vec![make_decl("a", Value::Bool(true))];
284 let current = vec![make_decl("a", Value::Bool(false))];
285 let msgs = reconcile(&prev, ¤t);
286 assert_eq!(msgs.len(), 1);
287 assert!(matches!(&msgs[0], ServerMsg::Update { id, .. } if id == "a"));
288 }
289
290 #[test]
291 fn reconcile_unchanged() {
292 let prev = vec![make_decl("a", Value::Bool(true))];
293 let current = vec![make_decl("a", Value::Bool(true))];
294 assert!(reconcile(&prev, ¤t).is_empty());
295 }
296
297 #[test]
298 fn reconcile_mixed() {
299 let prev = vec![
300 make_decl("keep", Value::Bool(true)),
301 make_decl("update", Value::Float(1.0)),
302 make_decl("remove", Value::Bool(false)),
303 ];
304 let current = vec![
305 make_decl("keep", Value::Bool(true)),
306 make_decl("update", Value::Float(2.0)),
307 make_decl("add", Value::Bool(true)),
308 ];
309 let msgs = reconcile(&prev, ¤t);
310 assert_eq!(msgs.len(), 3); }
312}