use opcua::types::NodeId;
use crate::messages::UiAction;
use crate::model::{AppModel, ConnectionState};
use crate::types::TreeChild;
pub fn draw(model: &AppModel, ui: &mut egui::Ui, actions: &mut Vec<UiAction>) {
ui.label(egui::RichText::new("Address Space").strong());
ui.separator();
if !matches!(model.connection, ConnectionState::Connected) {
ui.label(egui::RichText::new("Not connected").italics().weak());
return;
}
handle_subscribe_keys(model, ui, actions);
egui::ScrollArea::vertical().show(ui, |ui| {
draw_root(model, ui, actions);
});
}
fn handle_subscribe_keys(model: &AppModel, ui: &egui::Ui, actions: &mut Vec<UiAction>) {
let Some(node) = model.selected.as_ref() else {
return;
};
if model.subscribing.contains(node) {
return;
}
let unsubscribe = ui.input_mut(|i| i.consume_key(egui::Modifiers::SHIFT, egui::Key::S));
let subscribe = ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::S));
if unsubscribe && model.subscriptions.iter().any(|r| r.node_id == *node) {
actions.push(UiAction::Unsubscribe(node.clone()));
} else if subscribe {
actions.push(UiAction::Subscribe(node.clone()));
}
}
fn draw_root(model: &AppModel, ui: &mut egui::Ui, actions: &mut Vec<UiAction>) {
let root_id = model.root_node.clone();
let expanded = model.tree.expanded.contains(&root_id);
let loading = model.tree.loading.contains(&root_id);
let selected = model.selected.as_ref() == Some(&root_id);
let icon = if expanded { "▾" } else { "▸" };
ui.horizontal(|ui| {
if ui.small_button(icon).clicked() {
actions.push(UiAction::NodeToggleExpand(root_id.clone()));
}
let label = format!("Root ({root_id})");
let resp = ui.selectable_label(selected, label);
if resp.clicked() {
actions.push(UiAction::NodeSelected(root_id.clone()));
}
attach_node_context_menu(resp, &root_id, actions);
if loading {
ui.spinner();
}
});
if expanded
&& let Some(children) = model.tree.children.get(&root_id)
{
ui.indent("root_children", |ui| {
for child in children {
draw_child(model, ui, actions, child, 1);
}
});
}
}
fn draw_child(
model: &AppModel,
ui: &mut egui::Ui,
actions: &mut Vec<UiAction>,
child: &TreeChild,
depth: usize,
) {
let id = &child.node_id;
let expanded = model.tree.expanded.contains(id);
let loading = model.tree.loading.contains(id);
let selected = model.selected.as_ref() == Some(id);
ui.horizontal(|ui| {
if child.has_children {
let icon = if expanded { "▾" } else { "▸" };
if ui.small_button(icon).clicked() {
actions.push(UiAction::NodeToggleExpand(id.clone()));
}
} else {
ui.add_space(20.0);
}
let label_text = if child.display_name.is_empty() {
child.browse_name.clone()
} else {
child.display_name.clone()
};
let label = format!("{} [{:?}]", label_text, child.node_class);
let resp = ui.selectable_label(selected, label);
if resp.clicked() {
actions.push(UiAction::NodeSelected(id.clone()));
}
attach_node_context_menu(resp, id, actions);
if loading {
ui.spinner();
}
});
if expanded
&& child.has_children
&& let Some(grandkids) = model.tree.children.get(id)
{
let indent_id = format!("kids_{}_{}", depth, node_id_key(id));
ui.indent(indent_id, |ui| {
for gk in grandkids {
draw_child(model, ui, actions, gk, depth + 1);
}
});
}
}
fn node_id_key(id: &NodeId) -> String {
id.to_string()
}
pub(super) fn attach_node_context_menu(
resp: egui::Response,
id: &NodeId,
actions: &mut Vec<UiAction>,
) {
let id_string = id.to_string();
let id_clone = id.clone();
let mut copy_path = false;
resp.context_menu(|ui| {
if ui.button("Copy NodeId").clicked() {
ui.output_mut(|o| o.copied_text = id_string.clone());
tracing::info!("copied NodeId: {id_string}");
ui.close_menu();
}
if ui.button("Copy Path").clicked() {
copy_path = true;
ui.close_menu();
}
});
if copy_path {
actions.push(UiAction::CopyPath(id_clone));
}
}