1use std::sync::Arc;
2
3use tokio::runtime::Runtime;
4use tokio::sync::mpsc;
5
6use opcua::types::NodeId;
7
8use crate::client::{UaClient, parse_variant};
9use crate::messages::{UiAction, UiUpdate};
10use crate::model::{AppModel, ConnectionState, DetailTab, MethodCallState};
11use crate::types::{AuthSpec, EndpointInfo, ValueTree};
12
13#[derive(Debug, Clone, Copy)]
14pub enum FilePickTarget {
15 CertPath,
16 KeyPath,
17}
18
19pub trait FrontendCtx: Clone + Send + Sync + 'static {
20 fn request_repaint(&self);
21 fn set_clipboard(&self, text: &str);
22 fn pick_file(
23 &self,
24 rt: &Runtime,
25 update_tx: &mpsc::UnboundedSender<UiUpdate>,
26 target: FilePickTarget,
27 title: &str,
28 default_dir: &str,
29 );
30}
31
32pub struct Engine {
33 pub model: AppModel,
34 pub client: Arc<UaClient>,
35 pub rt: Runtime,
36 pub update_tx: mpsc::UnboundedSender<UiUpdate>,
37}
38
39impl Engine {
40 pub fn new(
41 rt: Runtime,
42 log_rx: mpsc::UnboundedReceiver<UiUpdate>,
43 ) -> (Self, mpsc::UnboundedReceiver<UiUpdate>) {
44 let (update_tx, update_rx) = mpsc::unbounded_channel();
45 forward_logs(log_rx, update_tx.clone());
46 let engine = Self {
47 model: AppModel::default(),
48 client: Arc::new(UaClient::new()),
49 rt,
50 update_tx,
51 };
52 (engine, update_rx)
53 }
54
55 pub fn apply_update<C: FrontendCtx>(&mut self, ctx: &C, update: UiUpdate) {
56 match update {
57 UiUpdate::ConnectStarted => self.model.connection = ConnectionState::Connecting,
58 UiUpdate::ConnectFinished(Ok(())) => {
59 self.model.connection = ConnectionState::Connected;
60 self.model.record_successful_connection();
61 tracing::info!("connected to {}", self.model.endpoint_url);
62 let saved = self
63 .model
64 .last_selection_paths
65 .get(&self.model.endpoint_url)
66 .cloned();
67 match saved {
68 Some(path) if !path.is_empty() => {
69 tracing::info!(
70 "restoring previous selection ({} ancestors)",
71 path.len()
72 );
73 self.spawn_restore_selection(ctx, path);
74 }
75 _ => {
76 let root = self.model.root_node.clone();
77 self.ensure_expanded(ctx, root);
78 }
79 }
80 }
81 UiUpdate::ConnectFinished(Err(e)) => {
82 self.model.connection = ConnectionState::Disconnected;
83 tracing::error!("connect failed: {e}");
84 }
85 UiUpdate::DisconnectStarted => self.model.connection = ConnectionState::Disconnecting,
86 UiUpdate::DisconnectFinished => {
87 self.model.connection = ConnectionState::Disconnected;
88 self.model.reset_session_state();
89 tracing::info!("disconnected");
90 }
91 UiUpdate::ChildrenLoaded { parent, children } => {
92 self.model.tree.loading.remove(&parent);
93 match children {
94 Ok(c) => {
95 self.model.tree.children.insert(parent.clone(), c);
96 self.model.tree.expanded.insert(parent);
97 }
98 Err(e) => tracing::error!("browse {parent} failed: {e}"),
99 }
100 }
101 UiUpdate::SummaryLoaded { node, summary } => {
102 if self.model.selected.as_ref() == Some(&node) {
103 match summary {
104 Ok(s) => self.model.node_summary = Some(s),
105 Err(e) => tracing::error!("read summary {node} failed: {e}"),
106 }
107 }
108 }
109 UiUpdate::ReferencesLoaded { node, refs } => {
110 if self.model.selected.as_ref() == Some(&node) {
111 self.model.references_loading = false;
112 match refs {
113 Ok(rs) => self.model.references = Some(rs),
114 Err(e) => tracing::error!("browse refs {node} failed: {e}"),
115 }
116 }
117 }
118 UiUpdate::SelectionPathResolved { url, path } => {
119 self.model.last_selection_paths.insert(url, path);
120 }
121 UiUpdate::RestoreSelection(node) => {
122 self.model.selected = Some(node.clone());
123 self.spawn_node_summary(ctx, node.clone());
124 if self.model.active_tab == DetailTab::References {
125 self.spawn_browse_references(ctx, node);
126 }
127 }
128 UiUpdate::PathReady { node, path } => match path {
129 Ok(p) => {
130 ctx.set_clipboard(&p);
131 tracing::info!("copied path: {p}");
132 }
133 Err(e) => tracing::error!("path for {node} failed: {e}"),
134 },
135 UiUpdate::CertPathPicked(p) => self.model.auth_cert_path = p,
136 UiUpdate::KeyPathPicked(p) => self.model.auth_key_path = p,
137 UiUpdate::FilePickerClosed => self.model.file_picker_open = false,
138 UiUpdate::EndpointsDiscovered { url, result } => {
139 if url != self.model.endpoint_url {
140 tracing::debug!("dropping endpoints result for stale url {url}");
141 } else {
142 self.model.endpoints_loading = false;
143 match result {
144 Ok(eps) => {
145 tracing::info!("discovered {} endpoint(s)", eps.len());
146 self.model.discovered_endpoints = Some(eps);
147 self.select_first_matching_endpoint();
148 }
149 Err(e) => {
150 tracing::error!("endpoint discovery failed: {e}");
151 self.model.discovered_endpoints = Some(Vec::new());
152 }
153 }
154 }
155 }
156 UiUpdate::MethodSignatureLoaded { node, result } => {
157 if !self.method_call_targets(&node) {
158 return;
159 }
160 match result {
161 Ok(signature) => {
162 let n_inputs = signature.inputs.len();
163 self.model.method_call = Some(MethodCallState::Inputs {
164 node,
165 signature,
166 edited: vec![String::new(); n_inputs],
167 field_errors: vec![None; n_inputs],
168 call_error: None,
169 });
170 }
171 Err(error) => {
172 tracing::error!("read method signature {node} failed: {error}");
173 self.model.method_call =
174 Some(MethodCallState::Failed { node, error });
175 }
176 }
177 }
178 UiUpdate::MethodCallFinished { node, result } => {
179 if !self.method_call_targets(&node) {
180 return;
181 }
182 let Some(MethodCallState::Calling {
183 node, signature, edited,
184 }) = self.model.method_call.take()
185 else {
186 return;
187 };
188 match result {
189 Ok(outcome) => {
190 self.model.method_call = Some(MethodCallState::Result {
191 node,
192 signature,
193 edited,
194 outcome,
195 });
196 }
197 Err(error) => {
198 tracing::error!("call method {node} failed: {error}");
199 let n_inputs = signature.inputs.len();
200 self.model.method_call = Some(MethodCallState::Inputs {
201 node,
202 signature,
203 edited,
204 field_errors: vec![None; n_inputs],
205 call_error: Some(error),
206 });
207 }
208 }
209 }
210 UiUpdate::Log(line) => self.model.push_log(line),
211 }
212 }
213
214 fn method_call_targets(&self, node: &NodeId) -> bool {
215 self.model
216 .method_call
217 .as_ref()
218 .map(|s| s.node() == node)
219 .unwrap_or(false)
220 }
221
222 pub fn dispatch<C: FrontendCtx>(&mut self, ctx: &C, action: UiAction) {
223 match action {
224 UiAction::EndpointEdited(s) => {
225 if s != self.model.endpoint_url {
226 self.model.endpoint_url = s;
227 self.model.discovered_endpoints = None;
228 self.model.selected_endpoint = None;
229 self.model.endpoints_loading = false;
230 self.model.apply_saved_connection_prefs();
231 }
232 }
233 UiAction::TabSelected(t) => {
234 self.model.active_tab = t;
235 if t == DetailTab::References
236 && let Some(node) = self.model.selected.clone()
237 && self.model.references.is_none()
238 && !self.model.references_loading
239 {
240 self.spawn_browse_references(ctx, node);
241 }
242 }
243 UiAction::ConnectClicked => {
244 if self.model.selected_endpoint.is_none() {
245 tracing::info!("no endpoint selected; opening picker");
246 self.open_endpoint_picker(ctx);
247 } else {
248 let ep = self.model.selected_endpoint.as_ref().unwrap();
249 tracing::info!(
250 "connecting with {} / {}",
251 ep.security_policy,
252 ep.security_mode.label()
253 );
254 self.spawn_connect(ctx);
255 }
256 }
257 UiAction::DisconnectClicked => self.spawn_disconnect(ctx),
258 UiAction::NodeToggleExpand(n) => self.toggle_expand(ctx, n),
259 UiAction::NodeSelected(n) => self.select_node(ctx, n),
260 UiAction::ClearSelection => {
261 self.model.selected = None;
262 self.model.node_summary = None;
263 self.model.references = None;
264 self.model.references_loading = false;
265 }
266 UiAction::RefreshClicked => {
267 if let Some(node) = self.model.selected.clone() {
268 self.spawn_node_summary(ctx, node.clone());
269 if self.model.active_tab == DetailTab::References {
270 self.spawn_browse_references(ctx, node);
271 }
272 }
273 }
274 UiAction::OpenEndpointPicker => {
275 self.open_endpoint_picker(ctx);
276 }
277 UiAction::CloseEndpointPicker => {
278 self.model.endpoints_dialog_open = false;
279 }
280 UiAction::ForceRefreshEndpoints => {
281 if !self.model.endpoints_loading {
282 self.spawn_discover_endpoints(ctx);
283 }
284 }
285 UiAction::SelectEndpoint(ep) => {
286 self.model.selected_endpoint = Some(ep);
287 }
288 UiAction::ClearSelectedEndpoint => {
289 self.model.selected_endpoint = None;
290 }
291 UiAction::SetAuthMode(mode) => self.model.auth_mode = mode,
292 UiAction::SetEndpointModeFilter(mode) => {
293 self.model.endpoint_mode_filter = mode;
294 self.select_first_matching_endpoint();
295 }
296 UiAction::AuthUsernameEdited(s) => self.model.auth_username = s,
297 UiAction::AuthPasswordEdited(s) => self.model.auth_password = s,
298 UiAction::AuthCertPathEdited(s) => self.model.auth_cert_path = s,
299 UiAction::AuthKeyPathEdited(s) => self.model.auth_key_path = s,
300 UiAction::PickAuthCertPath => {
301 if !self.model.file_picker_open {
302 self.model.file_picker_open = true;
303 let default_dir = self.model.auth_cert_path.clone();
304 ctx.pick_file(
305 &self.rt,
306 &self.update_tx,
307 FilePickTarget::CertPath,
308 "Pick client certificate",
309 &default_dir,
310 );
311 }
312 }
313 UiAction::PickAuthKeyPath => {
314 if !self.model.file_picker_open {
315 self.model.file_picker_open = true;
316 let default_dir = self.model.auth_key_path.clone();
317 ctx.pick_file(
318 &self.rt,
319 &self.update_tx,
320 FilePickTarget::KeyPath,
321 "Pick private key",
322 &default_dir,
323 );
324 }
325 }
326 UiAction::CopyPath(node) => self.spawn_browse_path(ctx, node),
327 UiAction::CopyNodeId(node) => {
328 let text = node.to_string();
329 ctx.set_clipboard(&text);
330 tracing::info!("copied node id: {text}");
331 }
332 UiAction::CopyNodeValue => {
333 let Some(summary) = self.model.node_summary.as_ref() else {
334 tracing::warn!("no node summary loaded; nothing to copy");
335 return;
336 };
337 match summary.attributes.iter().find(|a| a.name == "Value") {
338 Some(attr) => {
339 let text = render_value_for_clipboard(&attr.value);
340 ctx.set_clipboard(&text);
341 tracing::info!("copied value of {}", summary.node_id);
342 }
343 None => tracing::warn!(
344 "selected node {} has no Value attribute",
345 summary.node_id
346 ),
347 }
348 }
349 UiAction::ConfirmConnect => {
350 if self.model.selected_endpoint.is_some() {
351 self.model.endpoints_dialog_open = false;
352 self.spawn_connect(ctx);
353 } else {
354 tracing::warn!("ConfirmConnect with no endpoint selected");
355 }
356 }
357 UiAction::OpenMethodCall(node) => self.open_method_call(ctx, node),
358 UiAction::CloseMethodCall => {
359 self.model.method_call = None;
360 }
361 UiAction::MethodArgEdited { index, value } => match self.model.method_call.as_mut() {
362 Some(MethodCallState::Inputs { edited, call_error, field_errors, .. }) => {
363 if let Some(slot) = edited.get_mut(index) {
364 *slot = value;
365 *call_error = None;
366 if let Some(err_slot) = field_errors.get_mut(index) {
367 *err_slot = None;
368 }
369 }
370 }
371 Some(MethodCallState::Result { edited, .. }) => {
372 if let Some(slot) = edited.get_mut(index) {
373 *slot = value;
374 }
375 }
376 _ => {}
377 },
378 UiAction::CallMethodConfirmed => self.confirm_method_call(ctx),
379 }
380 }
381
382 fn open_method_call<C: FrontendCtx>(&mut self, ctx: &C, node: NodeId) {
383 self.model.method_call = Some(MethodCallState::Loading { node: node.clone() });
384 self.spawn_method_signature(ctx, node);
385 }
386
387 fn confirm_method_call<C: FrontendCtx>(&mut self, ctx: &C) {
388 let (node, signature, edited) = match self.model.method_call.as_ref() {
389 Some(MethodCallState::Inputs {
390 node, signature, edited, ..
391 })
392 | Some(MethodCallState::Result {
393 node, signature, edited, ..
394 }) => (node.clone(), signature.clone(), edited.clone()),
395 _ => return,
396 };
397
398 let mut variants = Vec::with_capacity(signature.inputs.len());
399 let mut field_errors = vec![None; signature.inputs.len()];
400 let mut any_error = false;
401 for (i, arg) in signature.inputs.iter().enumerate() {
402 let s = edited.get(i).cloned().unwrap_or_default();
403 match parse_variant(&s, &arg.data_type, arg.value_rank) {
404 Ok(v) => variants.push(v),
405 Err(e) => {
406 field_errors[i] = Some(e);
407 any_error = true;
408 }
409 }
410 }
411 if any_error {
412 self.model.method_call = Some(MethodCallState::Inputs {
413 node,
414 signature,
415 edited,
416 field_errors,
417 call_error: None,
418 });
419 return;
420 }
421
422 let parent = signature.parent_object.clone();
423 let method = signature.method_node.clone();
424 self.model.method_call = Some(MethodCallState::Calling {
425 node: node.clone(),
426 signature,
427 edited,
428 });
429 self.spawn_method_call(ctx, parent, method, variants, node);
430 }
431
432 fn spawn_method_signature<C: FrontendCtx>(&self, ctx: &C, node: NodeId) {
433 let client = self.client.clone();
434 let tx = self.update_tx.clone();
435 let ctx = ctx.clone();
436 self.rt.spawn(async move {
437 let result = client
438 .read_method_signature(&node)
439 .await
440 .map_err(|e| e.to_string());
441 let _ = tx.send(UiUpdate::MethodSignatureLoaded { node, result });
442 ctx.request_repaint();
443 });
444 }
445
446 fn spawn_method_call<C: FrontendCtx>(
447 &self,
448 ctx: &C,
449 parent: NodeId,
450 method: NodeId,
451 inputs: Vec<opcua::types::Variant>,
452 node: NodeId,
453 ) {
454 let client = self.client.clone();
455 let tx = self.update_tx.clone();
456 let ctx = ctx.clone();
457 self.rt.spawn(async move {
458 let result = client
459 .call_method(&parent, &method, inputs)
460 .await
461 .map_err(|e| e.to_string());
462 let _ = tx.send(UiUpdate::MethodCallFinished { node, result });
463 ctx.request_repaint();
464 });
465 }
466
467 fn toggle_expand<C: FrontendCtx>(&mut self, ctx: &C, node: NodeId) {
468 if self.model.tree.expanded.contains(&node) {
469 self.model.tree.expanded.remove(&node);
470 } else if self.model.tree.children.contains_key(&node) {
471 self.model.tree.expanded.insert(node);
472 } else if !self.model.tree.loading.contains(&node) {
473 self.model.tree.loading.insert(node.clone());
474 self.spawn_browse_children(ctx, node);
475 }
476 }
477
478 fn ensure_expanded<C: FrontendCtx>(&mut self, ctx: &C, node: NodeId) {
479 if self.model.tree.expanded.contains(&node) {
480 return;
481 }
482 if self.model.tree.children.contains_key(&node) {
483 self.model.tree.expanded.insert(node);
484 } else if !self.model.tree.loading.contains(&node) {
485 self.model.tree.loading.insert(node.clone());
486 self.spawn_browse_children(ctx, node);
487 }
488 }
489
490 fn select_node<C: FrontendCtx>(&mut self, ctx: &C, node: NodeId) {
491 self.model.selected = Some(node.clone());
492 self.model.node_summary = None;
493 self.model.references = None;
494 self.spawn_node_summary(ctx, node.clone());
495 if self.model.active_tab == DetailTab::References {
496 self.spawn_browse_references(ctx, node.clone());
497 }
498 self.spawn_resolve_path(ctx, node);
499 }
500
501 fn select_first_matching_endpoint(&mut self) {
502 if let Some(eps) = self.model.discovered_endpoints.as_ref() {
503 let mut filtered: Vec<&EndpointInfo> = eps
504 .iter()
505 .filter(|e| e.security_mode == self.model.endpoint_mode_filter)
506 .collect();
507 filtered.sort_by(|a, b| b.security_level.cmp(&a.security_level));
508 self.model.selected_endpoint = filtered.first().map(|&e| e.clone());
509 }
510 }
511
512 fn open_endpoint_picker<C: FrontendCtx>(&mut self, ctx: &C) {
513 self.model.endpoints_dialog_open = true;
514 if self.model.discovered_endpoints.is_none() && !self.model.endpoints_loading {
515 self.spawn_discover_endpoints(ctx);
516 }
517 }
518
519 fn spawn_resolve_path<C: FrontendCtx>(&self, ctx: &C, node: NodeId) {
520 let client = self.client.clone();
521 let tx = self.update_tx.clone();
522 let url = self.model.endpoint_url.clone();
523 let ctx = ctx.clone();
524 self.rt.spawn(async move {
525 match client.node_path(&node).await {
526 Ok(path) => {
527 let _ = tx.send(UiUpdate::SelectionPathResolved { url, path });
528 ctx.request_repaint();
529 }
530 Err(e) => tracing::debug!("node_path for {node} failed: {e}"),
531 }
532 });
533 }
534
535 pub fn navigate_to_textual_path<C: FrontendCtx>(&self, ctx: &C, path: String) {
536 let client = self.client.clone();
537 let tx = self.update_tx.clone();
538 let ctx = ctx.clone();
539 self.rt.spawn(async move {
540 let target = match client.resolve_browse_path(&path).await {
541 Ok(t) => t,
542 Err(e) => {
543 tracing::warn!("resolve path '{path}' failed: {e}");
544 return;
545 }
546 };
547 let chain = match client.node_path(&target).await {
548 Ok(c) => c,
549 Err(e) => {
550 tracing::warn!("node_path for '{path}' failed: {e}");
551 return;
552 }
553 };
554 if chain.is_empty() {
555 return;
556 }
557 let final_target = chain.last().cloned().unwrap();
558 for parent in chain.iter().take(chain.len() - 1) {
559 match client.browse_children(parent).await {
560 Ok(children) => {
561 let _ = tx.send(UiUpdate::ChildrenLoaded {
562 parent: parent.clone(),
563 children: Ok(children),
564 });
565 }
566 Err(e) => {
567 tracing::warn!("navigate: browse_children({parent}) failed: {e}");
568 ctx.request_repaint();
569 return;
570 }
571 }
572 }
573 let _ = tx.send(UiUpdate::RestoreSelection(final_target));
574 ctx.request_repaint();
575 });
576 }
577
578 fn spawn_restore_selection<C: FrontendCtx>(&self, ctx: &C, path: Vec<NodeId>) {
579 let client = self.client.clone();
580 let tx = self.update_tx.clone();
581 let ctx = ctx.clone();
582 self.rt.spawn(async move {
583 if path.is_empty() {
584 return;
585 }
586 let target = path.last().cloned().unwrap();
587 for parent in path.iter().take(path.len() - 1) {
588 match client.browse_children(parent).await {
589 Ok(children) => {
590 let _ = tx.send(UiUpdate::ChildrenLoaded {
591 parent: parent.clone(),
592 children: Ok(children),
593 });
594 }
595 Err(e) => {
596 tracing::warn!("restore: browse_children({parent}) failed: {e}");
597 ctx.request_repaint();
598 return;
599 }
600 }
601 }
602 let _ = tx.send(UiUpdate::RestoreSelection(target));
603 ctx.request_repaint();
604 });
605 }
606
607 fn spawn_connect<C: FrontendCtx>(&mut self, ctx: &C) {
608 let client = self.client.clone();
609 let tx = self.update_tx.clone();
610 let url = self.model.endpoint_url.clone();
611 let endpoint = self.model.selected_endpoint.clone();
612 let auth = AuthSpec {
613 mode: self.model.auth_mode,
614 username: self.model.auth_username.clone(),
615 password: self.model.auth_password.clone(),
616 cert_path: self.model.auth_cert_path.clone(),
617 key_path: self.model.auth_key_path.clone(),
618 };
619 let ctx = ctx.clone();
620 let _ = tx.send(UiUpdate::ConnectStarted);
621 self.rt.spawn(async move {
622 let r = client
623 .connect(&url, endpoint.as_ref(), &auth)
624 .await
625 .map_err(|e| e.to_string());
626 let _ = tx.send(UiUpdate::ConnectFinished(r));
627 ctx.request_repaint();
628 });
629 }
630
631 fn spawn_discover_endpoints<C: FrontendCtx>(&mut self, ctx: &C) {
632 self.model.endpoints_loading = true;
633 self.model.discovered_endpoints = None;
634 let client = self.client.clone();
635 let tx = self.update_tx.clone();
636 let url = self.model.endpoint_url.clone();
637 let ctx = ctx.clone();
638 self.rt.spawn(async move {
639 let r = client
640 .discover_endpoints(&url)
641 .await
642 .map_err(|e| e.to_string());
643 let _ = tx.send(UiUpdate::EndpointsDiscovered { url, result: r });
644 ctx.request_repaint();
645 });
646 }
647
648 fn spawn_disconnect<C: FrontendCtx>(&self, ctx: &C) {
649 let client = self.client.clone();
650 let tx = self.update_tx.clone();
651 let ctx = ctx.clone();
652 let _ = tx.send(UiUpdate::DisconnectStarted);
653 self.rt.spawn(async move {
654 if let Err(e) = client.disconnect().await {
655 tracing::warn!("disconnect: {e}");
656 }
657 let _ = tx.send(UiUpdate::DisconnectFinished);
658 ctx.request_repaint();
659 });
660 }
661
662 fn spawn_browse_children<C: FrontendCtx>(&self, ctx: &C, node: NodeId) {
663 let client = self.client.clone();
664 let tx = self.update_tx.clone();
665 let ctx = ctx.clone();
666 self.rt.spawn(async move {
667 let r = client.browse_children(&node).await.map_err(|e| e.to_string());
668 let _ = tx.send(UiUpdate::ChildrenLoaded {
669 parent: node,
670 children: r,
671 });
672 ctx.request_repaint();
673 });
674 }
675
676 fn spawn_node_summary<C: FrontendCtx>(&self, ctx: &C, node: NodeId) {
677 let client = self.client.clone();
678 let tx = self.update_tx.clone();
679 let ctx = ctx.clone();
680 self.rt.spawn(async move {
681 let r = client
682 .read_node_summary(&node)
683 .await
684 .map_err(|e| e.to_string());
685 let _ = tx.send(UiUpdate::SummaryLoaded { node, summary: r });
686 ctx.request_repaint();
687 });
688 }
689
690 fn spawn_browse_path<C: FrontendCtx>(&self, ctx: &C, node: NodeId) {
691 let client = self.client.clone();
692 let tx = self.update_tx.clone();
693 let ctx = ctx.clone();
694 self.rt.spawn(async move {
695 let r = client.browse_path(&node).await.map_err(|e| e.to_string());
696 let _ = tx.send(UiUpdate::PathReady { node, path: r });
697 ctx.request_repaint();
698 });
699 }
700
701 fn spawn_browse_references<C: FrontendCtx>(&mut self, ctx: &C, node: NodeId) {
702 self.model.references_loading = true;
703 let client = self.client.clone();
704 let tx = self.update_tx.clone();
705 let ctx = ctx.clone();
706 self.rt.spawn(async move {
707 let r = client
708 .browse_references(&node)
709 .await
710 .map_err(|e| e.to_string());
711 let _ = tx.send(UiUpdate::ReferencesLoaded { node, refs: r });
712 ctx.request_repaint();
713 });
714 }
715}
716
717fn render_value_for_clipboard(value: &ValueTree) -> String {
718 use std::fmt::Write as _;
719 fn render(v: &ValueTree, indent: usize, out: &mut String) {
720 let pad = " ".repeat(indent);
721 match v {
722 ValueTree::Null => {
723 let _ = write!(out, "{pad}<null>");
724 }
725 ValueTree::Leaf(s) => {
726 let _ = write!(out, "{pad}{s}");
727 }
728 ValueTree::Array(items) => {
729 for (i, item) in items.iter().enumerate() {
730 if i > 0 {
731 out.push('\n');
732 }
733 let _ = write!(out, "{pad}[{i}]");
734 out.push('\n');
735 render(item, indent + 1, out);
736 }
737 }
738 ValueTree::Object(fields) => {
739 for (i, (k, val)) in fields.iter().enumerate() {
740 if i > 0 {
741 out.push('\n');
742 }
743 let _ = write!(out, "{pad}{k}:");
744 out.push('\n');
745 render(val, indent + 1, out);
746 }
747 }
748 }
749 }
750 let mut out = String::new();
751 render(value, 0, &mut out);
752 out
753}
754
755fn forward_logs(
756 mut log_rx: mpsc::UnboundedReceiver<UiUpdate>,
757 update_tx: mpsc::UnboundedSender<UiUpdate>,
758) {
759 std::thread::spawn(move || {
760 while let Some(msg) = log_rx.blocking_recv() {
761 if update_tx.send(msg).is_err() {
762 break;
763 }
764 }
765 });
766}