OxiUI’s on_close, on_resize, and on_focus hooks have been part of the public API since the framework shipped — and until this release, none of them ever fired.
Today we released OxiUI 0.2.1 — a fix release that wires lifecycle hooks into the real eframe/iced event loops for the first time, makes with_persistent_state actually write your app’s state to disk instead of silently discarding it, and closes two bugs in the CPU software rasterizer: an integer-overflow bounds-check bypass and an unbounded-iteration denial-of-service.
No GTK. No Qt. No SDL. The dependency tree hasn’t changed — OxiUI still builds with a single cargo build in a clean rust:slim container, on the same egui/iced facade, the same wgpu and softbuffer rendering paths, and the same winit windowing. What changed in 0.2.1 is what the API actually does once your app is running.
Why OxiUI 0.2.1 is a game changer
The previous release had a class of bug that’s worse than a crash — silent no-ops behind a compiling, chainable API:
App::on_close/on_resize/on_focustype-checked, chained, and compiled cleanly — but nothing behind them ever invoked the closures. An app that persisted state on close, logged focus changes, or reacted to resize was silently doing nothing at all.with_persistent_state’sstorage_pathargument was accepted, type-checked, and then discarded (let _ = path) — “persistent” state reset on every single run.IcedRunneraccepted a configured windowwidth/heightand dropped both before constructing theiced::application— every iced-backed OxiUI app opened at iced’s default size, regardless of what you asked for.- In
oxiui-render-soft,composite_into’s bounds check computed the required source length asw * h * 4inu32arithmetic — which wraps to exactly0for largew/h(e.g. 65,536 × 65,536), defeating the guard and letting the pixel-copy loop read past the end ofsrc. fill_polygon,fill_polygon_clipped, andpaint_spaniterated scanlines using the shape’s own coordinates with no framebuffer clamp — a polygon or span with extreme vertex coordinates (attacker-influenced input, for instance) could drive billions of empty loop iterations before painting a single pixel.
OxiUI 0.2.1 ends all of that:
- A new
runner::LifecycleTrackerdedupes raw per-frame snapshots intoLifecycleEvents, and both backends now drive real hooks from it: egui polls viewport size and focus every frame and fireson_closefromeframe::App::on_exit; iced drives the same three hooks from aniced::event::listen_withsubscription overResized/Focused/Unfocused/CloseRequested. EguiRunner/IcedRunnerare now liveBackendRunners that carry the app’s theme, hooks, and plugins and actually own theeframe::run_native/iced::applicationevent loop —App::run()moves its state into the matching runner instead of returning before anything happens.with_persistent_stateshares state viaArc<Mutex<_>>between the content closure and theon_closehook, encodes it withoxicode, and writes it tostorage_pathon close — and after the single frame in headless mode, so persistence is deterministic in tests too.IcedRunnernow chains.window_size(...)and.exit_on_close_request(false), so the size you configured is honored andon_closehooks get to run before the window actually closes.composite_into’s length check now uses checkedusizearithmetic — an overflow is treated as unsatisfiable and the function safely returns0instead of under-guarding the copy.- Row and span ranges in the scanline rasterizer are clamped to the framebuffer’s own dimensions before iterating, with an edge fast-forward so shapes that are only partially off-screen still render correctly for their visible rows.
Technical Deep Dive: wiring three hooks into two different event loops
- Lifecycle detection (
runner::LifecycleTracker). Both backends poll or receive raw state every tick — egui reads the viewport size and focus flag each frame; iced receives raw winit-style window events. The tracker’s job is purely to de-duplicate: it holds the last-seen size/focus/close state and only emits aLifecycleEventwhen something actually changed, soon_resizedoesn’t fire every frame just because the window happened to redraw. - Runner ownership. Previously,
EguiRunner/IcedRunnerexisted as thin constructors that didn’t retain the app’s hooks. Now they’re realBackendRunnerimplementations:EguiRunner::new()/IcedRunner::new()(plus athemesetter) take ownership of the theme, hook vectors, and plugin list, andApp::run()simply hands its state to the matching runner and lets it drive the native event loop. - Persistence.
with_persistent_statedecodesStatefromstorage_pathviaoxicodeon startup (falling back to the caller’sinitialvalue on any decode error, with a stderr warning — never a panic), wraps the live value inArc<Mutex<State>>shared between the per-frame content closure and a newon_closehook, and encodes it back to disk when that hook fires. - Hardening the CPU rasterizer.
oxiui-render-soft’scomposite_intoguard and its scanline fill/paint routines both took raw, potentially attacker-shaped geometry as trusted input. The fix in both cases is the same shape: replace unchecked arithmetic (u32multiplication, unclamped iteration bounds) with checked arithmetic and framebuffer-relative clamping, so malformed or extreme input degrades to “draw nothing” instead of “read out of bounds” or “spin forever.”
Getting Started
cargo add oxiui
use oxiui::{App, theme};
fn main() -> Result<(), Box<dyn std::error::Error>> {
App::new("My App")
.theme(theme::cooljapan_default())
.on_close(|_ui| {
println!("window closing — this now actually fires");
})
.content(|ui| {
ui.heading("Hello from OxiUI");
})
.run()?;
Ok(())
}
With the persist feature on, the same builder can carry real state across runs:
use oxicode::{Encode, Decode};
use oxiui::App;
#[derive(Encode, Decode, Default)]
struct Counter {
n: u32,
}
let app = oxiui::App::new("Counter").with_persistent_state(
Counter::default(),
std::env::temp_dir().join("my_app_state.oxi"),
|ui, state| {
if ui.button("Click me").clicked() {
state.n += 1;
}
ui.label(&format!("Clicks: {}", state.n));
},
);
app.run()?;
Close the window, run it again, and state.n picks up where it left off — for real, this time.
What’s New in 0.2.1
- Added: lifecycle hooks (
on_close/on_resize/on_focus) now fire from the real egui and iced event loops, deduplicated through a newrunner::LifecycleTracker. - Added:
EguiRunner/IcedRunnerare liveBackendRunners that own the native event loop instead of returning immediately. - Added: the iced backend now honors the configured window size and delays closing until
on_closehooks run. - Added:
oxiui-egui’s IME preedit cursor position is now forwarded to egui 0.35 (ImeEvent::Preedit’s newactive_range_chars), converting byte offsets to char offsets with a UTF-8-boundary-safe fallback instead of always dropping it. - Changed:
with_persistent_stategenuinely persists to disk on close viaoxicode, replacing the previous no-op. - Fixed:
oxiui-render-soft’sgaussian_blur_alpha_fftno longer silently no-ops when thefft-blurfeature is disabled — it now forwards to the direct convolution path. - Security: a
u32overflow incomposite_into’s bounds check that could bypass the length guard on large images; checked arithmetic closes it. - Security: unbounded scanline iteration in
fill_polygon/fill_polygon_clipped/paint_spanthat could be driven into billions of empty loops by extreme input geometry; row/span ranges are now framebuffer-clamped. egui/eframe0.35.0,slint1.17.0,oxifft0.4.1,oxicode0.2.5,oxifont0.2.1,oxitext/oxitext-sdf0.2.1.- 1,969 tests passing across 16 crates (
cargo nextest run --all-features --workspace), zero warnings, Pure Rust default features, MSRV 1.89.
Tips
- If you registered
on_close/on_resize/on_focusbefore 0.2.1, check your assumptions. The hooks always compiled; they never ran. Upgrade and re-verify any behavior you thought was already live. with_persistent_stateneeds thepersistfeature (pulls inoxicode). The state type only needs#[derive(oxicode::Encode, oxicode::Decode)]plusSend + 'static— no manual (de)serialization code.- A corrupt or missing state file is never fatal. Decode failures fall back to the
initialvalue you supplied and print a warning to stderr; the same is true in reverse for encode failures on close. - Testing lifecycle or persistence logic?
run_headless_once()fireson_closeafter its single frame, so both hooks and persistence are exercised deterministically without a real window. - Driving the iced backend and relying on a specific window size? It’s honored now — no code change needed, just the version bump.
This is the foundation
OxiUI is part of NoFFI — the COOLJAPAN initiative to replace every C/C++/Fortran/-sys FFI dependency in the Rust ecosystem with a clean, memory-safe, 100% Pure Rust implementation. It’s built directly on its NoFFI siblings OxiText (text shaping) and OxiFont (font loading + raster), and this release matters most for anything that keeps state across runs or reacts to window lifecycle: settings panels, data UIs, and any app that used to lose its state on every restart without ever throwing an error.
Repository: https://github.com/cool-japan/oxiui
Star the repo if you’ve ever shipped a hook that quietly did nothing and want a framework that tells you when it does.
Pure Rust UI — sovereign, safe, and FFI-free.
— KitaSan at COOLJAPAN OÜ July 30, 2026