use std::rc::Rc;
use std::sync::Arc;
use slate_platform::{
DefaultPlatform, DefaultWindow, Event, Platform, Window, WindowId, WindowOptions, wake_run_loop,
};
use crate::app_state::{AppSignal, AppState, ErasedViewFactory};
use crate::erased_view::ErasedView;
use crate::event::{
EventCtx, ImeCommitEvent, ImeCommitHandler, ImeLifecycleEvent, ImeLifecycleHandler,
ImePreeditEvent, ImePreeditHandler, KeyEvent, KeyHandler, TextInputEvent, TextInputHandler,
};
use crate::executor::{BackgroundExecutor, Executor, RedrawRequester};
use crate::types::ElementId;
use crate::view::View;
#[cfg(all(target_os = "windows", feature = "test-hooks"))]
static FORCE_DEVICE_LOST_PENDING: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
#[cfg(all(target_os = "windows", feature = "test-hooks"))]
static FORCE_DEVICE_LOST_ARMED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
#[derive(Clone)]
pub struct AppContext {
pub(crate) runtime: Arc<slate_reactive::Runtime>,
pub(crate) background_executor: BackgroundExecutor,
pub(crate) state: Option<std::rc::Weak<AppState>>,
}
impl AppContext {
pub fn runtime(&self) -> Arc<slate_reactive::Runtime> {
self.runtime.clone()
}
pub fn create_window<V: View + 'static>(
&self,
opts: WindowOptions,
mut view_fn: impl FnMut(&AppContext) -> V + 'static,
) -> Option<WindowId> {
let weak = self.state.as_ref()?;
let state = weak.upgrade()?;
let platform_rc = state.platform.borrow().clone()?;
let window = platform_rc.create_window(opts);
let cx_clone = self.clone();
let factory: ErasedViewFactory = Box::new(move |_cx: &AppContext| -> Box<dyn ErasedView> {
Box::new(view_fn(&cx_clone))
});
Some(state.push_pending_window_create(window, factory))
}
pub fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
pub fn request_quit(&self) {
let Some(weak) = &self.state else { return };
let Some(state) = weak.upgrade() else { return };
state.pending_quit.set(true);
}
pub fn set_focus(&self, window: slate_platform::WindowId, id: ElementId) -> bool {
let Some(weak) = &self.state else {
return false;
};
let Some(state) = weak.upgrade() else {
return false;
};
let guard = state.windows.borrow();
if let Some(win) = guard.get(&window) {
win.focus_registry.borrow_mut().set_focus(id)
} else {
false
}
}
pub fn clear_focus(&self, window: slate_platform::WindowId) {
let Some(weak) = &self.state else { return };
let Some(state) = weak.upgrade() else { return };
let guard = state.windows.borrow();
if let Some(win) = guard.get(&window) {
win.focus_registry.borrow_mut().clear_focus();
}
}
pub fn focused_element(&self, window: slate_platform::WindowId) -> Option<ElementId> {
let weak = self.state.as_ref()?;
let state = weak.upgrade()?;
let guard = state.windows.borrow();
guard.get(&window)?.focus_registry.borrow().focused()
}
#[cfg(any(test, feature = "test-hooks"))]
#[doc(hidden)]
pub fn new_for_test(
runtime: Arc<slate_reactive::Runtime>,
background_executor: BackgroundExecutor,
) -> Self {
Self {
runtime,
background_executor,
state: None,
}
}
}
struct PendingPreRunWindow {
window: Arc<DefaultWindow>,
view_factory: ErasedViewFactory,
}
pub struct App {
platform: Rc<DefaultPlatform>,
window: Arc<DefaultWindow>,
pending_windows: Vec<PendingPreRunWindow>,
on_key_down: Vec<KeyHandler>,
on_key_up: Vec<KeyHandler>,
on_text_input: Vec<TextInputHandler>,
on_ime_preedit: Vec<ImePreeditHandler>,
on_ime_commit: Vec<ImeCommitHandler>,
on_ime_enabled: Vec<ImeLifecycleHandler>,
on_ime_disabled: Vec<ImeLifecycleHandler>,
}
impl App {
pub fn new(options: WindowOptions) -> Self {
let platform = Rc::new(DefaultPlatform::new());
let window = platform.create_window(options);
Self {
platform,
window,
pending_windows: Vec::new(),
on_key_down: Vec::new(),
on_key_up: Vec::new(),
on_text_input: Vec::new(),
on_ime_preedit: Vec::new(),
on_ime_commit: Vec::new(),
on_ime_enabled: Vec::new(),
on_ime_disabled: Vec::new(),
}
}
pub fn create_window<V: View + 'static>(
&mut self,
opts: WindowOptions,
mut view_fn: impl FnMut(&AppContext) -> V + 'static,
) -> WindowId {
let window = self.platform.create_window(opts);
let id = window.id();
let factory: ErasedViewFactory =
Box::new(move |cx: &AppContext| -> Box<dyn ErasedView> { Box::new(view_fn(cx)) });
self.pending_windows.push(PendingPreRunWindow {
window,
view_factory: factory,
});
id
}
pub fn on_key_down(mut self, handler: impl FnMut(&KeyEvent, &mut EventCtx) + 'static) -> Self {
self.on_key_down.push(Box::new(handler));
self
}
pub fn on_key_up(mut self, handler: impl FnMut(&KeyEvent, &mut EventCtx) + 'static) -> Self {
self.on_key_up.push(Box::new(handler));
self
}
pub fn on_text_input(
mut self,
handler: impl FnMut(&TextInputEvent, &mut EventCtx) + 'static,
) -> Self {
self.on_text_input.push(Box::new(handler));
self
}
pub fn on_ime_preedit(
mut self,
handler: impl FnMut(&ImePreeditEvent, &mut EventCtx) + 'static,
) -> Self {
self.on_ime_preedit.push(Box::new(handler));
self
}
pub fn on_ime_commit(
mut self,
handler: impl FnMut(&ImeCommitEvent, &mut EventCtx) + 'static,
) -> Self {
self.on_ime_commit.push(Box::new(handler));
self
}
pub fn on_ime_enabled(
mut self,
handler: impl FnMut(&ImeLifecycleEvent, &mut EventCtx) + 'static,
) -> Self {
self.on_ime_enabled.push(Box::new(handler));
self
}
pub fn on_ime_disabled(
mut self,
handler: impl FnMut(&ImeLifecycleEvent, &mut EventCtx) + 'static,
) -> Self {
self.on_ime_disabled.push(Box::new(handler));
self
}
pub fn run<V: View>(self, mut view_fn: impl FnMut(&AppContext) -> V + 'static) {
let App {
platform,
window,
pending_windows,
on_key_down,
on_key_up,
on_text_input,
on_ime_preedit,
on_ime_commit,
on_ime_enabled,
on_ime_disabled,
} = self;
let redraw_requester = RedrawRequester::new(wake_run_loop);
let executor = Executor::new(redraw_requester.clone());
let runtime = slate_reactive::Runtime::new();
let background_executor = executor.background.clone();
let state = Rc::new(AppState::new(
executor,
redraw_requester.clone(),
runtime.clone(),
));
state.install_platform(platform.clone());
let first_window_id = window.id();
state.install_window(window.clone());
let cx = AppContext {
runtime,
background_executor,
state: Some(Rc::downgrade(&state)),
};
state.install_key_handlers(on_key_down, on_key_up, on_text_input);
state.install_ime_handlers(
on_ime_preedit,
on_ime_commit,
on_ime_enabled,
on_ime_disabled,
);
let cx_clone = cx.clone();
let first_erased: ErasedViewFactory =
Box::new(move |_cx: &AppContext| -> Box<dyn ErasedView> {
Box::new(view_fn(&cx_clone))
});
let mut resumed_factories: Vec<(WindowId, ErasedViewFactory)> =
Vec::with_capacity(1 + pending_windows.len());
resumed_factories.push((first_window_id, first_erased));
for pre in pending_windows {
let id = pre.window.id();
state.install_window(pre.window);
resumed_factories.push((id, pre.view_factory));
}
let platform_ref = &*platform;
let state_ref = state.clone();
platform.run(move |event| {
if state_ref.pending_quit.get() {
platform_ref.quit();
return;
}
#[cfg(all(target_os = "windows", feature = "test-hooks"))]
if FORCE_DEVICE_LOST_PENDING.swap(false, std::sync::atomic::Ordering::AcqRel) {
log::warn!(
target: "slate::test_hooks",
"SLATE_FORCE_DEVICE_LOST_MS elapsed - firing force_renderer_device_lost"
);
let fired = state_ref.force_renderer_device_lost(first_window_id);
log::warn!(
target: "slate::test_hooks",
"force_renderer_device_lost returned fired={fired}"
);
}
let signal = match event {
Event::Resumed => {
let mut had_failure = false;
for (id, factory) in resumed_factories.iter_mut() {
if state_ref
.init_surfaces(*id, factory, &cx, platform_ref)
.is_err()
{
had_failure = true;
}
}
if had_failure {
AppSignal::RequestQuit
} else {
#[cfg(all(target_os = "windows", feature = "test-hooks"))]
arm_force_device_lost_trigger();
AppSignal::RequestRedraw {
window: first_window_id,
}
}
}
Event::WindowResized {
window,
physical_size,
..
} => {
state_ref.handle_window_resized(window, physical_size);
AppSignal::None
}
Event::WindowRedrawRequested { window, .. } => state_ref.dispatch_redraw(window),
Event::WindowCloseRequested { .. } => {
AppSignal::None
}
Event::WindowDestroyed { window, .. } => state_ref.handle_window_destroyed(window),
Event::Wake => state_ref.handle_wake(),
Event::MouseDown {
window,
position,
button,
modifiers,
..
} => state_ref.dispatch_mouse_down(window, position, button, modifiers),
Event::MouseUp {
window,
position,
button,
modifiers,
..
} => state_ref.dispatch_mouse_up(window, position, button, modifiers),
Event::MouseMoved {
window,
position,
modifiers,
..
} => state_ref.dispatch_mouse_moved(window, position, modifiers),
Event::MouseScrolled {
window,
position,
delta_x,
delta_y,
precise,
modifiers,
..
} => state_ref.dispatch_mouse_scrolled(
window, position, delta_x, delta_y, precise, modifiers,
),
Event::MouseExited { window, .. } => state_ref.dispatch_mouse_exited(window),
Event::CaptureLost { window, .. } => state_ref.dispatch_capture_lost(window),
Event::DeviceLost { window, fatal, .. } => {
state_ref.dispatch_device_lost(window, fatal)
}
Event::DeviceRestored { window, .. } => state_ref.dispatch_device_restored(window),
Event::KeyDown {
window,
code,
key,
modifiers,
is_repeat,
..
} => state_ref.dispatch_key_down(window, code, key, modifiers, is_repeat),
Event::KeyUp {
window,
code,
key,
modifiers,
..
} => state_ref.dispatch_key_up(window, code, key, modifiers),
Event::TextInput { window, text, .. } => {
state_ref.dispatch_text_input(window, text)
}
Event::ImeEnabled { window, .. } => state_ref.dispatch_ime_enabled(window),
Event::ImePreedit {
window,
text,
cursor_byte_offset,
selection,
..
} => state_ref.dispatch_ime_preedit(window, text, cursor_byte_offset, selection),
Event::ImeCommit { window, text, .. } => {
state_ref.dispatch_ime_commit(window, text)
}
Event::ImeDisabled { window, .. } => state_ref.dispatch_ime_disabled(window),
Event::Exiting => {
log::info!("exiting");
AppSignal::None
}
_ => AppSignal::None,
};
match signal {
AppSignal::RequestQuit => platform_ref.quit(),
AppSignal::RequestRedraw { window } => state_ref.request_redraw_for(window),
AppSignal::None => {}
}
state_ref.drain_pending_window_creates(&cx, platform_ref);
});
}
}
#[cfg(all(target_os = "windows", feature = "test-hooks"))]
fn arm_force_device_lost_trigger() {
use std::sync::atomic::Ordering;
if FORCE_DEVICE_LOST_ARMED.swap(true, Ordering::AcqRel) {
return;
}
let ms: u64 = match std::env::var("SLATE_FORCE_DEVICE_LOST_MS") {
Ok(s) => match s.trim().parse() {
Ok(n) => n,
Err(_) => {
log::warn!(
target: "slate::test_hooks",
"SLATE_FORCE_DEVICE_LOST_MS='{s}' is not a valid u64 - skipping"
);
return;
}
},
Err(_) => return,
};
log::warn!(
target: "slate::test_hooks",
"SLATE_FORCE_DEVICE_LOST_MS armed: will fire force_renderer_device_lost in {ms}ms"
);
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(ms));
FORCE_DEVICE_LOST_PENDING.store(true, Ordering::Release);
wake_run_loop();
});
}