Viewport Contract
The contract between your app and the renderer: make_view(w, h).
The contract
make_view has the signature:
#![allow(unused)]
fn main() {
FnMut(w: u32, h: u32) -> (VNode, Stylesheet)
}
where (w, h) are logical viewport dimensions in CSS pixels:
logical = Viewport::from_i32(physical_w, physical_h, scale_factor).logical_size()
// physical = (logical * scale).round(); the single rounding point is Viewport
The renderer calls make_view with the current logical w, h:
- once on startup (the initial frame, before the event loop),
- on every
RedrawRequestedafter aWindowEvent::Resized/ScaleFactorChanged.
The authoritative contract is the rustdoc on run_window_vnode_skia and run_window_vnode_skia_with_hmr in velox-renderer/src/lib.rs.
Why it matters (the Flutter invariant)
Layout is viewport-driven: compute_layout(&vnode, w as i32, h as i32). If make_view ignores w, h (writing |_w, _h|), the app is non-responsive — resize has no visible effect and elements hide when they should be visible.
A Velox app follows the Flutter invariant: it survives any window size, including 0×0 → restore, rapid drag-resize, and fractional DPI, with no hidden-when-visible element.
Template obligation
All shipped templates (veloxc/templates/project/, examples/counter/, examples/todo/, examples/showcase/, and projects created via veloxc init) follow two rules:
1. Acknowledge (w, h) — don’t discard them
#![allow(unused)]
fn main() {
// ❌ wrong
move |_w: u32, _h: u32| -> (VNode, Stylesheet) { ... }
// ✅ correct
move |w: u32, h: u32| -> (VNode, Stylesheet) {
let _viewport = (w, h);
// ... render_with_state, Stylesheet::parse ...
}
}
All templates bind w, h and keep let _viewport = (w, h); so clippy and future audits visibly track the contract (and it’s a hook for future viewport-aware rendering).
2. Use a viewport-filling root
So compute_layout actually reflows, the canonical pattern on the root .app is:
.app {
width: 100%;
min-height: 100vh;
/* display/flex per template varies */
}
width: 100% makes the root span the viewport width; min-height: 100vh pins it to at least the viewport height (not height: 100%, which over-constrains flex children). velox-dom already treats min-height: 100vh and width: 100% × height: 100% as viewport-filling and maps them to the avail_w/h of compute_layout (see layout.rs viewport-height handling).
Every shipped App.vx carries this — in veloxc/templates/project/src/App.vx, examples/counter/src/App.vx, examples/todo/src/App.vx, examples/showcase/src/App.vx, and the strings generated by veloxc/src/commands/init.rs (generate_app_vx / init_app).
Verification
- Repro tests —
velox-cli/tests/template_w_h_repro.rs(string checks that no|_wdiscard remains and the root haswidth: 100%/min-height: 100vh) andvelox-dom/tests/template_w_h_layout_repro.rs(a simulated screenshot: maximized vs 400×300 —compute_layoutmust yield a largerrect.wfor the larger viewport and must not hide a 50%-width child). - Manual drag-resize — run any example (
cargo run -p velox-example-todo), drag-resize the window arbitrarily; the root visibly fills with no clipping or hidden child. - DPI — fractional scale (
wlr-randr --scale 1.25) still reflows, because logicalw, hare alwaysViewport-derived with a single rounding point.