// Cross-window event push.
//
// The bus lives in the **main process** and maps each window label to its
// globally-unique window id. When one window emits a message targeted at
// another label, the main-process router sends a directed IPC event to that
// window; the target child process receives it on its IPC listener thread and
// evals a `CustomEvent("lepus_event")` into that window's JavaScript.
//
// Frontend:
// window.addEventListener("lepus_event", e => {
// const { event, payload } = e.detail; // event: string, payload: object
// });
// Or emit from any window: `window.lepusApi.events.emit({ target, event, payload })`.
///|
pub struct EmitRequest {
target : String
event : String
payload : Json
} derive(ToJson, FromJson)
///|
pub extend EmitRequest with ToJson::{to_json}
///|
pub extend EmitRequest with @json.FromJson::{from_json}
///|
pub struct EventBus {
window_ids : Map[String, Int]
}
///|
pub fn EventBus::new(window_ids : Map[String, Int]) -> EventBus {
{ window_ids, }
}
///|
/// Returns the window id for a label, or `-1` if unknown.
pub fn EventBus::window_id(self : EventBus, label : String) -> Int {
match self.window_ids.get(label) {
Some(id) => id
None => -1
}
}
///|
/// Constructs the JS snippet that dispatches `CustomEvent("lepus_event")` with
/// `detail = { event: , payload: }` into the target window.
fn event_eval_js(event : String, payload : Json) -> String {
let event_literal = Json::string(event).stringify()
let payload_json = payload.stringify()
"window.dispatchEvent(new CustomEvent(\"lepus_event\",{detail:" +
"{\"event\":" +
event_literal +
",\"payload\":" +
payload_json +
"}}));"
}
///|
/// Builds the `events` plugin. Its `emit` command resolves the target label
/// into a window id and sends a directed IPC event carrying the JS snippet.
///
/// `wm` must be the main-process `WindowManager` (the plugin is installed on
/// the main-process router, whose handler performs the directed send).
pub fn EventBus::plugin(
self : EventBus,
wm : @webview.WindowManager,
) -> @webview.Plugin {
@webview.Plugin("events", plugin => {
plugin.command_sync("emit", fn(request : EmitRequest) -> Bool {
let id = self.window_id(request.target)
guard id > 0 else { false }
let js = event_eval_js(request.event, request.payload)
wm.send_message(0, id, @webview.IpcMessageType::Event, "lepus-event", js) ==
0
})
})
}
///|
test "event eval snippet embeds event and payload as a JS object literal" {
let js = event_eval_js("greet", Json::string("hi"))
assert_eq(
js, "window.dispatchEvent(new CustomEvent(\"lepus_event\",{detail:{\"event\":\"greet\",\"payload\":\"hi\"}}));",
)
}
///|
test "event bus resolves window ids by label" {
let ids = Map([])
ids.set("main", 1)
ids.set("settings", 2)
let bus = EventBus::new(ids)
assert_eq(bus.window_id("main"), 1)
assert_eq(bus.window_id("settings"), 2)
assert_eq(bus.window_id("unknown"), -1)
}
// ════════════════════════════════════════════════════════════════
// 单进程模式:本地事件总线(直接 eval,无 IPC)
// ════════════════════════════════════════════════════════════════
// 单进程多窗口模式下,所有窗口在同一进程,事件可直接 `webview.eval`
// 投递到目标窗口,无需 IPC。这同时让整个应用共享一个 NSApplication /
// Dock 图标(macOS),窗口天然可聚焦。
///|
/// 本地事件总线:label → WebView 句柄的直接映射。
pub struct LocalEventBus {
webviews : Map[String, @webview.WebView[Unit]]
}
///|
pub fn LocalEventBus::new() -> LocalEventBus {
{ webviews: Map([]), }
}
///|
pub fn LocalEventBus::register(
self : LocalEventBus,
label : String,
webview : @webview.WebView[Unit],
) -> Unit {
self.webviews.set(label, webview)
}
///|
/// 构建 `events` 插件。`emit` 直接对目标窗口的 WebView 执行 eval,
/// 触发 `CustomEvent("lepus_event")`。同进程内即时送达,无 IPC 开销。
pub fn LocalEventBus::plugin(self : LocalEventBus) -> @webview.Plugin {
@webview.Plugin("events", plugin => {
plugin.command_sync("emit", fn(request : EmitRequest) -> Bool {
match self.webviews.get(request.target) {
Some(wv) => {
wv.eval(event_eval_js(request.event, request.payload))
true
}
None => false
}
})
})
}