use bevy_app::{App, PreUpdate};
use bevy_ecs::message::Messages;
use bevy_ecs::prelude::*;
use bevy_input::ButtonState;
use bevy_input::keyboard::{Key as BevyKey, KeyCode, KeyboardFocusLost, KeyboardInput};
use bevy_input::mouse::{
MouseButton as BevyMouseButton, MouseButtonInput, MouseScrollUnit, MouseWheel,
};
use bevy_input::touch::{TouchInput, TouchPhase};
use bevy_math::{IVec2, Vec2};
use bevy_window::{
CursorEntered, CursorIcon, CursorLeft, CursorMoved, CursorOptions, Ime, PrimaryWindow,
SystemCursorIcon, Window, WindowFocused, WindowPosition, WindowResized, WindowResolution,
WindowScaleFactorChanged,
};
use dear_imgui_bevy::{
ImguiContext, ImguiFrameState, ImguiPlugin, ImguiPrimaryContextPass, ImguiViewportWindow,
input::{
ImguiInputCapture, ImguiInputState, imgui_wants_any_input, imgui_wants_keyboard_input,
imgui_wants_pointer_input, imgui_wants_pointer_input_unless_popup_close,
imgui_wants_text_input, map_bevy_key_code,
},
};
use dear_imgui_rs as imgui;
use std::sync::{Mutex, OnceLock};
fn imgui_context_guard() -> std::sync::MutexGuard<'static, ()> {
static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
GUARD.get_or_init(|| Mutex::new(())).lock().unwrap()
}
fn app_with_primary_window() -> (App, Entity) {
let mut app = App::new();
app.add_plugins(ImguiPlugin::default());
let mut window = Window {
resolution: WindowResolution::new(1600, 1200),
..Default::default()
};
window.resolution.set_scale_factor(2.0);
let primary = app.world_mut().spawn((window, PrimaryWindow)).id();
prepare_imgui_context(&mut app);
(app, primary)
}
fn prepare_imgui_context(app: &mut App) {
let mut context = app
.world_mut()
.get_non_send_mut::<ImguiContext>()
.expect("ImguiPlugin should install an ImGui context");
let ctx = context.context_mut();
ctx.io_mut().set_delta_time(1.0 / 60.0);
ctx.io_mut().set_config_input_trickle_event_queue(false);
let _ = ctx.font_atlas_mut().build();
let _ = ctx.set_ini_filename::<std::path::PathBuf>(None);
}
fn current_frame_input_chars() -> Vec<u32> {
unsafe {
let io = imgui::sys::igGetIO_Nil();
let legacy_queue = &(*io).InputQueueCharacters;
let mut chars = Vec::new();
if legacy_queue.Size > 0 && !legacy_queue.Data.is_null() {
chars.extend(
std::slice::from_raw_parts(legacy_queue.Data, legacy_queue.Size as usize)
.iter()
.copied(),
);
}
chars
}
}
fn begin_frame_and_assert(app: &mut App, assert_ui: impl FnOnce(&imgui::Ui)) {
let mut context = app
.world_mut()
.get_non_send_mut::<ImguiContext>()
.expect("ImguiContext should exist");
let frame = context.context_mut().begin_frame();
assert_ui(frame.ui());
let _ = frame.render();
}
fn run_input_systems(app: &mut App) {
app.world_mut().run_schedule(PreUpdate);
}
fn run_condition_value<M>(app: &mut App, system: impl IntoSystem<(), bool, M> + 'static) -> bool {
app.world_mut().run_system_cached(system).unwrap()
}
fn request_text_cursor_and_ime(
mut imgui_context: NonSendMut<ImguiContext>,
frame_state: NonSend<ImguiFrameState>,
) {
let ui = frame_state.ui().expect("Dear ImGui frame should be open");
ui.set_mouse_cursor(Some(imgui::MouseCursor::TextInput));
imgui_context
.context_mut()
.io_mut()
.set_mouse_draw_cursor(false);
let raw_context = imgui_context.context().as_raw();
unsafe {
let ime_data = &mut (*raw_context).PlatformImeData;
ime_data.WantTextInput = true;
ime_data.InputPos = imgui::sys::ImVec2_c { x: 222.0, y: 333.0 };
}
}
fn request_text_cursor_and_secondary_viewport_ime(
mut imgui_context: NonSendMut<ImguiContext>,
frame_state: NonSend<ImguiFrameState>,
) {
let ui = frame_state.ui().expect("Dear ImGui frame should be open");
ui.set_mouse_cursor(Some(imgui::MouseCursor::TextInput));
imgui_context
.context_mut()
.io_mut()
.set_mouse_draw_cursor(false);
let raw_context = imgui_context.context().as_raw();
unsafe {
let ime_data = &mut (*raw_context).PlatformImeData;
ime_data.WantTextInput = true;
ime_data.InputPos = imgui::sys::ImVec2_c { x: 144.0, y: 205.0 };
ime_data.ViewportId = 0x501;
}
}
fn request_primary_cursor_and_secondary_viewport_ime(
mut imgui_context: NonSendMut<ImguiContext>,
frame_state: NonSend<ImguiFrameState>,
) {
let ui = frame_state.ui().expect("Dear ImGui frame should be open");
ui.set_mouse_cursor(Some(imgui::MouseCursor::TextInput));
imgui_context
.context_mut()
.io_mut()
.set_mouse_draw_cursor(false);
let raw_context = imgui_context.context().as_raw();
unsafe {
let ime_data = &mut (*raw_context).PlatformImeData;
ime_data.WantTextInput = true;
ime_data.InputPos = imgui::sys::ImVec2_c { x: 77.0, y: 88.0 };
ime_data.ViewportId = 0x502;
}
}
fn request_software_cursor(
mut imgui_context: NonSendMut<ImguiContext>,
frame_state: NonSend<ImguiFrameState>,
) {
let ui = frame_state.ui().expect("Dear ImGui frame should be open");
ui.set_mouse_cursor(Some(imgui::MouseCursor::Hand));
imgui_context
.context_mut()
.io_mut()
.set_mouse_draw_cursor(true);
let raw_context = imgui_context.context().as_raw();
unsafe {
let ime_data = &mut (*raw_context).PlatformImeData;
ime_data.WantTextInput = false;
ime_data.InputPos = imgui::sys::ImVec2_c { x: 0.0, y: 0.0 };
}
}
fn request_hidden_cursor(
mut imgui_context: NonSendMut<ImguiContext>,
frame_state: NonSend<ImguiFrameState>,
) {
let ui = frame_state.ui().expect("Dear ImGui frame should be open");
ui.set_mouse_cursor(None);
imgui_context
.context_mut()
.io_mut()
.set_mouse_draw_cursor(false);
}
fn key_input(
window: Entity,
key_code: KeyCode,
logical_key: BevyKey,
state: ButtonState,
text: Option<&str>,
) -> KeyboardInput {
KeyboardInput {
key_code,
logical_key,
state,
text: text.map(Into::into),
repeat: false,
window,
}
}
#[test]
fn primary_window_input_maps_window_mouse_and_scroll_into_imgui_io() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: primary,
position: Vec2::new(123.0, 45.0),
delta: None,
});
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Left,
state: ButtonState::Pressed,
window: primary,
});
app.world_mut()
.resource_mut::<Messages<MouseWheel>>()
.write(MouseWheel {
unit: MouseScrollUnit::Line,
x: 1.0,
y: -2.0,
window: primary,
phase: TouchPhase::Moved,
});
run_input_systems(&mut app);
{
let context = app
.world()
.get_non_send::<ImguiContext>()
.unwrap()
.context();
assert_eq!(context.io().display_size(), [800.0, 600.0]);
assert_eq!(context.io().display_framebuffer_scale(), [2.0, 2.0]);
}
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.mouse_pos(), [123.0, 45.0]);
assert!(ui.is_mouse_down(imgui::MouseButton::Left));
assert_eq!(ui.io().mouse_source(), imgui::MouseSource::Mouse);
assert_eq!(ui.io().mouse_wheel_h(), 1.0);
assert_eq!(ui.io().mouse_wheel(), -2.0);
});
}
#[test]
fn primary_window_input_reports_main_hovered_viewport_when_viewports_are_enabled() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let main_viewport_id = {
let mut context = app.world_mut().get_non_send_mut::<ImguiContext>().unwrap();
let context = context.context_mut();
context
.io_mut()
.set_config_flags(imgui::ConfigFlags::VIEWPORTS_ENABLE);
context.main_viewport().id()
};
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: primary,
position: Vec2::new(123.0, 45.0),
delta: None,
});
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Left,
state: ButtonState::Pressed,
window: primary,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.mouse_pos(), [123.0, 45.0]);
assert_eq!(ui.io().mouse_hovered_viewport(), main_viewport_id);
assert!(ui.is_mouse_down(imgui::MouseButton::Left));
});
}
#[test]
fn input_keyboard_and_ime_messages_update_imgui_keys_modifiers_and_text_queue() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::ControlLeft,
BevyKey::Control,
ButtonState::Pressed,
None,
));
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::KeyA,
BevyKey::Character("a".into()),
ButtonState::Pressed,
Some("a"),
));
app.world_mut()
.resource_mut::<Messages<Ime>>()
.write(Ime::Commit {
window: primary,
value: "好!".to_owned(),
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
let chars = current_frame_input_chars();
assert!(chars.contains(&('a' as u32)));
assert!(chars.contains(&('好' as u32)));
assert!(chars.contains(&('!' as u32)));
assert!(ui.is_key_down(imgui::Key::A));
});
}
#[test]
fn input_ime_enable_commit_and_disable_preserves_explicit_ime_state() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.resource_mut::<Messages<Ime>>()
.write(Ime::Enabled { window: primary });
run_input_systems(&mut app);
assert!(app.world().resource::<ImguiInputState>().ime_enabled());
app.world_mut()
.resource_mut::<Messages<Ime>>()
.write(Ime::Commit {
window: primary,
value: "好".to_owned(),
});
run_input_systems(&mut app);
assert!(
app.world().resource::<ImguiInputState>().ime_enabled(),
"committed text should not imply that the platform IME was disabled"
);
begin_frame_and_assert(&mut app, |_ui| {
assert!(current_frame_input_chars().contains(&('好' as u32)));
});
app.world_mut()
.resource_mut::<Messages<Ime>>()
.write(Ime::Disabled { window: primary });
run_input_systems(&mut app);
assert!(!app.world().resource::<ImguiInputState>().ime_enabled());
}
#[test]
fn input_resize_dpi_and_cursor_leave_messages_update_imgui_io() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.resource_mut::<Messages<WindowResized>>()
.write(WindowResized {
window: primary,
width: 1024.0,
height: 768.0,
});
app.world_mut()
.resource_mut::<Messages<WindowScaleFactorChanged>>()
.write(WindowScaleFactorChanged {
window: primary,
scale_factor: 1.5,
});
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: primary,
position: Vec2::new(8.0, 9.0),
delta: None,
});
app.world_mut()
.resource_mut::<Messages<CursorLeft>>()
.write(CursorLeft { window: primary });
run_input_systems(&mut app);
{
let context = app
.world()
.get_non_send::<ImguiContext>()
.unwrap()
.context();
assert_eq!(context.io().display_size(), [1024.0, 768.0]);
assert_eq!(context.io().display_framebuffer_scale(), [1.5, 1.5]);
}
begin_frame_and_assert(&mut app, |ui| {
assert!(
ui.mouse_pos()[0] < -1.0e30 && ui.mouse_pos()[1] < -1.0e30,
"CursorLeft should move the Dear ImGui mouse position outside every window"
);
});
}
#[test]
fn input_invalid_window_metrics_are_sanitized_before_reaching_imgui_io() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.get_mut::<Window>(primary)
.unwrap()
.resolution
.set_scale_factor(f32::NAN);
app.world_mut()
.resource_mut::<Messages<WindowResized>>()
.write(WindowResized {
window: primary,
width: f32::NAN,
height: -10.0,
});
app.world_mut()
.resource_mut::<Messages<WindowScaleFactorChanged>>()
.write(WindowScaleFactorChanged {
window: primary,
scale_factor: f64::INFINITY,
});
run_input_systems(&mut app);
let context = app
.world()
.get_non_send::<ImguiContext>()
.unwrap()
.context();
assert_eq!(context.io().display_size(), [0.0, 0.0]);
assert_eq!(context.io().display_framebuffer_scale(), [1.0, 1.0]);
}
#[test]
fn input_platform_feedback_updates_primary_window_cursor_and_ime_state() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.add_systems(ImguiPrimaryContextPass, request_text_cursor_and_ime);
app.update();
let entity = app.world().entity(primary);
assert!(
entity.get::<CursorOptions>().unwrap().visible,
"OS cursor should stay visible when Dear ImGui is not drawing a software cursor"
);
assert_eq!(
entity.get::<CursorIcon>(),
Some(&CursorIcon::System(SystemCursorIcon::Text))
);
let window = entity.get::<Window>().unwrap();
assert!(window.ime_enabled);
assert_eq!(window.ime_position, Vec2::new(222.0, 333.0));
}
#[test]
fn input_platform_feedback_updates_secondary_viewport_window_cursor_and_ime_state() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let viewport_id = imgui::Id::from(0x501);
let secondary = app
.world_mut()
.spawn((
Window {
position: WindowPosition::At(IVec2::new(100, 150)),
resolution: WindowResolution::new(640, 480),
..Default::default()
},
ImguiViewportWindow { viewport_id },
))
.id();
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: secondary,
position: Vec2::new(10.0, 20.0),
delta: None,
});
app.add_systems(
ImguiPrimaryContextPass,
request_text_cursor_and_secondary_viewport_ime,
);
app.update();
let primary_window = app.world().entity(primary).get::<Window>().unwrap();
assert!(
!primary_window.ime_enabled,
"IME feedback for a secondary viewport should not be applied to the primary window"
);
let entity = app.world().entity(secondary);
assert!(
entity.get::<CursorOptions>().unwrap().visible,
"OS cursor should stay visible when Dear ImGui is not drawing a software cursor"
);
assert_eq!(
entity.get::<CursorIcon>(),
Some(&CursorIcon::System(SystemCursorIcon::Text))
);
let window = entity.get::<Window>().unwrap();
assert!(window.ime_enabled);
assert_eq!(window.ime_position, Vec2::new(44.0, 55.0));
}
#[test]
fn input_platform_feedback_routes_cursor_independently_from_ime_viewport() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let viewport_id = imgui::Id::from(0x502);
let secondary = app
.world_mut()
.spawn((
Window {
resolution: WindowResolution::new(640, 480),
..Default::default()
},
ImguiViewportWindow { viewport_id },
))
.id();
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: primary,
position: Vec2::new(11.0, 22.0),
delta: None,
});
app.add_systems(
ImguiPrimaryContextPass,
request_primary_cursor_and_secondary_viewport_ime,
);
app.update();
let primary_entity = app.world().entity(primary);
assert_eq!(
primary_entity.get::<CursorIcon>(),
Some(&CursorIcon::System(SystemCursorIcon::Text)),
"cursor feedback should follow the hovered Bevy window"
);
assert!(
!primary_entity.get::<Window>().unwrap().ime_enabled,
"IME feedback for a secondary viewport should not be applied to the primary window"
);
let secondary_entity = app.world().entity(secondary);
assert!(
secondary_entity.get::<CursorIcon>().is_none(),
"IME viewport must not pull cursor feedback onto a non-hovered window"
);
let secondary_window = secondary_entity.get::<Window>().unwrap();
assert!(secondary_window.ime_enabled);
assert_eq!(secondary_window.ime_position, Vec2::new(77.0, 88.0));
}
#[test]
fn input_platform_feedback_hides_os_cursor_when_imgui_draws_software_cursor() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.entity_mut(primary)
.insert(CursorIcon::from(SystemCursorIcon::Pointer));
app.add_systems(ImguiPrimaryContextPass, request_software_cursor);
app.update();
let entity = app.world().entity(primary);
assert!(
!entity.get::<CursorOptions>().unwrap().visible,
"OS cursor should be hidden while Dear ImGui draws the software cursor"
);
assert!(entity.get::<CursorIcon>().is_none());
assert!(!entity.get::<Window>().unwrap().ime_enabled);
}
#[test]
fn input_platform_feedback_hides_os_cursor_when_imgui_requests_no_cursor() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.entity_mut(primary)
.insert(CursorIcon::from(SystemCursorIcon::Pointer));
app.add_systems(ImguiPrimaryContextPass, request_hidden_cursor);
app.update();
let entity = app.world().entity(primary);
assert!(
!entity.get::<CursorOptions>().unwrap().visible,
"OS cursor should be hidden when Dear ImGui reports no cursor"
);
assert!(entity.get::<CursorIcon>().is_none());
}
#[test]
fn input_platform_feedback_restores_cursor_on_previous_hovered_window() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let secondary = app
.world_mut()
.spawn((
Window::default(),
ImguiViewportWindow {
viewport_id: imgui::Id::from(0x503),
},
))
.id();
app.add_systems(ImguiPrimaryContextPass, request_software_cursor);
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: secondary,
position: Vec2::new(10.0, 20.0),
delta: None,
});
app.update();
assert!(
!app.world()
.entity(secondary)
.get::<CursorOptions>()
.unwrap()
.visible,
"the hovered secondary window should inherit Dear ImGui's hidden software-cursor state"
);
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: primary,
position: Vec2::new(30.0, 40.0),
delta: None,
});
app.update();
let secondary_entity = app.world().entity(secondary);
assert!(
secondary_entity.get::<CursorOptions>().unwrap().visible,
"moving hover away from a secondary window must restore its OS cursor visibility"
);
assert!(
secondary_entity.get::<CursorIcon>().is_none(),
"moving hover away from a secondary window must clear stale ImGui cursor icons"
);
assert!(
!app.world()
.entity(primary)
.get::<CursorOptions>()
.unwrap()
.visible,
"the newly hovered primary window should now inherit Dear ImGui's hidden software-cursor state"
);
}
#[test]
fn input_focus_loss_releases_tracked_keyboard_and_mouse_state() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::ControlLeft,
BevyKey::Control,
ButtonState::Pressed,
None,
));
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::KeyA,
BevyKey::Character("a".into()),
ButtonState::Pressed,
Some("a"),
));
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Left,
state: ButtonState::Pressed,
window: primary,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert!(ui.is_key_down(imgui::Key::A));
assert!(ui.is_mouse_down(imgui::MouseButton::Left));
});
app.world_mut()
.resource_mut::<Messages<WindowFocused>>()
.write(WindowFocused {
window: primary,
focused: false,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert!(!ui.is_key_down(imgui::Key::A));
assert!(!ui.is_mouse_down(imgui::MouseButton::Left));
});
}
#[test]
fn input_focus_switch_between_viewport_windows_keeps_sticky_input_pressed() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let secondary = app
.world_mut()
.spawn((
Window::default(),
ImguiViewportWindow {
viewport_id: imgui::Id::from(0x560),
},
))
.id();
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::KeyA,
BevyKey::Character("a".into()),
ButtonState::Pressed,
None,
));
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Left,
state: ButtonState::Pressed,
window: primary,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert!(ui.is_key_down(imgui::Key::A));
assert!(ui.is_mouse_down(imgui::MouseButton::Left));
});
app.world_mut()
.resource_mut::<Messages<WindowFocused>>()
.write(WindowFocused {
window: primary,
focused: false,
});
app.world_mut()
.resource_mut::<Messages<WindowFocused>>()
.write(WindowFocused {
window: secondary,
focused: true,
});
run_input_systems(&mut app);
let input_state = app.world().resource::<ImguiInputState>();
assert_eq!(input_state.primary_window_focused(), Some(false));
assert_eq!(input_state.focused_window(), Some(secondary));
begin_frame_and_assert(&mut app, |ui| {
assert!(
ui.is_key_down(imgui::Key::A),
"switching focus between mapped ImGui windows must not synthesize a global key release"
);
assert!(
ui.is_mouse_down(imgui::MouseButton::Left),
"switching focus between mapped ImGui windows must not synthesize a global mouse release"
);
});
}
#[test]
fn input_primary_focus_sync_does_not_blur_while_secondary_viewport_is_focused() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut().get_mut::<Window>(primary).unwrap().focused = true;
let secondary = app
.world_mut()
.spawn((
Window::default(),
ImguiViewportWindow {
viewport_id: imgui::Id::from(0x561),
},
))
.id();
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::KeyA,
BevyKey::Character("a".into()),
ButtonState::Pressed,
None,
));
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Left,
state: ButtonState::Pressed,
window: primary,
});
run_input_systems(&mut app);
app.world_mut()
.resource_mut::<Messages<WindowFocused>>()
.write(WindowFocused {
window: secondary,
focused: true,
});
run_input_systems(&mut app);
app.world_mut().get_mut::<Window>(primary).unwrap().focused = false;
run_input_systems(&mut app);
let input_state = app.world().resource::<ImguiInputState>();
assert_eq!(input_state.primary_window_focused(), Some(false));
assert_eq!(input_state.focused_window(), Some(secondary));
begin_frame_and_assert(&mut app, |ui| {
assert!(
ui.is_key_down(imgui::Key::A),
"primary focus sync must not release keys while a secondary ImGui viewport is focused"
);
assert!(
ui.is_mouse_down(imgui::MouseButton::Left),
"primary focus sync must not release mouse buttons while a secondary ImGui viewport is focused"
);
});
}
#[test]
fn input_stale_focused_viewport_window_releases_sticky_input() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut().get_mut::<Window>(primary).unwrap().focused = true;
let secondary = app
.world_mut()
.spawn((
Window::default(),
ImguiViewportWindow {
viewport_id: imgui::Id::from(0x562),
},
))
.id();
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::KeyA,
BevyKey::Character("a".into()),
ButtonState::Pressed,
None,
));
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Left,
state: ButtonState::Pressed,
window: primary,
});
run_input_systems(&mut app);
app.world_mut()
.resource_mut::<Messages<WindowFocused>>()
.write(WindowFocused {
window: primary,
focused: false,
});
app.world_mut()
.resource_mut::<Messages<WindowFocused>>()
.write(WindowFocused {
window: secondary,
focused: true,
});
run_input_systems(&mut app);
app.world_mut().get_mut::<Window>(primary).unwrap().focused = false;
app.world_mut().despawn(secondary);
run_input_systems(&mut app);
let input_state = app.world().resource::<ImguiInputState>();
assert_eq!(input_state.focused_window(), None);
begin_frame_and_assert(&mut app, |ui| {
assert!(
!ui.is_key_down(imgui::Key::A),
"destroying the focused secondary viewport must release sticky keys"
);
assert!(
!ui.is_mouse_down(imgui::MouseButton::Left),
"destroying the focused secondary viewport must release sticky mouse buttons"
);
});
}
#[test]
fn input_keyboard_focus_lost_releases_tracked_state_without_window_message() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::KeyA,
BevyKey::Character("a".into()),
ButtonState::Pressed,
None,
));
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Right,
state: ButtonState::Pressed,
window: primary,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert!(ui.is_key_down(imgui::Key::A));
assert!(ui.is_mouse_down(imgui::MouseButton::Right));
});
app.world_mut()
.resource_mut::<Messages<KeyboardFocusLost>>()
.write(KeyboardFocusLost);
run_input_systems(&mut app);
assert_eq!(
app.world()
.resource::<ImguiInputState>()
.primary_window_focused(),
Some(false)
);
begin_frame_and_assert(&mut app, |ui| {
assert!(!ui.is_key_down(imgui::Key::A));
assert!(!ui.is_mouse_down(imgui::MouseButton::Right));
});
}
#[test]
fn input_missing_primary_window_releases_tracked_state_and_clears_window_state() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let main_viewport_id = {
let mut context = app.world_mut().get_non_send_mut::<ImguiContext>().unwrap();
let context = context.context_mut();
context
.io_mut()
.set_config_flags(imgui::ConfigFlags::VIEWPORTS_ENABLE);
context.main_viewport().id()
};
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: primary,
position: Vec2::new(12.0, 34.0),
delta: None,
});
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
primary,
KeyCode::KeyA,
BevyKey::Character("a".into()),
ButtonState::Pressed,
None,
));
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Right,
state: ButtonState::Pressed,
window: primary,
});
app.world_mut()
.resource_mut::<Messages<TouchInput>>()
.write(TouchInput {
phase: TouchPhase::Started,
position: Vec2::new(56.0, 78.0),
window: primary,
force: None,
id: 9,
});
app.world_mut()
.resource_mut::<Messages<Ime>>()
.write(Ime::Enabled { window: primary });
run_input_systems(&mut app);
assert_eq!(
app.world()
.resource::<ImguiInputState>()
.mouse_hovered_window(),
Some(primary)
);
assert_eq!(
app.world().resource::<ImguiInputState>().active_touch_id(),
Some(9)
);
assert!(app.world().resource::<ImguiInputState>().ime_enabled());
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.io().mouse_hovered_viewport(), main_viewport_id);
assert!(ui.is_key_down(imgui::Key::A));
assert!(ui.is_mouse_down(imgui::MouseButton::Right));
assert!(ui.is_mouse_down(imgui::MouseButton::Left));
});
app.world_mut().despawn(primary);
run_input_systems(&mut app);
let input_state = app.world().resource::<ImguiInputState>();
assert_eq!(input_state.primary_window_focused(), Some(false));
assert_eq!(input_state.focused_window(), None);
assert_eq!(input_state.mouse_hovered_window(), None);
assert_eq!(input_state.active_touch_id(), None);
assert!(!input_state.ime_enabled());
begin_frame_and_assert(&mut app, |ui| {
assert!(!ui.is_key_down(imgui::Key::A));
assert!(!ui.is_mouse_down(imgui::MouseButton::Right));
assert!(!ui.is_mouse_down(imgui::MouseButton::Left));
assert!(
ui.mouse_pos()[0] < -1.0e30 && ui.mouse_pos()[1] < -1.0e30,
"removing the primary window must clear the ImGui mouse position"
);
assert_eq!(
ui.io().mouse_hovered_viewport(),
imgui::Id::from(0),
"removing the primary window must clear the hovered viewport id"
);
});
}
#[test]
fn input_touch_events_drive_first_active_finger_as_touchscreen_mouse() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.resource_mut::<Messages<TouchInput>>()
.write(TouchInput {
phase: TouchPhase::Started,
position: Vec2::new(10.0, 20.0),
window: primary,
force: None,
id: 7,
});
run_input_systems(&mut app);
assert_eq!(
app.world().resource::<ImguiInputState>().active_touch_id(),
Some(7)
);
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.mouse_pos(), [10.0, 20.0]);
assert_eq!(ui.io().mouse_source(), imgui::MouseSource::TouchScreen);
assert!(ui.is_mouse_down(imgui::MouseButton::Left));
});
app.world_mut()
.resource_mut::<Messages<TouchInput>>()
.write(TouchInput {
phase: TouchPhase::Ended,
position: Vec2::new(15.0, 25.0),
window: primary,
force: None,
id: 7,
});
run_input_systems(&mut app);
assert_eq!(
app.world().resource::<ImguiInputState>().active_touch_id(),
None
);
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.mouse_pos(), [15.0, 25.0]);
assert_eq!(ui.io().mouse_source(), imgui::MouseSource::TouchScreen);
assert!(!ui.is_mouse_down(imgui::MouseButton::Left));
});
}
#[test]
fn input_stale_touched_viewport_window_clears_touch_mouse_state() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let viewport_id = imgui::Id::from(0x563);
let secondary = app
.world_mut()
.spawn((
Window {
position: WindowPosition::At(IVec2::new(120, 180)),
resolution: WindowResolution::new(640, 480),
..Default::default()
},
ImguiViewportWindow { viewport_id },
))
.id();
app.world_mut()
.resource_mut::<Messages<TouchInput>>()
.write(TouchInput {
phase: TouchPhase::Started,
position: Vec2::new(15.0, 25.0),
window: secondary,
force: None,
id: 7,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.mouse_pos(), [15.0, 25.0]);
assert_eq!(ui.io().mouse_source(), imgui::MouseSource::TouchScreen);
assert!(ui.is_mouse_down(imgui::MouseButton::Left));
});
app.world_mut().despawn(secondary);
run_input_systems(&mut app);
let input_state = app.world().resource::<ImguiInputState>();
assert_eq!(input_state.active_touch_id(), None);
begin_frame_and_assert(&mut app, |ui| {
assert!(
ui.mouse_pos()[0] < -1.0e30 && ui.mouse_pos()[1] < -1.0e30,
"destroying the touched secondary viewport must clear the ImGui mouse position"
);
assert!(!ui.is_mouse_down(imgui::MouseButton::Left));
assert_eq!(ui.io().mouse_hovered_viewport(), imgui::Id::from(0));
});
assert!(app.world().get::<Window>(primary).is_some());
}
#[test]
fn input_non_primary_window_messages_are_ignored() {
let _guard = imgui_context_guard();
let (mut app, _primary) = app_with_primary_window();
let secondary = app.world_mut().spawn(Window::default()).id();
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
secondary,
KeyCode::KeyX,
BevyKey::Character("x".into()),
ButtonState::Pressed,
Some("x"),
));
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: secondary,
position: Vec2::new(300.0, 400.0),
delta: None,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert!(current_frame_input_chars().is_empty());
assert!(!ui.is_key_down(imgui::Key::X));
assert_ne!(ui.mouse_pos(), [300.0, 400.0]);
});
}
#[test]
fn input_secondary_viewport_window_messages_use_imgui_platform_coordinates_when_viewports_are_enabled()
{
let _guard = imgui_context_guard();
let (mut app, _primary) = app_with_primary_window();
let viewport_id = imgui::Id::from(0x500);
let secondary = app
.world_mut()
.spawn((
Window {
position: WindowPosition::At(IVec2::new(200, 300)),
resolution: WindowResolution::new(640, 480),
..Default::default()
},
ImguiViewportWindow { viewport_id },
))
.id();
{
let mut window = app.world_mut().get_mut::<Window>(secondary).unwrap();
window.resolution.set_scale_factor(2.0);
}
{
let mut context = app.world_mut().get_non_send_mut::<ImguiContext>().unwrap();
let io = context.context_mut().io_mut();
io.set_config_flags(io.config_flags() | imgui::ConfigFlags::VIEWPORTS_ENABLE);
}
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: secondary,
position: Vec2::new(300.0, 400.0),
delta: None,
});
app.world_mut()
.resource_mut::<Messages<MouseButtonInput>>()
.write(MouseButtonInput {
button: BevyMouseButton::Right,
state: ButtonState::Pressed,
window: secondary,
});
app.world_mut()
.resource_mut::<Messages<MouseWheel>>()
.write(MouseWheel {
unit: MouseScrollUnit::Pixel,
x: -24.0,
y: 24.0,
window: secondary,
phase: TouchPhase::Moved,
});
app.world_mut()
.resource_mut::<Messages<KeyboardInput>>()
.write(key_input(
secondary,
KeyCode::KeyX,
BevyKey::Character("x".into()),
ButtonState::Pressed,
Some("x"),
));
app.world_mut()
.resource_mut::<Messages<Ime>>()
.write(Ime::Commit {
window: secondary,
value: "界".to_owned(),
});
app.world_mut()
.resource_mut::<Messages<WindowFocused>>()
.write(WindowFocused {
window: secondary,
focused: true,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.mouse_pos(), [400.0, 550.0]);
assert_eq!(ui.io().mouse_hovered_viewport(), viewport_id);
assert!(ui.is_mouse_down(imgui::MouseButton::Right));
assert_eq!(ui.io().mouse_wheel_h(), -1.0);
assert_eq!(ui.io().mouse_wheel(), 1.0);
assert!(ui.is_key_down(imgui::Key::X));
let chars = current_frame_input_chars();
assert!(chars.contains(&('x' as u32)));
assert!(chars.contains(&('界' as u32)));
});
}
#[test]
fn input_cursor_left_from_previous_window_does_not_clear_new_hovered_viewport_position() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let viewport_id = imgui::Id::from(0x550);
let secondary = app
.world_mut()
.spawn((
Window {
position: WindowPosition::At(IVec2::new(200, 300)),
resolution: WindowResolution::new(640, 480),
..Default::default()
},
ImguiViewportWindow { viewport_id },
))
.id();
{
let mut context = app.world_mut().get_non_send_mut::<ImguiContext>().unwrap();
let io = context.context_mut().io_mut();
io.set_config_flags(io.config_flags() | imgui::ConfigFlags::VIEWPORTS_ENABLE);
}
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: primary,
position: Vec2::new(10.0, 20.0),
delta: None,
});
run_input_systems(&mut app);
assert_eq!(
app.world()
.resource::<ImguiInputState>()
.mouse_hovered_window(),
Some(primary)
);
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: secondary,
position: Vec2::new(30.0, 40.0),
delta: None,
});
app.world_mut()
.resource_mut::<Messages<CursorLeft>>()
.write(CursorLeft { window: primary });
run_input_systems(&mut app);
assert_eq!(
app.world()
.resource::<ImguiInputState>()
.mouse_hovered_window(),
Some(secondary)
);
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.mouse_pos(), [230.0, 340.0]);
assert_eq!(ui.io().mouse_hovered_viewport(), viewport_id);
});
}
#[test]
fn input_stale_hovered_viewport_window_clears_imgui_mouse_hover() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
let viewport_id = imgui::Id::from(0x551);
let secondary = app
.world_mut()
.spawn((
Window {
position: WindowPosition::At(IVec2::new(200, 300)),
resolution: WindowResolution::new(640, 480),
..Default::default()
},
ImguiViewportWindow { viewport_id },
))
.id();
{
let mut context = app.world_mut().get_non_send_mut::<ImguiContext>().unwrap();
let io = context.context_mut().io_mut();
io.set_config_flags(io.config_flags() | imgui::ConfigFlags::VIEWPORTS_ENABLE);
}
app.world_mut()
.resource_mut::<Messages<CursorMoved>>()
.write(CursorMoved {
window: secondary,
position: Vec2::new(30.0, 40.0),
delta: None,
});
run_input_systems(&mut app);
begin_frame_and_assert(&mut app, |ui| {
assert_eq!(ui.mouse_pos(), [230.0, 340.0]);
assert_eq!(ui.io().mouse_hovered_viewport(), viewport_id);
});
app.world_mut().despawn(secondary);
run_input_systems(&mut app);
assert_eq!(
app.world()
.resource::<ImguiInputState>()
.mouse_hovered_window(),
None
);
begin_frame_and_assert(&mut app, |ui| {
assert!(
ui.mouse_pos()[0] < -1.0e30 && ui.mouse_pos()[1] < -1.0e30,
"destroying the hovered secondary viewport must clear the ImGui mouse position"
);
assert_eq!(
ui.io().mouse_hovered_viewport(),
imgui::Id::from(0),
"destroying the hovered secondary viewport must clear the hovered viewport id"
);
});
assert!(app.world().get::<Window>(primary).is_some());
}
#[test]
fn input_cursor_entered_tracks_hovered_window_without_requiring_motion() {
let _guard = imgui_context_guard();
let (mut app, primary) = app_with_primary_window();
app.world_mut()
.resource_mut::<Messages<CursorEntered>>()
.write(CursorEntered { window: primary });
run_input_systems(&mut app);
assert_eq!(
app.world()
.resource::<ImguiInputState>()
.mouse_hovered_window(),
Some(primary)
);
}
#[test]
fn input_key_mapping_covers_modifiers_and_common_keys() {
assert_eq!(
map_bevy_key_code(KeyCode::ControlLeft),
Some(imgui::Key::LeftCtrl)
);
assert_eq!(
map_bevy_key_code(KeyCode::ShiftRight),
Some(imgui::Key::RightShift)
);
assert_eq!(map_bevy_key_code(KeyCode::KeyZ), Some(imgui::Key::Z));
assert_eq!(
map_bevy_key_code(KeyCode::NumpadEnter),
Some(imgui::Key::KeypadEnter)
);
}
#[test]
fn input_capture_predicates_and_run_conditions_expose_imgui_policy_hints() {
let mut app = App::new();
app.init_resource::<ImguiInputCapture>();
assert!(!run_condition_value(&mut app, imgui_wants_pointer_input));
assert!(!run_condition_value(
&mut app,
imgui_wants_pointer_input_unless_popup_close
));
assert!(!run_condition_value(&mut app, imgui_wants_keyboard_input));
assert!(!run_condition_value(&mut app, imgui_wants_text_input));
assert!(!run_condition_value(&mut app, imgui_wants_any_input));
{
let mut capture = app.world_mut().resource_mut::<ImguiInputCapture>();
capture.want_capture_mouse = true;
capture.want_capture_mouse_unless_popup_close = true;
capture.want_capture_keyboard = true;
capture.want_text_input = true;
assert!(capture.wants_pointer_input());
assert!(capture.wants_pointer_input_unless_popup_close());
assert!(capture.wants_keyboard_input());
assert!(capture.wants_text_input());
assert!(capture.wants_any_input());
}
assert!(run_condition_value(&mut app, imgui_wants_pointer_input));
assert!(run_condition_value(
&mut app,
imgui_wants_pointer_input_unless_popup_close
));
assert!(run_condition_value(&mut app, imgui_wants_keyboard_input));
assert!(run_condition_value(&mut app, imgui_wants_text_input));
assert!(run_condition_value(&mut app, imgui_wants_any_input));
}