///|
/// Application callbacks run only on Orby's UI thread.
pub(open) trait App {
fn started(Self, ActiveApp) -> Unit raise AppError = _
fn window_event(Self, ActiveApp, WindowId, WindowEvent) -> Unit = _
fn proxy_message(Self, ActiveApp, Bytes) -> Unit = _
fn about_to_wait(Self, ActiveApp) -> Unit = _
fn exiting(Self, ActiveApp) -> Unit = _
}
///|
impl App with fn started(_self, _app) {
()
}
///|
impl App with fn window_event(_self, _app, _id, _event) {
()
}
///|
impl App with fn proxy_message(_self, _app, _message) {
()
}
///|
impl App with fn about_to_wait(_self, _app) {
()
}
///|
impl App with fn exiting(_self, _app) {
()
}
///|
pub struct EventLoop {
priv backend : NativeBackend
priv proxy_generation : UInt64
priv next_window_id : Ref[Int]
priv runtime_failure : Ref[String?]
} derive(Debug)
///|
pub struct ActiveApp {
priv backend : NativeBackend
priv proxy_generation : UInt64
priv next_window_id : Ref[Int]
priv runtime_failure : Ref[String?]
} derive(Debug)
///|
priv struct ExternalAppState[A] {
app : A
active : ActiveApp
mut terminated : Bool
mut exit_code : Int?
}
///|
let event_loop_active : Ref[Bool] = Ref(false)
///|
/// A manually pumped application loop for integration with a foreign runtime.
///
/// `poll` and `terminate` must run on Orby's UI thread. The callback returned
/// by `wakeup_callback_for_foreign_thread` is the sole exception: it is safe
/// to invoke from a foreign thread because it calls directly into the native
/// backend without accessing MoonBit-managed application state.
pub struct ExternalAppLoop[A] {
priv backend : NativeBackend
priv state : Ref[ExternalAppState[A]]
}
///|
fn reserve_event_loop() -> Unit raise InitError {
if event_loop_active.val || native_event_loop_is_active() {
raise InitError::AlreadyActive
}
event_loop_active.val = true
}
///|
fn release_event_loop() -> Unit {
event_loop_active.val = false
}
///|
pub fn EventLoop::new() -> EventLoop raise InitError {
reserve_event_loop()
let backend = initialize_backend() catch {
error => {
release_event_loop()
if native_event_loop_is_active() {
raise InitError::AlreadyActive
}
raise error
}
}
let proxy_generation = native_open_proxy(backend)
{
backend,
proxy_generation,
next_window_id: Ref(1),
runtime_failure: Ref(None),
}
}
///|
fn EventLoop::active(self : EventLoop) -> ActiveApp {
{
backend: self.backend,
proxy_generation: self.proxy_generation,
next_window_id: self.next_window_id,
runtime_failure: self.runtime_failure,
}
}
///|
/// Requests a checked application failure from a UI callback. The first reason
/// wins. Callers must destroy child runtimes before their parent windows.
pub fn ActiveApp::fail(self : ActiveApp, reason : String) -> Unit {
if record_runtime_failure(self.runtime_failure, reason) {
native_exit(self.backend, 1)
}
}
///|
fn record_runtime_failure(failure : Ref[String?], reason : String) -> Bool {
match failure.val {
None => {
failure.val = Some(reason)
true
}
Some(_) => false
}
}
///|
pub fn ActiveApp::exit(self : ActiveApp) -> Unit {
native_exit(self.backend, 0)
}
///|
pub fn ActiveApp::exit_with_code(self : ActiveApp, code : Int) -> Unit {
native_exit(self.backend, code)
}
///|
pub fn ActiveApp::set_control_flow(
self : ActiveApp,
control_flow : ControlFlow,
) -> Unit {
native_set_control_flow(
self.backend,
match control_flow {
Poll => true
Wait => false
},
)
}
///|
pub fn ActiveApp::create_window(
self : ActiveApp,
options : WindowOptions,
) -> Window raise WindowError {
let id = self.next_window_id.val
self.next_window_id.val = id + 1
let handle = native_create_window(
self.backend,
@utf8.encode(options.title, bom=false),
options.size.width,
options.size.height,
options.visible,
options.resizable,
id,
)
if handle == 0UL {
raise WindowError::CreationFailed("native window creation returned null")
}
{
id_: WindowId::from_raw(id),
handle,
alive: Ref(true),
backend: self.backend,
}
}
///|
pub fn EventLoop::available_monitors(self : EventLoop) -> Array[Monitor] {
available_monitors_for(self.backend)
}
///|
pub fn EventLoop::monitor(self : EventLoop, id : MonitorId) -> Monitor? {
monitor_from_index(self.backend, id.index())
}
///|
pub fn EventLoop::primary_monitor(self : EventLoop) -> Monitor? {
monitor_from_index(self.backend, native_primary_monitor_index(self.backend))
}
///|
/// Starts an application without taking ownership of the native event loop.
///
/// Call `poll` repeatedly from the embedding event loop, then call `terminate`
/// exactly once after the embedding runtime has stopped. This is intended for
/// integrations such as `moonbitlang/async`'s external event loop support.
pub fn[A : App] EventLoop::start_external_app(
self : EventLoop,
app : A,
) -> ExternalAppLoop[A] raise AppError {
let active = self.active()
let state = Ref({ app, active, terminated: false, exit_code: None })
native_set_event_callback(self.backend, fn(
kind,
id,
arg0,
arg1,
argd0,
argd1,
) {
let current = state.val
if current.active.runtime_failure.val is None {
dispatch_event(
current.app,
current.active,
kind,
id,
arg0,
arg1,
argd0,
argd1,
)
}
})
state.val.app.started(active) catch {
error => {
ignore(native_finish(self.backend))
release_event_loop()
state.val.app.exiting(active)
raise error
}
}
{ backend: self.backend, state }
}
///|
/// Pumps pending native events, then waits for no longer than `timeout`
/// milliseconds. Omitting `timeout` waits indefinitely. `Exited(code)` means
/// native cleanup and `App::exiting` have already run.
pub fn[A : App] ExternalAppLoop::poll(
self : ExternalAppLoop[A],
timeout? : Int,
) -> ExternalPoll raise AppError {
if self.state.val.terminated {
return Exited(self.state.val.exit_code.unwrap_or(0))
}
let status = native_poll(self.backend, timeout?)
if status < 0 {
ignore(self.terminate() catch { _ => 0 })
raise AppError::RuntimeFailed("native event loop polling failed")
}
match self.state.val.active.runtime_failure.val {
Some(reason) => {
ignore(self.terminate() catch { _ => 0 })
raise AppError::RuntimeFailed(reason)
}
None => if status == 0 { Exited(self.terminate()) } else { Continue }
}
}
///|
/// Returns a wakeup callback suitable for a native foreign thread.
///
/// The callback does not inspect application state and only invokes the native
/// backend wake primitive. Do not wrap it in another MoonBit callback that
/// captures MoonBit-managed values before passing it to a foreign thread.
pub fn[A] ExternalAppLoop::wakeup_callback_for_foreign_thread(
self : ExternalAppLoop[A],
) -> FuncRef[() -> Unit] {
let _ = self
() => wake_external_loop_from_foreign_thread()
}
///|
/// Releases native event-loop state and invokes the application's `exiting`
/// callback. It is idempotent so cleanup paths can safely call it after an
/// initialization or runtime failure.
pub fn[A : App] ExternalAppLoop::terminate(
self : ExternalAppLoop[A],
) -> Int raise AppError {
if self.state.val.terminated {
return self.state.val.exit_code.unwrap_or(0)
}
self.state.val.terminated = true
let code = native_finish(self.backend)
self.state.val.exit_code = Some(code)
release_event_loop()
self.state.val.app.exiting(self.state.val.active)
match self.state.val.active.runtime_failure.val {
Some(reason) => raise AppError::RuntimeFailed(reason)
None => code
}
}
///|
pub fn[A : App] EventLoop::run_app(
self : EventLoop,
app : A,
) -> Int raise AppError {
let active = self.active()
let startup_failure : Ref[String?] = Ref(None)
native_set_event_callback(self.backend, fn(
kind,
id,
arg0,
arg1,
argd0,
argd1,
) {
if active.runtime_failure.val is None {
dispatch_event(app, active, kind, id, arg0, arg1, argd0, argd1)
}
})
app.started(active) catch {
AppError::StartupFailed(reason) => {
startup_failure.val = Some(reason)
native_exit(self.backend, 1)
}
AppError::RuntimeFailed(reason) => {
startup_failure.val = Some(reason)
native_exit(self.backend, 1)
}
}
let code = native_run(self.backend)
release_event_loop()
app.exiting(active)
match startup_failure.val {
Some(reason) => raise AppError::StartupFailed(reason)
None =>
match active.runtime_failure.val {
Some(reason) => raise AppError::RuntimeFailed(reason)
None => code
}
}
}
///|
fn available_monitors_for(backend : NativeBackend) -> Array[Monitor] {
let monitors : Array[Monitor] = []
let count = native_monitor_count(backend)
for index in 0.. monitors.push(monitor)
None => ()
}
}
monitors
}
///|
fn monitor_from_index(backend : NativeBackend, index : Int) -> Monitor? {
if index < 0 || index >= native_monitor_count(backend) {
None
} else {
match MonitorId::from_index(index) {
Some(id) =>
Some(
Monitor::new(
id~,
x=native_monitor_metric(backend, index, 0),
y=native_monitor_metric(backend, index, 1),
width=native_monitor_metric(backend, index, 2),
height=native_monitor_metric(backend, index, 3),
work_x=native_monitor_metric(backend, index, 4),
work_y=native_monitor_metric(backend, index, 5),
work_width=native_monitor_metric(backend, index, 6),
work_height=native_monitor_metric(backend, index, 7),
scale_factor=native_monitor_scale(backend, index),
),
)
None => None
}
}
}
///|
fn[A : App] dispatch_event(
app : A,
active : ActiveApp,
kind : Int,
id : Int,
arg0 : Int,
arg1 : Int,
argd0 : Double,
argd1 : Double,
) -> Unit {
match kind {
14 => app.about_to_wait(active)
15 => dispatch_proxy_messages(app, active)
_ => {
let window_id = WindowId::from_raw(id)
match kind {
1 => app.window_event(active, window_id, CloseRequested)
2 => app.window_event(active, window_id, Destroyed)
3 =>
app.window_event(
active,
window_id,
Resized(Size::new(width=arg0, height=arg1)),
)
4 => app.window_event(active, window_id, ScaleFactorChanged(argd0))
5 => app.window_event(active, window_id, FocusChanged(arg0 != 0))
6 => app.window_event(active, window_id, RedrawRequested)
7 =>
app.window_event(
active,
window_id,
KeyInput(
NativeKeyEvent::new(
code=arg0,
state=if (arg1 & 16) != 0 { Pressed } else { Released },
repeat=(arg1 & 32) != 0,
modifiers=Modifiers::new(
shift=(arg1 & 1) != 0,
control=(arg1 & 2) != 0,
alt=(arg1 & 4) != 0,
meta=(arg1 & 8) != 0,
),
),
),
)
8 =>
match arg0.to_char() {
Some(character) =>
app.window_event(
active,
window_id,
TextInput(character.to_string()),
)
None => ()
}
9 =>
app.window_event(
active,
window_id,
PointerMoved(Position::new(x=arg0, y=arg1)),
)
10 => app.window_event(active, window_id, PointerEntered)
11 => app.window_event(active, window_id, PointerLeft)
12 =>
app.window_event(
active,
window_id,
MouseInput(
match arg0 {
1 => MouseButton::Left
2 => MouseButton::Right
3 => MouseButton::Middle
4 => MouseButton::Back
5 => MouseButton::Forward
_ => MouseButton::Other(arg0)
},
if (arg1 & 16) != 0 {
Pressed
} else {
Released
},
Modifiers::new(
shift=(arg1 & 1) != 0,
control=(arg1 & 2) != 0,
alt=(arg1 & 4) != 0,
meta=(arg1 & 8) != 0,
),
),
)
13 =>
app.window_event(
active,
window_id,
MouseWheel(
ScrollDelta::new(x=argd0, y=argd1),
Modifiers::new(
shift=(arg1 & 1) != 0,
control=(arg1 & 2) != 0,
alt=(arg1 & 4) != 0,
meta=(arg1 & 8) != 0,
),
),
)
_ => ()
}
}
}
}