// Label-addressed window manager.
//
// `WindowRegistry` maps each window label to its globally-unique window id and
// gives any window the ability to open, close, show, hide, move, resize,
// minimize, maximize or fullscreen any other window by its stable label.
//
// The plugin lives in the **main process** (like `EventBus`). Commands resolve
// a label to a window id and issue the corresponding `wm_*` call on the main
// process `WindowManager`. Lazy windows — declared with `WindowConfig.hidden`
// — are not spawned up front; `open` spawns their child process on first use.
///|
/// Snapshot of one managed window returned by the `windows` plugin `list` API.
pub struct WindowInfo {
label : String
open : Bool
} derive(ToJson, FromJson)
///|
/// Target window label request.
pub struct LabelRequest {
label : String
} derive(ToJson, FromJson)
///|
/// Move a target window to an absolute position.
pub struct MoveWindowRequest {
label : String
x : Int
y : Int
} derive(ToJson, FromJson)
///|
/// Resize a target window.
pub struct ResizeWindowRequest {
label : String
width : Int
height : Int
} derive(ToJson, FromJson)
///|
/// Toggle a target window's fullscreen state.
pub struct FullscreenWindowRequest {
label : String
fullscreen : Bool
} derive(ToJson, FromJson)
///|
pub extend WindowInfo with ToJson::{to_json}
///|
pub extend WindowInfo with @json.FromJson::{from_json}
///|
pub extend LabelRequest with ToJson::{to_json}
///|
pub extend LabelRequest with @json.FromJson::{from_json}
///|
pub extend MoveWindowRequest with ToJson::{to_json}
///|
pub extend MoveWindowRequest with @json.FromJson::{from_json}
///|
pub extend ResizeWindowRequest with ToJson::{to_json}
///|
pub extend ResizeWindowRequest with @json.FromJson::{from_json}
///|
pub extend FullscreenWindowRequest with ToJson::{to_json}
///|
pub extend FullscreenWindowRequest with @json.FromJson::{from_json}
///|
/// Cross-window control payload pushed to a target window over the directed
/// IPC event channel (event name `lepus:window`). The target window's
/// `window_controls` script invokes `window.LepusWindow[op](...args)`, so the
/// native operation runs in the process that actually owns the window.
priv struct WindowControl {
op : String
args : Json
} derive(ToJson)
///|
/// Empty argument list for `WindowRegistry::send_control`.
fn no_control_args() -> Json {
@json.to_json(([] : Array[Json]))
}
///|
/// Shared, mutable registry owned by the main process.
///
/// `ids` maps label -> window-id (== spawn base); `pids` maps label -> child
/// pid once spawned; `children` lists every live child so the serving loop can
/// wait on them; `focused` mirrors each window's `WindowConfig.focused` flag so
/// `open` knows whether a re-shown window should also grab focus.
pub struct WindowRegistry {
ids : Map[String, Int]
pids : Map[String, Int]
children : Array[Int]
focused : Map[String, Bool]
}
///|
/// Builds a window registry for the given configs. Window ids are assigned in
/// declaration order, matching the base each child uses when creating its own
/// window (see `assign_window_ids` in `lepus.mbt`).
pub fn WindowRegistry::new(configs : Array[WindowConfig]) -> WindowRegistry {
let focused : Map[String, Bool] = Map([])
for config in configs {
focused.set(config.label(), config.focused)
}
{ ids: assign_window_ids(configs), pids: Map([]), children: [], focused, }
}
///|
/// Constructs the `windows` plugin. `argv0` is the executable path used to
/// spawn lazy child windows; `wm` must be the main-process `WindowManager`.
pub fn WindowRegistry::plugin(
self : WindowRegistry,
wm : @webview.WindowManager,
argv0 : String,
) -> @webview.Plugin {
@webview.Plugin("windows", plugin => {
plugin.command_sync("list", fn(_ : Json) -> Array[WindowInfo] {
let out : Array[WindowInfo] = []
self.ids.each(fn(label, _id) {
out.push({ label, open: self.is_open(label), })
})
out
})
plugin.command_sync("open", fn(req : LabelRequest) -> Bool {
self.open(wm, argv0, req.label)
})
plugin.command_sync("focus", fn(req : LabelRequest) -> Bool {
self.with_window(req.label, fn(id) {
wm.focus_window(id) == 0 ||
self.send_control(wm, req.label, "focus", no_control_args())
})
})
plugin.command_sync("close", fn(req : LabelRequest) -> Bool {
self.with_window(req.label, fn(id) {
wm.close_window(id) == 0 ||
self.send_control(wm, req.label, "close", no_control_args())
})
})
plugin.command_sync("show", fn(req : LabelRequest) -> Bool {
self.with_window(req.label, fn(id) {
wm.set_visibility(id, true) == 0 ||
self.send_control(wm, req.label, "show", no_control_args())
})
})
plugin.command_sync("hide", fn(req : LabelRequest) -> Bool {
self.with_window(req.label, fn(id) {
wm.set_visibility(id, false) == 0 ||
self.send_control(wm, req.label, "hide", no_control_args())
})
})
plugin.command_sync("move", fn(req : MoveWindowRequest) -> Bool {
self.with_window(req.label, fn(id) {
wm.set_position(id, req.x, req.y) == 0 ||
self.send_control(
wm,
req.label,
"setPosition",
@json.to_json([req.x, req.y]),
)
})
})
plugin.command_sync("resize", fn(req : ResizeWindowRequest) -> Bool {
self.with_window(req.label, fn(id) {
wm.set_size(id, req.width, req.height, None) == 0 ||
self.send_control(
wm,
req.label,
"setSize",
@json.to_json([req.width, req.height]),
)
})
})
plugin.command_sync("minimize", fn(req : LabelRequest) -> Bool {
self.with_window(req.label, fn(id) {
wm.minimize_window(id) == 0 ||
self.send_control(wm, req.label, "minimize", no_control_args())
})
})
plugin.command_sync("maximize", fn(req : LabelRequest) -> Bool {
self.with_window(req.label, fn(id) {
wm.maximize_window(id) == 0 ||
self.send_control(wm, req.label, "maximize", no_control_args())
})
})
plugin.command_sync("set_fullscreen", fn(
req : FullscreenWindowRequest,
) -> Bool {
self.with_window(req.label, fn(id) {
wm.set_fullscreen_window(id, req.fullscreen) == 0 ||
self.send_control(
wm,
req.label,
"setFullscreen",
@json.to_json([req.fullscreen]),
)
})
})
})
}
///|
fn WindowRegistry::is_open(self : WindowRegistry, label : String) -> Bool {
match self.pids.get(label) {
Some(pid) => pid > 0
None => false
}
}
///|
/// Pushes a window-control request to a (possibly remote, child-process)
/// window by label, using the same directed IPC event channel as the
/// `EventBus` (`lepus-event`). The target window's `window_controls` script
/// performs the native operation in its own process. Returns true when the
/// message was accepted for sending; false for unknown labels or no transport.
fn WindowRegistry::send_control(
self : WindowRegistry,
wm : @webview.WindowManager,
label : String,
op : String,
args : Json,
) -> Bool {
let id = match self.ids.get(label) {
Some(id) => id
None => return false
}
let js = event_eval_js(
"lepus:window",
@json.to_json(WindowControl::{ op, args, }),
)
wm.send_message(0, id, @webview.IpcMessageType::Event, "lepus-event", js) == 0
}
///|
/// Opens (spawns if needed) a window by label and ensures it is visible.
fn WindowRegistry::open(
self : WindowRegistry,
wm : @webview.WindowManager,
argv0 : String,
label : String,
) -> Bool {
let id = match self.ids.get(label) {
Some(id) => id
None => return false
}
// Already spawned: reveal it, applying the window's `focused` attribute
// (Tauri `focused` parity — re-opened windows grab focus unless declared
// with `focused: false`). The control pushes act on remote child-process
// windows and arrive in order after `show`.
if self.is_open(label) {
let shown = wm.set_visibility(id, true) == 0 ||
self.send_control(wm, label, "show", no_control_args())
if self.focused.get(label).unwrap_or(true) {
ignore(self.send_control(wm, label, "focus", no_control_args()))
}
return shown
}
let pid = wm.spawn_process(argv0, @webview.child_arg_for(id, label))
guard pid > 0 else { return false }
self.pids.set(label, pid)
self.children.push(pid)
true
}
///|
/// Runs `action` with the window id for `label` when it is currently spawned.
fn WindowRegistry::with_window(
self : WindowRegistry,
label : String,
action : (Int) -> Bool,
) -> Bool {
let id = match self.ids.get(label) {
Some(id) => id
None => return false
}
guard self.is_open(label) else { return false }
action(id)
}
///|
/// Returns the live child pids (used by the serving loop to know when to stop
/// and to pick up lazily-spawned windows).
fn WindowRegistry::live_children(self : WindowRegistry) -> Array[Int] {
self.children
}
///|
test "window registry keeps each window's focused attribute" {
let configs = [
WindowConfig::new(
label="a",
title="A",
focused=false,
source=Source::html(""),
),
WindowConfig::new(label="b", title="B", source=Source::html("")),
]
let registry = WindowRegistry::new(configs)
assert_eq(registry.focused.get("a"), Some(false))
assert_eq(registry.focused.get("b"), Some(true))
}
///|
test "window control payload carries op and args for the JS facade" {
let payload = @json.to_json(WindowControl::{
op: "focus",
args: no_control_args(),
})
assert_eq(payload.stringify(), "{\"op\":\"focus\",\"args\":[]}")
}
///|
/// A multi-window app with runtime window management (see `Windows::run`).
pub struct Windows {
configs : Array[WindowConfig]
plugins : Array[@webview.Plugin]
}
///|
/// Builds a window-managed app.
pub fn Windows::new(
configs : Array[WindowConfig],
plugins? : Array[@webview.Plugin] = [],
) -> Windows {
{ configs, plugins, }
}
///|
/// Runs a window-managed app end-to-end.
///
/// The main process spawns non-hidden windows up front, installs the
/// label-addressed `windows` plugin plus the cross-window event bus, and serves
/// commands from every child. Hidden windows are spawned lazily by `open`.
pub async fn Windows::run(self : Windows) -> Unit {
assert_unique_labels(self.configs)
match @webview.detect_child_window() {
Some((base, label)) => {
let bus = EventBus::new(Map([]))
let wm = @webview.WindowManager::init(is_main=false)
let registry = WindowRegistry::new(self.configs)
let all_plugins = self.plugins + [bus.plugin(wm), registry.plugin(wm, "")]
let config = find_window_config(self.configs, label)
let window = build_runtime_window(config, all_plugins)
window.run_child(base)
}
None => {
let args = @env.args()
guard args.length() > 0 else { abort("Windows::run: missing argv[0]") }
let argv0 = args[0]
let wm = @webview.WindowManager::init(is_main=true)
let registry = WindowRegistry::new(self.configs)
// Spawn non-hidden windows immediately.
for config in self.configs {
if !config.hidden {
let opened = registry.open(wm, argv0, config.label())
guard opened else {
abort("Windows::run: spawn child for " + config.label() + " failed")
}
}
}
let bus = EventBus::new(assign_window_ids(self.configs))
let all_plugins = self.plugins +
[bus.plugin(wm), registry.plugin(wm, argv0)]
let router = @webview.build_router(all_plugins)
router.serve_many_dynamic(wm, fn() { registry.live_children() })
}
}
}