1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 13:20:05 -04:00

Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports

This commit is contained in:
Konkitoman
2023-09-19 17:22:31 +03:00
91 changed files with 3056 additions and 1525 deletions

View File

@@ -1158,6 +1158,7 @@ impl Storage for DummyStorage {
/// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key.
#[cfg(feature = "ron")]
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
crate::profile_function!(key);
storage
.get_string(key)
.and_then(|value| match ron::from_str(&value) {
@@ -1172,6 +1173,7 @@ pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &st
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
#[cfg(feature = "ron")]
pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) {
crate::profile_function!(key);
match ron::ser::to_string(value) {
Ok(string) => storage.set_string(key, string),
Err(err) => log::error!("eframe failed to encode data using ron: {}", err),

View File

@@ -324,7 +324,6 @@ pub type Result<T> = std::result::Result<T, Error>;
// ---------------------------------------------------------------------------
#[cfg(not(target_arch = "wasm32"))]
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
@@ -333,6 +332,7 @@ mod profiling_scopes {
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
@@ -342,11 +342,12 @@ mod profiling_scopes {
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(unused_imports)]
pub(crate) use profiling_scopes::*;

View File

@@ -191,6 +191,8 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
crate::profile_function!();
use cocoa::{
appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow},
base::{id, nil},
@@ -221,12 +223,15 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
png_bytes.len() as u64,
);
let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data);
crate::profile_scope!("setApplicationIconImage_");
app.setApplicationIconImage_(app_icon);
}
// Change the title in the top bar - for python processes this would be again "python" otherwise.
let main_menu = app.mainMenu();
let app_menu: id = msg_send![main_menu.itemAtIndex_(0), submenu];
crate::profile_scope!("setTitle_");
app_menu.setTitle_(NSString::alloc(nil).init_str(title));
// The title in the Dock apparently can't be changed.

View File

@@ -178,6 +178,7 @@ pub fn apply_native_options_to_window(
native_options: &crate::NativeOptions,
window_settings: Option<WindowSettings>,
) {
crate::profile_function!();
use winit::window::WindowLevel;
window.set_window_level(if native_options.always_on_top {
WindowLevel::AlwaysOnTop
@@ -443,6 +444,8 @@ impl EpiIntegration {
egui_winit: &mut egui_winit::State,
viewport_id: ViewportId,
) -> EventResponse {
crate::profile_function!();
use winit::event::{ElementState, MouseButton, WindowEvent};
match event {
@@ -614,6 +617,7 @@ const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
const STORAGE_WINDOW_KEY: &str = "window";
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
crate::profile_function!();
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
@@ -623,6 +627,7 @@ pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<Windo
}
pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
crate::profile_function!();
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)

View File

@@ -1,5 +1,6 @@
use std::{
collections::HashMap,
io::Write,
path::{Path, PathBuf},
};
@@ -31,6 +32,7 @@ pub struct FileStorage {
impl Drop for FileStorage {
fn drop(&mut self) {
if let Some(join_handle) = self.last_save_join_handle.take() {
crate::profile_scope!("wait_for_save");
join_handle.join().ok();
}
}
@@ -39,6 +41,7 @@ impl Drop for FileStorage {
impl FileStorage {
/// Store the state in this .ron file.
fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
crate::profile_function!();
let ron_filepath: PathBuf = ron_filepath.into();
log::debug!("Loading app state from {:?}…", ron_filepath);
Self {
@@ -51,6 +54,7 @@ impl FileStorage {
/// Find a good place to put the files that the OS likes.
pub fn from_app_id(app_id: &str) -> Option<Self> {
crate::profile_function!(app_id);
if let Some(data_dir) = storage_dir(app_id) {
if let Err(err) = std::fs::create_dir_all(&data_dir) {
log::warn!(
@@ -83,6 +87,7 @@ impl crate::Storage for FileStorage {
fn flush(&mut self) {
if self.dirty {
crate::profile_function!();
self.dirty = false;
let file_path = self.ron_filepath.clone();
@@ -122,10 +127,13 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
match std::fs::File::create(file_path) {
Ok(file) => {
let mut writer = std::io::BufWriter::new(file);
let config = Default::default();
if let Err(err) = ron::ser::to_writer_pretty(file, &kv, config) {
log::warn!("Failed to serialize app state: {err}");
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
{
log::warn!("Failed to serialize app state: {}", err);
} else {
log::trace!("Persisted to {:?}", file_path);
}
@@ -142,6 +150,7 @@ fn read_ron<T>(ron_path: impl AsRef<Path>) -> Option<T>
where
T: serde::de::DeserializeOwned,
{
crate::profile_function!();
match std::fs::File::open(ron_path) {
Ok(file) => {
let reader = std::io::BufReader::new(file);

View File

@@ -120,6 +120,7 @@ trait WinitApp {
fn create_event_loop_builder(
native_options: &mut epi::NativeOptions,
) -> EventLoopBuilder<UserEvent> {
crate::profile_function!();
let mut event_loop_builder = winit::event_loop::EventLoopBuilder::with_user_event();
if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) {
@@ -129,6 +130,14 @@ fn create_event_loop_builder(
event_loop_builder
}
fn create_event_loop(native_options: &mut epi::NativeOptions) -> EventLoop<UserEvent> {
crate::profile_function!();
let mut builder = create_event_loop_builder(native_options);
crate::profile_scope!("EventLoopBuilder::build");
builder.build()
}
/// Access a thread-local event loop.
///
/// We reuse the event-loop so we can support closing and opening an eframe window
@@ -145,8 +154,7 @@ fn with_event_loop<R>(
// do that as part of the lazy thread local storage initialization and so we instead
// create the event loop lazily here
let mut event_loop = event_loop.borrow_mut();
let event_loop = event_loop
.get_or_insert_with(|| create_event_loop_builder(&mut native_options).build());
let event_loop = event_loop.get_or_insert_with(|| create_event_loop(&mut native_options));
f(event_loop, native_options)
})
}
@@ -165,6 +173,8 @@ fn run_and_return(
let mut returned_result = Ok(());
event_loop.run_return(|event, event_loop, control_flow| {
crate::profile_scope!("winit_event", short_event_description(&event));
WINIT_EVENT_LOOP.with(|row_event_loop| *row_event_loop.write() = event_loop);
let events = match &event {
@@ -324,6 +334,8 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
let mut windows_next_repaint_times = HashMap::default();
event_loop.run(move |event, event_loop, control_flow| {
crate::profile_scope!("winit_event", short_event_description(&event));
WINIT_EVENT_LOOP.with(|row_event_loop| *row_event_loop.write() = event_loop);
let events = match event {
@@ -536,6 +548,8 @@ mod glow_integration {
native_options: &epi::NativeOptions,
event_loop: &EventLoopWindowTarget<UserEvent>,
) -> Result<Self> {
crate::profile_function!();
use glutin::prelude::*;
// convert native options to glutin options
let hardware_acceleration = match native_options.hardware_acceleration {
@@ -576,26 +590,35 @@ mod glow_integration {
"trying to create glutin Display with config: {:?}",
&config_template_builder
);
// create gl display. this may probably create a window too on most platforms. definitely on `MS windows`. never on android.
let (window, gl_config) = glutin_winit::DisplayBuilder::new()
// Create GL display. This may probably create a window too on most platforms. Definitely on `MS windows`. Never on Android.
let display_builder = glutin_winit::DisplayBuilder::new()
// we might want to expose this option to users in the future. maybe using an env var or using native_options.
.with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150
.with_window_builder(Some(create_winit_window_builder(&window_builder)))
.build(
event_loop,
config_template_builder.clone(),
|mut config_iterator| {
let config = config_iterator.next().expect(
.with_window_builder(Some(create_winit_window_builder(&window_builder)));
let (window, gl_config) = {
crate::profile_scope!("DisplayBuilder::build");
display_builder
.build(
event_loop,
config_template_builder.clone(),
|mut config_iterator| {
let config = config_iterator.next().expect(
"failed to find a matching configuration for creating glutin config",
);
log::debug!(
"using the first config from config picker closure. config: {:?}",
&config
);
config
},
)
.map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))?;
log::debug!(
"using the first config from config picker closure. config: {:?}",
&config
);
config
},
)
.map_err(|e| {
crate::Error::NoGlutinConfigs(config_template_builder.build(), e)
})?
};
let gl_display = gl_config.display();
log::debug!(
@@ -615,10 +638,15 @@ mod glow_integration {
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
.with_context_api(glutin::context::ContextApi::Gles(None))
.build(raw_window_handle);
let gl_context = match gl_config
.display()
.create_context(&gl_config, &context_attributes)
{
let gl_context_result = {
crate::profile_scope!("create_context");
gl_config
.display()
.create_context(&gl_config, &context_attributes)
};
let gl_context = match gl_context_result {
Ok(it) => it,
Err(err) => {
log::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {err}");
@@ -675,6 +703,8 @@ mod glow_integration {
///
/// we presently assume that we will
fn on_resume(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
crate::profile_function!();
let values = self
.windows
.values()
@@ -849,6 +879,7 @@ mod glow_integration {
native_options: epi::NativeOptions,
app_creator: epi::AppCreator,
) -> Self {
crate::profile_function!();
Self {
repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())),
app_name: app_name.to_owned(),
@@ -889,6 +920,7 @@ mod glow_integration {
}
let gl = unsafe {
crate::profile_scope!("glow::Context::from_loader_function");
glow::Context::from_loader_function(|s| {
let s = std::ffi::CString::new(s)
.expect("failed to construct C string from string for gl proc address");
@@ -901,6 +933,7 @@ mod glow_integration {
}
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
crate::profile_function!();
let storage = epi_integration::create_storage(
self.native_options
.app_id
@@ -960,14 +993,6 @@ mod glow_integration {
let theme = system_theme.unwrap_or(self.native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
gl_window
.window(ViewportId::MAIN)
.read()
.window
.as_ref()
.unwrap()
.read()
.set_ime_allowed(true);
if self.native_options.mouse_passthrough {
gl_window
.window(ViewportId::MAIN)
@@ -1286,7 +1311,10 @@ mod glow_integration {
}
fn save_and_destroy(&mut self) {
crate::profile_function!();
if let Some(running) = self.running.write().take() {
crate::profile_function!();
running.integration.write().save(
running.app.write().as_mut(),
running
@@ -1543,6 +1571,8 @@ mod glow_integration {
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> Result<EventResult> {
crate::profile_function!();
Ok(match event {
winit::event::Event::Resumed => {
// first resume event.
@@ -1632,7 +1662,7 @@ mod glow_integration {
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
let glutin_ctx = &mut *running.glutin_ctx.write();
if physical_size.width > 0 && physical_size.height > 0 {
if 0 < physical_size.width && 0 < physical_size.height {
if let Some(id) = glutin_ctx.window_maps.get(window_id) {
glutin_ctx.resize(*id, *physical_size);
}
@@ -1723,11 +1753,14 @@ mod glow_integration {
EventResult::Wait
}
}
#[cfg(feature = "accesskit")]
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
accesskit_winit::ActionRequestEvent { request, window_id },
)) => {
if let Some(running) = self.running.read().as_ref() {
crate::profile_scope!("on_accesskit_action_request");
let glutin_ctx = running.glutin_ctx.read();
if let Some(viewport_id) = glutin_ctx.window_maps.get(window_id).copied() {
if let Some(viewport) = glutin_ctx.windows.get(&viewport_id).cloned() {
@@ -1764,14 +1797,14 @@ mod glow_integration {
run_and_return(event_loop, glow_eframe)
})
} else {
let event_loop = create_event_loop_builder(&mut native_options).build();
let event_loop = create_event_loop(&mut native_options);
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, glow_eframe);
}
#[cfg(target_os = "ios")]
{
let event_loop = create_event_loop_builder(&mut native_options).build();
let event_loop = create_event_loop(&mut native_options);
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, glow_eframe);
}
@@ -1847,6 +1880,7 @@ mod wgpu_integration {
native_options: epi::NativeOptions,
app_creator: epi::AppCreator,
) -> Self {
crate::profile_function!();
#[cfg(feature = "__screenshot")]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
@@ -1870,10 +1904,15 @@ mod wgpu_integration {
native_options: &NativeOptions,
) -> std::result::Result<(winit::window::Window, ViewportBuilder), winit::error::OsError>
{
crate::profile_function!();
let window_settings = epi_integration::load_window_settings(storage);
let window_builder =
epi_integration::window_builder(event_loop, title, native_options, window_settings);
let window = create_winit_window_builder(&window_builder).build(event_loop)?;
let window = {
crate::profile_scope!("WindowBuilder::build");
create_winit_window_builder(&window_builder).build(event_loop)?
};
epi_integration::apply_native_options_to_window(
&window,
native_options,
@@ -1935,6 +1974,7 @@ mod wgpu_integration {
fn set_window(&mut self, id: ViewportId) -> std::result::Result<(), egui_wgpu::WgpuError> {
if let Some(running) = &mut self.running {
crate::profile_function!();
if let Some(Window { window, .. }) = running.windows.read().get(&id) {
let window = window.clone();
if let Some(win) = &window {
@@ -1966,6 +2006,8 @@ mod wgpu_integration {
window: winit::window::Window,
builder: ViewportBuilder,
) -> std::result::Result<(), egui_wgpu::WgpuError> {
crate::profile_function!();
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = egui_wgpu::winit::Painter::new(
self.native_options.wgpu_options.clone(),
@@ -2001,8 +2043,6 @@ mod wgpu_integration {
let theme = system_theme.unwrap_or(self.native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
window.set_ime_allowed(true);
{
let event_loop_proxy = self.repaint_proxy.clone();
@@ -2026,7 +2066,7 @@ mod wgpu_integration {
let app_creator = std::mem::take(&mut self.app_creator)
.expect("Single-use AppCreator has unexpectedly already been taken");
let mut app = app_creator(&epi::CreationContext {
let cc = epi::CreationContext {
egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info().clone(),
storage: integration.frame.storage(),
@@ -2035,7 +2075,11 @@ mod wgpu_integration {
wgpu_render_state,
raw_display_handle: window.raw_display_handle(),
raw_window_handle: window.raw_window_handle(),
});
};
let mut app = {
crate::profile_scope!("user_app_creator");
app_creator(&cc)
};
if app.warm_up_enabled() {
integration.warm_up(app.as_mut(), &window, &mut state);
@@ -2189,6 +2233,7 @@ mod wgpu_integration {
fn save_and_destroy(&mut self) {
if let Some(mut running) = self.running.take() {
crate::profile_function!();
if let Some(Window { window, .. }) = running.windows.read().get(&ViewportId::MAIN) {
running
.integration
@@ -2403,7 +2448,9 @@ mod wgpu_integration {
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> Result<EventResult> {
crate::profile_function!();
self.build_windows(event_loop);
Ok(match event {
winit::event::Event::Resumed => {
if let Some(running) = &self.running {
@@ -2484,7 +2531,7 @@ mod wgpu_integration {
if let Some(viewport_id) =
running.windows_id.read().get(window_id).copied()
{
if physical_size.width > 0 && physical_size.height > 0 {
if 0 < physical_size.width && 0 < physical_size.height {
running.painter.write().on_window_resized(
viewport_id,
physical_size.width,
@@ -2600,14 +2647,14 @@ mod wgpu_integration {
run_and_return(event_loop, wgpu_eframe)
})
} else {
let event_loop = create_event_loop_builder(&mut native_options).build();
let event_loop = create_event_loop(&mut native_options);
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe);
}
#[cfg(target_os = "ios")]
{
let event_loop = create_event_loop_builder(&mut native_options).build();
let event_loop = create_event_loop(&mut native_options);
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe);
}
@@ -2628,3 +2675,67 @@ fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Opti
None
}
}
// For the puffin profiler!
#[allow(dead_code)] // Only used for profiling
fn short_event_description(event: &winit::event::Event<'_, UserEvent>) -> &'static str {
use winit::event::{DeviceEvent, Event, StartCause, WindowEvent};
match event {
Event::Suspended => "Event::Suspended",
Event::Resumed => "Event::Resumed",
Event::MainEventsCleared => "Event::MainEventsCleared",
Event::RedrawRequested(_) => "Event::RedrawRequested",
Event::RedrawEventsCleared => "Event::RedrawEventsCleared",
Event::LoopDestroyed => "Event::LoopDestroyed",
Event::UserEvent(user_event) => match user_event {
UserEvent::RequestRepaint { .. } => "UserEvent::RequestRepaint",
#[cfg(feature = "accesskit")]
UserEvent::AccessKitActionRequest(_) => "UserEvent::AccessKitActionRequest",
},
Event::DeviceEvent { event, .. } => match event {
DeviceEvent::Added { .. } => "DeviceEvent::Added",
DeviceEvent::Removed { .. } => "DeviceEvent::Removed",
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
DeviceEvent::Button { .. } => "DeviceEvent::Button",
DeviceEvent::Key { .. } => "DeviceEvent::Key",
DeviceEvent::Text { .. } => "DeviceEvent::Text",
},
Event::NewEvents(start_cause) => match start_cause {
StartCause::ResumeTimeReached { .. } => "NewEvents::ResumeTimeReached",
StartCause::WaitCancelled { .. } => "NewEvents::WaitCancelled",
StartCause::Poll => "NewEvents::Poll",
StartCause::Init => "NewEvents::Init",
},
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized { .. } => "WindowEvent::Resized",
WindowEvent::Moved { .. } => "WindowEvent::Moved",
WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested",
WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed",
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled",
WindowEvent::ReceivedCharacter { .. } => "WindowEvent::ReceivedCharacter",
WindowEvent::Focused { .. } => "WindowEvent::Focused",
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
WindowEvent::Ime { .. } => "WindowEvent::Ime",
WindowEvent::CursorMoved { .. } => "WindowEvent::CursorMoved",
WindowEvent::CursorEntered { .. } => "WindowEvent::CursorEntered",
WindowEvent::CursorLeft { .. } => "WindowEvent::CursorLeft",
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
WindowEvent::TouchpadMagnify { .. } => "WindowEvent::TouchpadMagnify",
WindowEvent::SmartMagnify { .. } => "WindowEvent::SmartMagnify",
WindowEvent::TouchpadRotate { .. } => "WindowEvent::TouchpadRotate",
WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",
WindowEvent::AxisMotion { .. } => "WindowEvent::AxisMotion",
WindowEvent::Touch { .. } => "WindowEvent::Touch",
WindowEvent::ScaleFactorChanged { .. } => "WindowEvent::ScaleFactorChanged",
WindowEvent::ThemeChanged { .. } => "WindowEvent::ThemeChanged",
WindowEvent::Occluded { .. } => "WindowEvent::Occluded",
},
}
}

View File

@@ -101,14 +101,13 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> {
// When input lost focus, focus on it again.
// It is useful when user click somewhere outside canvas.
let input_refocus = input.clone();
runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{AGENT_ID}').focus()"
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
let input_refocus = input_refocus.clone();
call_after_delay(std::time::Duration::from_millis(10), move || {
input_refocus.focus().ok();
});
})?;
body.append_child(&input)?;