ua-client 0.5.0

Native OPC UA browser/inspector GUI built on async-opcua and egui
use crate::messages::UiAction;
use crate::model::{AppModel, ConnectionState};

pub fn draw(model: &AppModel, ui: &mut egui::Ui, actions: &mut Vec<UiAction>) {
    let visuals = &ui.style().visuals;
    let stroke_color = if visuals.dark_mode {
        egui::Color32::from_gray(110)
    } else {
        egui::Color32::from_gray(140)
    };
    egui::Frame::default()
        .stroke(egui::Stroke::new(1.0, stroke_color))
        .rounding(6.0)
        .inner_margin(egui::Margin::symmetric(10.0, 8.0))
        .show(ui, |ui| {
            ui.horizontal_centered(|ui| {
                ui.label("URI:");
                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                    draw_status(ui, model);
                    draw_disconnect(ui, model, actions);
                    draw_connect(ui, model, actions);
                    draw_security_info(ui, model, actions);
                    draw_history_dropdown(ui, model, actions);
                    draw_url(ui, model, actions);
                });
            });
        });
}

fn draw_url(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
    let editable = matches!(model.connection, ConnectionState::Disconnected);
    let mut url = model.endpoint_url.clone();
    let resp = ui.add_enabled(
        editable,
        egui::TextEdit::singleline(&mut url)
            .desired_width(ui.available_width())
            .margin(egui::vec2(8.0, 6.0)),
    );
    if resp.changed() {
        actions.push(UiAction::EndpointEdited(url));
    }
    if editable && resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
        actions.push(UiAction::ConnectClicked);
    }
}

fn draw_history_dropdown(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
    let editable = matches!(model.connection, ConnectionState::Disconnected);
    let enabled = editable && !model.endpoint_history.is_empty();
    let popup_id = ui.make_persistent_id("endpoint_history_popup");
    let btn = ui.add_enabled(enabled, egui::Button::new("â–ľ"));
    if btn.clicked() {
        ui.memory_mut(|m| m.toggle_popup(popup_id));
    }
    egui::popup_below_widget(
        ui,
        popup_id,
        &btn,
        egui::PopupCloseBehavior::CloseOnClick,
        |ui| {
            ui.set_min_width(360.0);
            for past in &model.endpoint_history {
                if ui.selectable_label(false, past).clicked() {
                    actions.push(UiAction::EndpointEdited(past.clone()));
                }
            }
        },
    );
}

fn draw_security_info(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
    let editable = matches!(model.connection, ConnectionState::Disconnected);
    let (text, color) = match model.selected_endpoint.as_ref() {
        Some(ep) => (
            format!("đź”’ {} / {}", ep.security_policy, ep.security_mode.label()),
            egui::Color32::LIGHT_GREEN,
        ),
        None => ("🔓 no endpoint chosen".to_string(), egui::Color32::GRAY),
    };
    let widget = egui::Label::new(egui::RichText::new(text).color(color))
        .sense(if editable {
            egui::Sense::click()
        } else {
            egui::Sense::hover()
        });
    let resp = ui.add(widget);
    let resp = if editable {
        resp.on_hover_text("Click to change endpoint")
    } else {
        resp.on_hover_text("Selected endpoint (disconnect to change)")
    };
    if editable && resp.clicked() {
        actions.push(UiAction::OpenEndpointPicker);
    }
}

fn draw_connect(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
    let enabled = matches!(model.connection, ConnectionState::Disconnected);
    if ui
        .add_enabled(enabled, egui::Button::new("Connect"))
        .clicked()
    {
        actions.push(UiAction::ConnectClicked);
    }
}

fn draw_disconnect(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
    let enabled = matches!(model.connection, ConnectionState::Connected);
    if ui
        .add_enabled(enabled, egui::Button::new("Disconnect"))
        .clicked()
    {
        actions.push(UiAction::DisconnectClicked);
    }
}

fn draw_status(ui: &mut egui::Ui, model: &AppModel) {
    let (label, color) = match model.connection {
        ConnectionState::Disconnected => ("Disconnected", egui::Color32::GRAY),
        ConnectionState::Connecting => ("Connecting…", egui::Color32::YELLOW),
        ConnectionState::Connected => ("Connected", egui::Color32::LIGHT_GREEN),
        ConnectionState::Disconnecting => ("Disconnecting…", egui::Color32::YELLOW),
    };
    ui.colored_label(color, label);
}