egui 0.37.0

An easy-to-use immediate mode GUI that runs on both web and native
Documentation
use egui::{
    Button, Color32, Context, DragAndDrop, Event, Frame, Modifiers, PointerButton, RawInput, Rect,
    Stroke, Ui, epaint::ClippedShape, pos2, vec2,
};

#[derive(Debug, PartialEq)]
struct Output {
    shapes: Vec<ClippedShape>,
    content: Rect,
    zone: Rect,
    sibling: Rect,
    payload: Option<u32>,
}

fn show(ctx: &Context, frame: Frame, drop_zone: bool, events: Vec<Event>) -> Output {
    let mut result = Output {
        shapes: Vec::new(),
        content: Rect::NOTHING,
        zone: Rect::NOTHING,
        sibling: Rect::NOTHING,
        payload: None,
    };
    let output = ctx.run_ui(
        RawInput {
            screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 300.0))),
            events,
            ..Default::default()
        },
        |ui| {
            let contents = |ui: &mut Ui| {
                ui.add_sized(vec2(40.0, 20.0), Button::new(""));
                ui.min_rect()
            };
            let response = if drop_zone {
                let (response, payload) = ui.dnd_drop_zone::<u32, _>(frame, contents);
                result.payload = payload.as_deref().copied();
                response
            } else {
                frame.show(ui, contents)
            };
            result.content = response.inner;
            result.zone = response.response.rect;
            result.sibling = ui.add_sized(vec2(40.0, 20.0), Button::new("")).rect;
        },
    );
    result.shapes = output.shapes.clone();
    output.drop_without_applying_deltas();
    result
}

fn custom_frame() -> Frame {
    Frame::NONE
        .inner_margin(8)
        .outer_margin(3)
        .fill(Color32::RED)
        .stroke(Stroke::new(3.0, Color32::GREEN))
        .corner_radius(7)
        .shadow(egui::epaint::Shadow {
            offset: [2, 3],
            blur: 4,
            spread: 1,
            color: Color32::BLACK,
        })
}

#[test]
fn idle_drop_zone_preserves_frame_and_contents() {
    let frame = custom_frame();
    let plain = Context::default();
    let drop_zone = Context::default();
    for _ in 0..3 {
        assert_eq!(
            show(&plain, frame, false, vec![]),
            show(&drop_zone, frame, true, vec![]),
        );
    }
}

#[test]
fn idle_drop_zone_without_styling_uses_inactive_style() {
    for frame in [Frame::NONE, Frame::default().inner_margin(4.0)] {
        let ctx = Context::default();
        for _ in 0..3 {
            let output = show(&ctx, frame, true, vec![]);
            let inactive = ctx.style_of(ctx.theme()).visuals.widgets.inactive;
            let expected = frame.fill(inactive.bg_fill).stroke(inactive.bg_stroke);
            assert!(
                output
                    .shapes
                    .iter()
                    .any(|s| s.shape == expected.paint(output.content))
            );
        }
    }
}

#[test]
fn drop_zone_drag_feedback_and_release() {
    for accepted in [true, false] {
        let ctx = Context::default();
        let frame = custom_frame();
        let idle = show(&ctx, frame, true, vec![]);
        let inside = idle.zone.min + vec2(4.0, 4.0);
        let outside = pos2(300.0, 200.0);
        if accepted {
            DragAndDrop::set_payload(&ctx, 42_u32);
        } else {
            DragAndDrop::set_payload(&ctx, "wrong type");
        }
        for pointer in [outside, inside, outside, inside] {
            let output = show(&ctx, frame, true, vec![Event::PointerMoved(pointer)]);
            let visuals = ctx.style_of(ctx.theme()).visuals.clone();
            let style = if accepted && pointer == inside {
                visuals.widgets.active
            } else {
                visuals.widgets.inactive
            };
            let mut expected = frame.fill(style.bg_fill).stroke(style.bg_stroke);
            if !accepted {
                expected.fill = visuals.disable(expected.fill);
                expected.stroke.color = visuals.disable(expected.stroke.color);
            }
            assert!(
                output
                    .shapes
                    .iter()
                    .any(|s| s.shape == expected.paint(output.content))
            );
            assert_eq!(output.content, idle.content);
            assert_eq!(output.zone, idle.zone);
            assert_eq!(output.sibling, idle.sibling);
            assert_eq!(output.payload, None);
        }
        let release = show(
            &ctx,
            frame,
            true,
            vec![Event::PointerButton {
                pos: inside,
                button: PointerButton::Primary,
                pressed: false,
                modifiers: Modifiers::NONE,
            }],
        );
        assert_eq!(release.payload, accepted.then_some(42));
        assert!(!DragAndDrop::has_any_payload(&ctx));
        let restored = show(&ctx, frame, true, vec![Event::PointerGone]);
        assert_eq!(restored, idle);
    }
}