aeronet_websocket 0.18.0

WebSocket IO layer implementation for `aeronet`
Documentation
//! Example showing a WebSocket client which can send and receive UTF-8
//! strings.

use {
    aeronet_io::{
        Session, SessionEndpoint,
        connection::{Disconnect, DisconnectReason, Disconnected, LocalAddr, PeerAddr},
    },
    aeronet_websocket::client::{ClientConfig, WebSocketClient, WebSocketClientPlugin},
    bevy::prelude::*,
    bevy_egui::{EguiContexts, EguiPlugin, EguiPrimaryContextPass, egui},
    core::mem,
};

fn main() -> AppExit {
    App::new()
        .add_plugins((DefaultPlugins, EguiPlugin::default(), WebSocketClientPlugin))
        .init_resource::<Log>()
        .add_systems(Startup, setup_camera)
        .add_systems(Update, add_msgs_to_ui)
        .add_systems(EguiPrimaryContextPass, (global_ui, session_ui).chain())
        .add_observer(on_connecting)
        .add_observer(on_connected)
        .add_observer(on_disconnected)
        .run()
}

#[derive(Debug, Default, Deref, DerefMut, Resource)]
struct Log(Vec<String>);

#[derive(Debug, Default, Component)]
struct SessionUi {
    msg: String,
    log: Vec<String>,
}

fn setup_camera(mut commands: Commands) {
    commands.spawn(Camera2d);
}

fn on_connecting(trigger: On<Add, SessionEndpoint>, names: Query<&Name>, mut log: ResMut<Log>) {
    let target = trigger.event_target();
    let name = names
        .get(target)
        .expect("our session entity should have a name");
    log.push(format!("{name} connected"));
}

fn on_connected(trigger: On<Add, Session>, names: Query<&Name>, mut log: ResMut<Log>) {
    let target = trigger.event_target();
    let name = names
        .get(target)
        .expect("our session entity should have a name");
    log.push(format!("{name} connected"));
}

fn on_disconnected(trigger: On<Disconnected>, names: Query<&Name>, mut log: ResMut<Log>) {
    let target = trigger.event_target();
    let name = names
        .get(target)
        .expect("our session entity should have a name");
    log.push(match &trigger.reason {
        DisconnectReason::ByUser(reason) => {
            format!("{name} disconnected by user: {reason}")
        }
        DisconnectReason::ByPeer(reason) => {
            format!("{name} disconnected by peer: {reason}")
        }
        DisconnectReason::ByError(err) => {
            format!("{name} disconnected due to error: {err:#}")
        }
    });
}

fn global_ui(
    mut egui: EguiContexts,
    mut commands: Commands,
    log: Res<Log>,
    mut target: Local<String>,
    mut session_id: Local<usize>,
) -> Result<(), BevyError> {
    const DEFAULT_TARGET: &str = "wss://127.0.0.1:25570";

    egui::Window::new("Connect").show(egui.ctx_mut()?, |ui| {
        let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter));

        let mut connect = false;
        ui.horizontal(|ui| {
            let connect_resp = ui.add(
                egui::TextEdit::singleline(&mut *target)
                    .hint_text(format!("{DEFAULT_TARGET} | [enter] to connect")),
            );
            connect |= connect_resp.lost_focus() && enter_pressed;
            connect |= ui.button("Connect").clicked();
        });

        if connect {
            let mut target = target.clone();
            if target.is_empty() {
                DEFAULT_TARGET.clone_into(&mut target);
            }

            let config = client_config();

            *session_id += 1;
            let name = format!("{}. {target}", *session_id);
            commands
                .spawn((Name::new(name), SessionUi::default()))
                .queue(WebSocketClient::connect(config, target));
        }

        for msg in log.iter() {
            ui.label(msg);
        }
    });

    Ok(())
}

#[cfg(target_family = "wasm")]
fn client_config() -> ClientConfig {
    #[expect(
        clippy::default_constructed_unit_structs,
        reason = "keep parity with non-WASM"
    )]
    ClientConfig::default()
}

#[cfg(not(target_family = "wasm"))]
fn client_config() -> ClientConfig {
    ClientConfig::builder().with_no_cert_validation()
}

fn add_msgs_to_ui(mut sessions: Query<(&mut SessionUi, &mut Session)>) {
    for (mut ui_state, mut session) in &mut sessions {
        for packet in session.recv.drain(..) {
            let msg =
                String::from_utf8(packet.payload.into()).unwrap_or_else(|_| "(not UTF-8)".into());
            ui_state.log.push(format!("> {msg}"));
        }
    }
}

fn session_ui(
    mut egui: EguiContexts,
    mut commands: Commands,
    mut sessions: Query<(
        Entity,
        &Name,
        &mut SessionUi,
        Option<&mut Session>,
        Option<&LocalAddr>,
        Option<&PeerAddr>,
    )>,
) -> Result<(), BevyError> {
    for (entity, name, mut ui_state, mut session, local_addr, peer_addr) in &mut sessions {
        egui::Window::new(name.to_string()).show(egui.ctx_mut()?, |ui| {
            let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter));

            let mut send_msg = false;
            let msg_resp = ui
                .horizontal(|ui| {
                    if session.is_none() {
                        ui.disable();
                    }

                    let msg_resp = ui.add(
                        egui::TextEdit::singleline(&mut ui_state.msg).hint_text("[enter] to send"),
                    );
                    send_msg |= msg_resp.lost_focus() && enter_pressed;
                    send_msg |= ui.button("Send").clicked();
                    msg_resp
                })
                .inner;

            if send_msg && let Some(session) = &mut session {
                let msg = mem::take(&mut ui_state.msg);
                ui_state.log.push(format!("< {msg}"));
                session.send.push(msg.into());
                ui.memory_mut(|m| m.request_focus(msg_resp.id));
            }

            if ui.button("Disconnect").clicked() {
                commands.trigger(Disconnect::new(entity, "pressed disconnect button"));
            }

            let stats = session.as_ref().map(|s| s.stats).unwrap_or_default();

            egui::Grid::new("stats").show(ui, |ui| {
                ui.label("Packet MTU");
                ui.label(format!("{}", session.map(|s| s.mtu()).unwrap_or_default()));
                ui.end_row();

                ui.label("Packets recv/sent");
                ui.label(format!("{} / {}", stats.packets_recv, stats.packets_sent));
                ui.end_row();

                ui.label("Bytes recv/sent");
                ui.label(format!("{} / {}", stats.bytes_recv, stats.bytes_sent));
                ui.end_row();

                ui.label("Local address");
                ui.label(
                    local_addr
                        .map(|LocalAddr(addr)| format!("{addr:?}"))
                        .unwrap_or_default(),
                );
                ui.end_row();

                ui.label("Peer address");
                ui.label(
                    peer_addr
                        .map(|PeerAddr(addr)| format!("{addr:?}"))
                        .unwrap_or_default(),
                );
                ui.end_row();
            });

            egui::ScrollArea::vertical().show(ui, |ui| {
                for msg in &ui_state.log {
                    ui.label(msg);
                }
            });
        });
    }

    Ok(())
}