dear-implot 0.16.0-alpha.3

High-level Rust bindings to ImPlot with dear-imgui-rs integration
Documentation
use super::{PlotContext, validation::axis_tick_count_to_i32};
use crate::sys;
use crate::{Axis, PlotCond, XAxis, YAxis};
use dear_imgui_rs::{Context, ContextBindingError};
use std::cell::Cell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{Mutex, OnceLock};

fn test_guard() -> std::sync::MutexGuard<'static, ()> {
    static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
    GUARD
        .get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(|err| err.into_inner())
}

fn prepare_imgui(imgui: &mut Context) {
    let io = imgui.io_mut();
    io.set_display_size([800.0, 600.0]);
    io.set_delta_time(1.0 / 60.0);
    imgui
        .font_atlas()
        .try_claim_legacy_renderer()
        .expect("headless test requires the legacy font-atlas capability")
        .build();
}

#[test]
fn nested_plot_bindings_restore_imgui_and_implot_contexts() {
    let _guard = test_guard();
    let imgui_a = Context::create();
    let imgui_a_raw = imgui_a.as_raw();
    let plot_a = PlotContext::create(&imgui_a);
    let plot_a_raw = unsafe { plot_a.raw() };
    let suspended_a = imgui_a.suspend_or_panic();

    let imgui_b = Context::create();
    let imgui_b_raw = imgui_b.as_raw();
    let plot_b = PlotContext::create(&imgui_b);
    let plot_b_raw = unsafe { plot_b.raw() };
    unsafe { sys::ImPlot_SetCurrentContext(plot_b_raw) };

    let binding_a = plot_a.binding();
    let binding_b = plot_b.binding();
    binding_a.with_bound_context("test outer ImPlot binding", || {
        assert_eq!(
            unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
            imgui_a_raw
        );
        assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, plot_a_raw);

        binding_b.with_bound_context("test inner ImPlot binding", || {
            assert_eq!(
                unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
                imgui_b_raw
            );
            assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, plot_b_raw);
        });

        assert_eq!(
            unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
            imgui_a_raw
        );
        assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, plot_a_raw);
    });

    assert_eq!(
        unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
        imgui_b_raw
    );
    assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, plot_b_raw);

    let panic_result = catch_unwind(AssertUnwindSafe(|| {
        binding_a.with_bound_context("test panicking outer ImPlot binding", || {
            binding_b.with_bound_context("test panicking inner ImPlot binding", || {
                panic!("nested binding panic probe");
            });
        });
    }));
    assert!(panic_result.is_err());
    assert_eq!(
        unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
        imgui_b_raw
    );
    assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, plot_b_raw);

    drop(plot_b);
    drop(imgui_b);
    let imgui_a = suspended_a
        .activate()
        .unwrap_or_else(|_| panic!("context A should reactivate after context B is dropped"));
    drop(plot_a);
    drop(imgui_a);
}

#[test]
fn nested_plot_binding_never_restores_a_destroyed_outer_context() {
    let _guard = test_guard();
    let imgui_a = Context::create();
    let plot_a = PlotContext::create(&imgui_a);
    let binding_a = plot_a.binding();
    let mut suspended_a = Some(imgui_a.suspend_or_panic());

    let imgui_b = Context::create();
    let imgui_b_raw = imgui_b.as_raw();
    let plot_b = PlotContext::create(&imgui_b);
    let plot_b_raw = unsafe { plot_b.raw() };
    let binding_b = plot_b.binding();
    unsafe { sys::ImPlot_SetCurrentContext(plot_b_raw) };

    binding_a.with_bound_context("test outer ImPlot binding", || {
        binding_b.with_bound_context("test inner ImPlot binding", || {
            drop(
                suspended_a
                    .take()
                    .expect("context A must still be owned by the test"),
            );
            assert_eq!(
                unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
                imgui_b_raw
            );
        });

        assert!(unsafe { dear_imgui_rs::sys::igGetCurrentContext() }.is_null());
    });

    assert_eq!(
        unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
        imgui_b_raw
    );
    assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, plot_b_raw);

    drop(plot_a);
    assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, plot_b_raw);
    drop(plot_b);
    drop(imgui_b);
}

#[test]
fn dead_plot_binding_never_enters_the_native_scope() {
    let _guard = test_guard();
    let imgui = Context::create();
    let plot = PlotContext::create(&imgui);
    let binding = plot.binding();
    drop(imgui);

    let entered = Cell::new(false);
    let result = binding.try_with_bound_context(|| entered.set(true));

    assert!(matches!(result, Err(ContextBindingError::NativeDestroyed)));
    assert!(!entered.get());

    drop(plot);
    assert!(!entered.get());
}

#[test]
fn axis_ticks_range_count_is_checked_before_ffi() {
    assert_eq!(axis_tick_count_to_i32("test", 1), 1);
    assert_eq!(axis_tick_count_to_i32("test", i32::MAX as usize), i32::MAX);

    assert!(
        std::panic::catch_unwind(|| axis_tick_count_to_i32("test", 0)).is_err(),
        "zero tick counts must not cross the safe API boundary"
    );
    assert!(
        std::panic::catch_unwind(|| {
            axis_tick_count_to_i32("test", i32::MAX as usize + 1);
        })
        .is_err(),
        "oversized tick counts must not cross the safe API boundary"
    );
}

#[test]
fn plot_ui_binds_own_context_before_calls() {
    let _guard = test_guard();
    let mut imgui = Context::create();
    prepare_imgui(&mut imgui);
    let plot_a = PlotContext::create(&imgui);
    let raw_a = unsafe { plot_a.raw() };
    let plot_b = PlotContext::create(&imgui);
    let raw_b = unsafe { plot_b.raw() };

    {
        let frame = imgui.begin_frame();
        let plot_ui = plot_a.get_plot_ui(frame.ui());
        unsafe { sys::ImPlot_SetCurrentContext(raw_b) };

        {
            plot_ui.with_bound_context(|| {
                assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_a);
            })
        }
        assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_b);

        plot_ui.set_next_axes_to_fit();

        assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_b);
    }

    drop(plot_b);
    drop(plot_a);
}

#[test]
fn plot_token_binds_own_context_before_drop() {
    let _guard = test_guard();
    let mut imgui = Context::create();
    prepare_imgui(&mut imgui);
    let plot_a = PlotContext::create(&imgui);
    let plot_b = PlotContext::create(&imgui);
    let raw_b = unsafe { plot_b.raw() };

    {
        let frame = imgui.begin_frame();
        let plot_ui = plot_a.get_plot_ui(frame.ui());
        let token = plot_ui.begin_plot("token").expect("failed to begin plot");

        unsafe { sys::ImPlot_SetCurrentContext(raw_b) };
        drop(token);

        assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_b);
    }

    drop(plot_b);
    drop(plot_a);
}

#[test]
fn style_and_plot_clip_tokens_bind_own_context_before_drop() {
    let _guard = test_guard();
    let mut imgui = Context::create();
    prepare_imgui(&mut imgui);
    let plot_a = PlotContext::create(&imgui);
    let plot_b = PlotContext::create(&imgui);
    let raw_b = unsafe { plot_b.raw() };

    {
        let frame = imgui.begin_frame();
        let plot_ui = plot_a.get_plot_ui(frame.ui());
        let style = plot_ui.push_style_var_f32(crate::StyleVar::MinorAlpha, 0.5);
        unsafe { sys::ImPlot_SetCurrentContext(raw_b) };
        drop(style);
        assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_b);

        let token = plot_ui.begin_plot("clip").expect("failed to begin plot");
        let clip = token.push_plot_clip_rect(0.0);
        unsafe { sys::ImPlot_SetCurrentContext(raw_b) };
        drop(clip);
        assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_b);
        drop(token);
    }

    drop(plot_b);
    drop(plot_a);
}

#[test]
fn dropping_current_plot_context_clears_current_context() {
    let _guard = test_guard();
    let imgui = Context::create();
    let plot = PlotContext::create(&imgui);
    let raw = unsafe { plot.raw() };

    unsafe { sys::ImPlot_SetCurrentContext(raw) };
    drop(plot);

    assert!(unsafe { sys::ImPlot_GetCurrentContext() }.is_null());
}

#[test]
fn dropping_non_current_plot_context_restores_previous_context() {
    let _guard = test_guard();
    let imgui = Context::create();
    let plot_a = PlotContext::create(&imgui);
    let plot_b = PlotContext::create(&imgui);
    let raw_b = unsafe { plot_b.raw() };

    unsafe { sys::ImPlot_SetCurrentContext(raw_b) };
    drop(plot_a);

    assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_b);
    drop(plot_b);
}

#[test]
#[should_panic(expected = "PlotUi::set_next_x_axis_limits() min must be finite")]
fn set_next_axis_limits_rejects_non_finite_values_before_ffi() {
    let _guard = test_guard();
    let mut imgui = Context::create();
    prepare_imgui(&mut imgui);
    let plot = PlotContext::create(&imgui);

    {
        let frame = imgui.begin_frame();
        let plot_ui = plot.get_plot_ui(frame.ui());
        plot_ui.set_next_x_axis_limits(XAxis::X1, f64::NAN, 1.0, PlotCond::Once);
    }
}

#[test]
#[should_panic(expected = "PlotUi::setup_axis_zoom_constraints() min must be positive")]
fn axis_zoom_constraints_reject_non_positive_min_before_ffi() {
    let _guard = test_guard();
    let mut imgui = Context::create();
    prepare_imgui(&mut imgui);
    let plot = PlotContext::create(&imgui);

    {
        let frame = imgui.begin_frame();
        let plot_ui = plot.get_plot_ui(frame.ui());
        let token = plot_ui
            .begin_plot("constraints")
            .expect("failed to begin plot");
        plot_ui.setup_axis_zoom_constraints(Axis::Y1, 0.0, 10.0);
        token.end();
    }
}

#[test]
fn typed_axis_apis_accept_valid_axes() {
    let _guard = test_guard();
    let mut imgui = Context::create();
    prepare_imgui(&mut imgui);
    let plot = PlotContext::create(&imgui);

    {
        let frame = imgui.begin_frame();
        let plot_ui = plot.get_plot_ui(frame.ui());
        plot_ui.set_next_axis_to_fit(Axis::X1);

        let token = plot_ui
            .begin_plot("typed-axis")
            .expect("failed to begin plot");
        plot_ui.setup_x_axis(XAxis::X1, None, crate::AxisFlags::NONE);
        plot_ui.setup_y_axis(YAxis::Y1, None, crate::AxisFlags::NONE);
        let mut min = 0.0;
        let mut max = 1.0;
        plot_ui.setup_axis_links(Axis::Y1, Some(&mut min), Some(&mut max));
        plot_ui.setup_axis_limits_constraints(Axis::Y1, -10.0, 10.0);
        plot_ui.setup_axis_zoom_constraints(Axis::Y1, 0.1, 20.0);
        token.end();
    }
    drop(plot);
}