diff options
| author | Adam Malczewski <[email protected]> | 2026-06-14 16:00:49 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-14 16:00:49 +0900 |
| commit | 41abc47bfb4a5098bff611e3e241d2b63788cbec (patch) | |
| tree | 9df638f56c16c2cc6cf9dac0648cb50687ff0929 /packages/kernel/src/server.cpp | |
| parent | 89c40575c353dfd3c9fcaf60d2bd45d3fa2b6792 (diff) | |
| download | unbox-41abc47bfb4a5098bff611e3e241d2b63788cbec.tar.gz unbox-41abc47bfb4a5098bff611e3e241d2b63788cbec.zip | |
kernel: add request_frames() frame callback + UiSurface::transition_timing()
Two additive primitives for C++-driven, RCSS-tunable animation:
- Host::request_frames(cb) -> FrameRequest: a per-frame callback (RAII handle)
run before tick_all each frame; the kernel schedules frames continuously while
>=1 request is alive and stops at rest. Fills the missing animation timer.
- UiSurface::transition_timing(element_id, property): reads the RCSS-authored
transition duration + easing, returning RmlUi's tween wrapped as a pure
std::function (no RmlUi types cross the contract) so an extension can drive its
own animation with hot-reloadable, designer-tunable timing/easing.
Diffstat (limited to 'packages/kernel/src/server.cpp')
| -rw-r--r-- | packages/kernel/src/server.cpp | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/packages/kernel/src/server.cpp b/packages/kernel/src/server.cpp index 66f2cc3..15158a9 100644 --- a/packages/kernel/src/server.cpp +++ b/packages/kernel/src/server.cpp @@ -399,6 +399,26 @@ auto Server::Impl::file_watcher() -> FileWatcher* { return watcher.get(); } +auto Server::Impl::frame_driver() -> FrameDriver* { + // Lazily create the per-frame animation driver on first use, carrying the + // kernel's disable sink for error isolation. No loop/wlr resource of its own + // (the frame handler drives it), so it is always creatable. + if (frames == nullptr) { + frames = std::make_unique<FrameDriver>([this](ExtensionId who) { disable(who); }); + } + return frames.get(); +} + +void Server::Impl::schedule_driver_frame() { + // Pick / keep the primary driving output: the first one still present. + if (frame_driver_output == nullptr && !outputs.empty()) { + frame_driver_output = outputs.front()->output; + } + if (frame_driver_output != nullptr) { + wlr_output_schedule_frame(frame_driver_output); + } +} + void Server::Impl::shutdown() { // Destroy extensions FIRST, in reverse activation order: their RAII members // (Subscriptions, Listeners, scene nodes) release while the wlr objects @@ -531,6 +551,33 @@ void Server::Impl::handle_new_output(wlr_output* wlr_output) { outputs.push_back(std::move(owned)); output->frame.connect(wlr_output->events.frame, [this, output](void*) { + // Per-frame animation callbacks (Host::request_frames) run on the PRIMARY + // output's frame only — the first output added is the frame driver, so a + // multi-output session gets ONE shared dt and the callbacks fire once per + // displayed frame rather than once per output. They run BEFORE + // substrate->tick_all()/commit so a callback that updates state + + // UiSurface::dirty() is composited THIS frame. + if (frame_driver_output == nullptr) { + frame_driver_output = output->output; // promote a survivor / first output + } + if (output->output == frame_driver_output && frames != nullptr && + frames->has_requests()) { + timespec ts{}; + clock_gettime(CLOCK_MONOTONIC, &ts); + const double t = static_cast<double>(ts.tv_sec) + static_cast<double>(ts.tv_nsec) / 1e9; + const double dt = (last_frame_time < 0.0) ? 0.0 : (t - last_frame_time); + last_frame_time = t; + frames->drain(dt); // error-isolated; may add/remove requests (incl. its own) + // Keep the frames coming while any request is still alive after the + // drain (a callback may have removed the last one — then we stop, + // returning to idle: no busy render at rest). + if (frames->has_requests()) { + wlr_output_schedule_frame(frame_driver_output); + } else { + last_frame_time = -1.0; // reset the dt base for the next animation + } + } + if (substrate != nullptr) { substrate->tick_all(); } @@ -548,6 +595,27 @@ void Server::Impl::handle_new_output(wlr_output* wlr_output) { output->destroy.connect(wlr_output->events.destroy, [this, output](void*) { const OutputEvent ev{output->output}; ev_output_removed.emit(ev); + // If the frame DRIVER output is going away, re-point it (and reset the dt + // base) so live animations keep advancing on a surviving output. This + // MUST all happen BEFORE `outputs.remove_if` below: that call destroys + // `output` together with THIS very listener's std::function storage, so + // it has to be the LAST action — nothing may touch the lambda or its + // captures afterwards. + if (frame_driver_output == output->output) { + frame_driver_output = nullptr; + last_frame_time = -1.0; + // Promote a SURVIVING output (skip the one being destroyed) and, if + // any frame request is alive, schedule its next frame. + for (const auto& owned : outputs) { + if (owned.get() != output) { + frame_driver_output = owned->output; + break; + } + } + if (frame_driver_output != nullptr && frames != nullptr && frames->has_requests()) { + wlr_output_schedule_frame(frame_driver_output); + } + } // Last action: destroys `output` (and these listeners with it). outputs.remove_if([output](const auto& owned) { return owned.get() == output; }); }); |
