bevy_boxdd 0.5.0

Bevy integration for the boxdd Rust bindings to Box2D
Documentation
use crate::control::{EventStats, TestbedState};
use crate::scenes::{SCENE_REGISTRY, TestbedEntity};
use crate::switch_scene;
use bevy::prelude::*;
use bevy_egui::egui::{LayerId, Ui, UiBuilder};
use bevy_egui::{EguiContexts, egui};

pub(crate) fn draw_testbed_ui(
    mut contexts: EguiContexts,
    mut state: ResMut<TestbedState>,
    mut commands: Commands,
    entities: Query<Entity, With<TestbedEntity>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    mut stats: ResMut<EventStats>,
) -> Result {
    let mut requested_scene = None;

    let ctx = contexts.ctx_mut()?;
    let mut root_ui = Ui::new(
        ctx.clone(),
        "boxdd_testbed_root".into(),
        UiBuilder::new()
            .layer_id(LayerId::background())
            .max_rect(ctx.viewport_rect()),
    );

    egui::Panel::left("boxdd_testbed_panel")
        .default_size(300.0)
        .min_size(260.0)
        .resizable(true)
        .show(&mut root_ui, |ui| {
            ui.heading(if state.scene_switching_enabled {
                "boxdd Testbed"
            } else {
                SCENE_REGISTRY[state.scene_index].name
            });
            ui.separator();

            ui.horizontal(|ui| {
                if ui
                    .button(if state.paused { "Resume" } else { "Pause" })
                    .clicked()
                {
                    state.paused = !state.paused;
                    if !state.paused {
                        state.cancel_single_step();
                    }
                }
                if ui.button("Reset").clicked() {
                    requested_scene = Some(state.scene_index);
                }
                if ui
                    .add_enabled(state.paused, egui::Button::new("Step"))
                    .clicked()
                {
                    state.request_single_step();
                }
            });

            ui.separator();
            draw_scene_list(ui, &mut state, &mut requested_scene);

            ui.separator();
            ui.label(egui::RichText::new("World").strong());
            ui.checkbox(&mut state.gravity_enabled, "Gravity");
            ui.checkbox(&mut state.sleeping_enabled, "Sleeping");
            ui.checkbox(&mut state.warm_starting_enabled, "Warm starting");
            ui.checkbox(&mut state.continuous_enabled, "Continuous collision");
            ui.add(
                egui::Slider::new(
                    &mut state.sub_step_count,
                    crate::control::MIN_SUB_STEPS..=crate::control::MAX_SUB_STEPS,
                )
                .text("Substeps"),
            );
            ui.add(
                egui::Slider::new(
                    &mut state.hertz,
                    crate::control::MIN_HERTZ..=crate::control::MAX_HERTZ,
                )
                .text("Hz"),
            );

            ui.separator();
            ui.label(egui::RichText::new("Events").strong());
            ui.label(format!(
                "Contact begin/end: {} / {}",
                stats.contact_begin_total, stats.contact_end_total
            ));
            ui.label(format!("Contact hits: {}", stats.contact_hit_total));
            ui.label(format!(
                "Sensor begin/end: {} / {}",
                stats.sensor_begin_total, stats.sensor_end_total
            ));
            ui.label(format!(
                "This frame: C {} E {} H {} S {} X {}",
                stats.contact_begin_frame,
                stats.contact_end_frame,
                stats.contact_hit_frame,
                stats.sensor_begin_frame,
                stats.sensor_end_frame
            ));

            ui.separator();
            ui.label(egui::RichText::new("Draw").strong());
            ui.checkbox(&mut state.draw_overlays, "Joint overlays");
        });

    if let Some(scene_index) = requested_scene {
        switch_scene(
            scene_index,
            &mut state,
            &mut commands,
            &entities,
            &mut meshes,
            &mut materials,
            &mut stats,
        );
    }

    Ok(())
}

fn draw_scene_list(ui: &mut Ui, state: &mut TestbedState, requested_scene: &mut Option<usize>) {
    if state.scene_switching_enabled {
        ui.label(egui::RichText::new("Scenes").strong());
        egui::ScrollArea::vertical()
            .max_height(310.0)
            .show(ui, |ui| {
                let mut current_category = None;
                for (index, metadata) in SCENE_REGISTRY.iter().enumerate() {
                    if current_category != Some(metadata.category) {
                        current_category = Some(metadata.category);
                        ui.add_space(4.0);
                        ui.label(
                            egui::RichText::new(metadata.category)
                                .small()
                                .color(egui::Color32::GRAY),
                        );
                    }

                    if ui
                        .selectable_label(state.scene_index == index, metadata.name)
                        .clicked()
                    {
                        *requested_scene = Some(index);
                    }

                    if state.scene_index == index {
                        draw_scene_metadata(ui, metadata);
                    }
                }
            });
    } else {
        draw_scene_metadata(ui, &SCENE_REGISTRY[state.scene_index]);
    }
}

fn draw_scene_metadata(ui: &mut Ui, metadata: &crate::scenes::TestbedSceneMetadata) {
    ui.label(egui::RichText::new(metadata.description).small());
    ui.label(
        egui::RichText::new(metadata.source_label())
            .small()
            .color(egui::Color32::LIGHT_GRAY),
    );
    for upstream in metadata.upstream {
        ui.label(
            egui::RichText::new(format!(
                "{} / {} ({})",
                upstream.category,
                upstream.name,
                upstream.mode.as_str()
            ))
            .small()
            .color(egui::Color32::GRAY),
        );
    }
}