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: Option<mpsc::SyncSender<Vec<ServerMsg>>>,
40 edit_rx: Mutex<Option<mpsc::Receiver<(ElementId, Value)>>>,
43 incoming_edits: HashMap<ElementId, Value>,
45 shutdown: Arc<AtomicBool>,
47
48 prev_frame: Vec<ElementDecl>,
49 current_frame: Vec<ElementDecl>,
50 http_port: u16,
51 ws_port: u16,
52 _http_handle: Option<JoinHandle<()>>,
53 _ws_handle: Option<JoinHandle<()>>,
54}
55
56impl Context {
57 pub fn new() -> Self {
59 Self::with_options(ContextOptions::default())
60 }
61
62 pub fn with_port(start_port: u16) -> Self {
64 Self::with_options(ContextOptions {
65 start_port,
66 ..Default::default()
67 })
68 }
69
70 pub fn with_options(opts: ContextOptions) -> Self {
74 let bind_addr = if opts.public { "0.0.0.0" } else { "127.0.0.1" };
75
76 if let Some((http_listener, ws_listener)) = server::find_port_pair(opts.start_port, bind_addr) {
77 let http_port = http_listener.local_addr().map(|a| a.port()).unwrap_or(0);
78 let ws_port = ws_listener.local_addr().map(|a| a.port()).unwrap_or(0);
79
80 let (ws_tx, ws_rx) = mpsc::sync_channel::<Vec<ServerMsg>>(2);
82 let (edit_tx, edit_rx) = mpsc::channel::<(ElementId, Value)>();
83 let shutdown = Arc::new(AtomicBool::new(false));
84
85 let http_handle =
86 server::spawn_http(shutdown.clone(), http_listener, &opts.title, opts.favicon);
87 let ws_handle = server::spawn_ws(ws_rx, edit_tx, ws_listener, shutdown.clone());
88
89 println!("wgui: UI available at http://{bind_addr}:{http_port}");
90
91 Self {
92 ws_tx: Some(ws_tx),
93 edit_rx: Mutex::new(Some(edit_rx)),
94 incoming_edits: HashMap::new(),
95 shutdown,
96 prev_frame: Vec::new(),
97 current_frame: Vec::new(),
98 http_port,
99 ws_port,
100 _http_handle: Some(http_handle),
101 _ws_handle: Some(ws_handle),
102 }
103 } else {
104 log::warn!("wgui: running in headless mode (no free ports)");
105 Self {
106 ws_tx: None,
107 edit_rx: Mutex::new(None),
108 incoming_edits: HashMap::new(),
109 shutdown: Arc::new(AtomicBool::new(false)),
110 prev_frame: Vec::new(),
111 current_frame: Vec::new(),
112 http_port: 0,
113 ws_port: 0,
114 _http_handle: None,
115 _ws_handle: None,
116 }
117 }
118 }
119
120 pub fn http_port(&self) -> u16 {
122 self.http_port
123 }
124
125 pub fn ws_port(&self) -> u16 {
127 self.ws_port
128 }
129
130 pub fn window(&mut self, name: &str) -> Window<'_> {
132 Window::new(name.to_string(), self)
133 }
134
135 pub(crate) fn consume_edit(&mut self, id: &str) -> Option<Value> {
137 let rx = self.edit_rx.lock().unwrap();
139 if let Some(ref channel) = *rx {
140 while let Ok((elem_id, value)) = channel.try_recv() {
141 self.incoming_edits.insert(elem_id, value);
142 }
143 }
144 drop(rx);
145 self.incoming_edits.remove(id)
146 }
147
148 pub(crate) fn declare(&mut self, decl: ElementDecl) {
150 self.current_frame.push(decl);
151 }
152
153 pub fn end_frame(&mut self) {
156 let outgoing = reconcile(&self.prev_frame, &self.current_frame);
157
158 if !outgoing.is_empty() {
159 if let Some(ref tx) = self.ws_tx {
160 match tx.try_send(outgoing) {
161 Ok(()) => {}
162 Err(mpsc::TrySendError::Full(_)) => {
163 log::debug!("wgui: WS channel backpressure, skipping frame update");
164 }
165 Err(mpsc::TrySendError::Disconnected(_)) => {
166 log::debug!("wgui: WS thread disconnected");
167 }
168 }
169 }
170 }
171
172 self.prev_frame = std::mem::take(&mut self.current_frame);
174 }
175}
176
177impl Default for Context {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183impl Drop for Context {
184 fn drop(&mut self) {
185 self.shutdown.store(true, Ordering::Release);
186 if let Some(handle) = self._http_handle.take() {
192 let _ = handle.join();
193 }
194 if let Some(handle) = self._ws_handle.take() {
195 let _ = handle.join();
196 }
197 }
198}
199
200fn reconcile(prev: &[ElementDecl], current: &[ElementDecl]) -> Vec<ServerMsg> {
203 let mut outgoing = Vec::new();
204
205 let prev_index: HashMap<&str, usize> = prev
207 .iter()
208 .enumerate()
209 .map(|(i, d)| (d.id.as_str(), i))
210 .collect();
211
212 for decl in current {
214 match prev_index.get(decl.id.as_str()) {
215 None => {
216 outgoing.push(ServerMsg::Add {
217 element: decl.clone(),
218 });
219 }
220 Some(&idx) => {
221 let prev_decl = &prev[idx];
222 let value_changed = prev_decl.value != decl.value || prev_decl.kind != decl.kind || prev_decl.label != decl.label;
223 let meta_changed = prev_decl.meta != decl.meta;
224 let label_changed = prev_decl.label != decl.label;
225 if value_changed || meta_changed || label_changed {
226 outgoing.push(ServerMsg::Update {
227 id: decl.id.clone(),
228 value: decl.value.clone(),
229 label: if label_changed {
230 Some(decl.label.clone())
231 } else {
232 None
233 },
234 meta: if meta_changed {
235 Some(decl.meta.clone())
236 } else {
237 None
238 },
239 });
240 }
241 }
242 }
243 }
244
245 let current_ids: HashSet<&str> = current.iter().map(|d| d.id.as_str()).collect();
247 for prev_decl in prev {
248 if !current_ids.contains(prev_decl.id.as_str()) {
249 outgoing.push(ServerMsg::Remove {
250 id: prev_decl.id.clone(),
251 });
252 }
253 }
254
255 let mut prev_order: HashMap<&str, Vec<&str>> = HashMap::new();
266 for d in prev {
267 prev_order.entry(d.window.as_ref()).or_default().push(&d.id);
268 }
269 let mut curr_windows: Vec<&str> = Vec::new();
270 let mut curr_order: HashMap<&str, Vec<&str>> = HashMap::new();
271 for d in current {
272 let w = d.window.as_ref();
273 if !curr_order.contains_key(w) {
274 curr_windows.push(w);
275 }
276 curr_order.entry(w).or_default().push(&d.id);
277 }
278 let empty: Vec<&str> = Vec::new();
279 for win in curr_windows {
280 let desired = &curr_order[win];
281 let prev_ids = prev_order.get(win).unwrap_or(&empty);
282 let curr_set: HashSet<&str> = desired.iter().copied().collect();
283 let prev_set: HashSet<&str> = prev_ids.iter().copied().collect();
284 let mut predicted: Vec<&str> = prev_ids.iter().copied().filter(|id| curr_set.contains(id)).collect();
286 predicted.extend(desired.iter().copied().filter(|id| !prev_set.contains(id)));
287 if &predicted != desired {
288 outgoing.push(ServerMsg::Reorder {
289 window: win.to_string(),
290 ids: desired.iter().map(|s| s.to_string()).collect(),
291 });
292 }
293 }
294
295 outgoing
296}
297
298const _: () = {
299 fn _assert_send_sync<T: Send + Sync>() {}
300 fn _check() { _assert_send_sync::<Context>(); }
301};
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::element::{ElementKind, ElementMeta, Value};
307 use std::sync::Arc;
308
309 fn make_decl(id: &str, value: Value) -> ElementDecl {
310 ElementDecl {
311 id: id.to_string(),
312 kind: ElementKind::Label,
313 label: id.to_string(),
314 value,
315 meta: ElementMeta::default(),
316 window: Arc::from("test"),
317 }
318 }
319
320 #[test]
321 fn reconcile_detects_additions() {
322 let msgs = reconcile(&[], &[make_decl("a", Value::Bool(true))]);
323 assert_eq!(msgs.len(), 1);
324 assert!(matches!(&msgs[0], ServerMsg::Add { element } if element.id == "a"));
325 }
326
327 #[test]
328 fn reconcile_detects_removals() {
329 let msgs = reconcile(&[make_decl("a", Value::Bool(true))], &[]);
330 assert_eq!(msgs.len(), 1);
331 assert!(matches!(&msgs[0], ServerMsg::Remove { id } if id == "a"));
332 }
333
334 #[test]
335 fn reconcile_detects_updates() {
336 let prev = vec![make_decl("a", Value::Bool(true))];
337 let current = vec![make_decl("a", Value::Bool(false))];
338 let msgs = reconcile(&prev, ¤t);
339 assert_eq!(msgs.len(), 1);
340 assert!(matches!(&msgs[0], ServerMsg::Update { id, .. } if id == "a"));
341 }
342
343 #[test]
344 fn reconcile_unchanged() {
345 let prev = vec![make_decl("a", Value::Bool(true))];
346 let current = vec![make_decl("a", Value::Bool(true))];
347 assert!(reconcile(&prev, ¤t).is_empty());
348 }
349
350 #[test]
351 fn reconcile_mixed() {
352 let prev = vec![
353 make_decl("keep", Value::Bool(true)),
354 make_decl("update", Value::Float(1.0)),
355 make_decl("remove", Value::Bool(false)),
356 ];
357 let current = vec![
358 make_decl("keep", Value::Bool(true)),
359 make_decl("update", Value::Float(2.0)),
360 make_decl("add", Value::Bool(true)),
361 ];
362 let msgs = reconcile(&prev, ¤t);
363 assert_eq!(msgs.len(), 3); }
365}