0.42.0: the webview emitter writes htmx 4
`2246072d`. Max ruled it on 2026-08-18: the webview emitter targets htmx 4, not 2.x. quasi
is pre-release, so writing the transport against a model that is being replaced means
writing it twice, and 4.0 having no announced stable date is a bet taken deliberately
rather than a thing to wait out. Wiki `quasi-overview`, "htmx 4, ruled 2026-08-18".
Every change was checked against 4.0.0-beta6's own source rather than against the
migration guide, which is also what the template now vendors.
The swap is `outerMorph`, and it is unconditional. Under 2.x morphing came from idiomorph,
which is why the shell carried a `morph_src`, registered `hx-ext="morph"` on the body and
the emitter asked for a morph only when the extension was loaded: htmx fell back to a
destructive `innerHTML` in silence when it was missing, and silently destructive is the
one outcome decision 7 exists to avoid. In 4 there is nothing that can be missing, so
`morph_src`, `without_morph()`, `morphs()` and the `morphs: bool` threaded through sixty
call sites are all gone. `outerMorph` and not `innerMorph` because the extension's bare
`morph` meant outerHTML.
The lock is `hx-disable`, 4's name for 2.x's `hx-disabled-elt`. Worth saying out loud
because the rename is a swap of meaning: what 2.x called `hx-disable` is `hx-ignore` in 4,
so a find-and-replace in either direction is wrong. Nothing here emits `hx-ignore`.
Inheritance is explicit, so the headers a document sends go on the body as
`hx-headers:inherited`. A bare `hx-headers` there would be a header the body sends on its
own requests and no control sends on any, which is the whole reason they are on the body.
An emitter knows every attribute it writes, so this costs one attribute name and no
design.
`quasi_http::htmx::CONFIG_META` and `RESPONSE_HANDLING` are deleted rather than updated. 4
swaps every status but 204 and 304, and `responseHandling` is not a config key any more.
That constant existed because 2.x dropped a 4xx, which made decision 9's classified errors
(403 `Denied`, 404 `NotFound`) render nothing at all; the emitter gets it for free now,
and a screen wanting per-status behaviour says so on the element with `hx-status:XXX`.
The two scripts this crate ships listen on `htmx:after:settle`, 4's spelling of the settle
event.
The scaffolder vendors htmx 4.0.0-beta6 and no longer vendors idiomorph. A 2.x bundle
behind `Shell::htmx_src` is a page whose controls fetch nothing and whose swaps destroy
state, so the bundle and this crate move together.
One behaviour difference is documented rather than designed around: on a GET, htmx 4
deletes an address parameter whose name an `hx-vals` value repeats before appending it,
where 2.x appended both. Nothing in the tree names one thing in both bags on a read.
Consumers forward-fixed to 0.42: MNW server (2282 lib tests green), goingson under
`--features quasi` (677 green), audiofiles. MNW is now in the state the ruling sequences
for: its described screens emit 4's markup while `static/htmx.min.js` is still 2.0.4, and
the site's own migration is mnw-server `6b87e5ff`. Exposure is bounded by `QUASI_SCREENS`,
which is off unless a host sets it.
Workspace: 23 test binaries green, clippy clean but for the pre-existing
`large_enum_variant` on `Outcome`.
28 files changed,
+311 insertions,
-383 deletions
| 3404 |
3404 |
|
|
| 3405 |
3405 |
|
[[package]]
|
| 3406 |
3406 |
|
name = "quasi"
|
| 3407 |
|
- |
version = "0.41.0"
|
|
3407 |
+ |
version = "0.42.0"
|
| 3408 |
3408 |
|
|
| 3409 |
3409 |
|
[[package]]
|
| 3410 |
3410 |
|
name = "quasi-axum"
|
| 3411 |
|
- |
version = "0.41.0"
|
|
3411 |
+ |
version = "0.42.0"
|
| 3412 |
3412 |
|
dependencies = [
|
| 3413 |
3413 |
|
"axum",
|
| 3414 |
3414 |
|
"http",
|
| 3422 |
3422 |
|
|
| 3423 |
3423 |
|
[[package]]
|
| 3424 |
3424 |
|
name = "quasi-basics"
|
| 3425 |
|
- |
version = "0.41.0"
|
|
3425 |
+ |
version = "0.42.0"
|
| 3426 |
3426 |
|
dependencies = [
|
| 3427 |
3427 |
|
"makeover-layout",
|
| 3428 |
3428 |
|
"quasi-http",
|
| 3432 |
3432 |
|
|
| 3433 |
3433 |
|
[[package]]
|
| 3434 |
3434 |
|
name = "quasi-bench"
|
| 3435 |
|
- |
version = "0.41.0"
|
|
3435 |
+ |
version = "0.42.0"
|
| 3436 |
3436 |
|
dependencies = [
|
| 3437 |
3437 |
|
"dhat",
|
| 3438 |
3438 |
|
"makeover",
|
| 3447 |
3447 |
|
|
| 3448 |
3448 |
|
[[package]]
|
| 3449 |
3449 |
|
name = "quasi-http"
|
| 3450 |
|
- |
version = "0.41.0"
|
|
3450 |
+ |
version = "0.42.0"
|
| 3451 |
3451 |
|
dependencies = [
|
| 3452 |
3452 |
|
"form_urlencoded",
|
| 3453 |
3453 |
|
"http",
|
| 3456 |
3456 |
|
|
| 3457 |
3457 |
|
[[package]]
|
| 3458 |
3458 |
|
name = "quasi-immediate"
|
| 3459 |
|
- |
version = "0.41.0"
|
|
3459 |
+ |
version = "0.42.0"
|
| 3460 |
3460 |
|
dependencies = [
|
| 3461 |
3461 |
|
"docengine",
|
| 3462 |
3462 |
|
"egui",
|
| 3466 |
3466 |
|
|
| 3467 |
3467 |
|
[[package]]
|
| 3468 |
3468 |
|
name = "quasi-notifs"
|
| 3469 |
|
- |
version = "0.41.0"
|
|
3469 |
+ |
version = "0.42.0"
|
| 3470 |
3470 |
|
dependencies = [
|
| 3471 |
3471 |
|
"quasi-router",
|
| 3472 |
3472 |
|
"synckit-config",
|
| 3474 |
3474 |
|
|
| 3475 |
3475 |
|
[[package]]
|
| 3476 |
3476 |
|
name = "quasi-router"
|
| 3477 |
|
- |
version = "0.41.0"
|
|
3477 |
+ |
version = "0.42.0"
|
| 3478 |
3478 |
|
dependencies = [
|
| 3479 |
3479 |
|
"makeover-layout",
|
| 3480 |
3480 |
|
]
|
| 3491 |
3491 |
|
|
| 3492 |
3492 |
|
[[package]]
|
| 3493 |
3493 |
|
name = "quasi-tauri"
|
| 3494 |
|
- |
version = "0.41.0"
|
|
3494 |
+ |
version = "0.42.0"
|
| 3495 |
3495 |
|
dependencies = [
|
| 3496 |
3496 |
|
"http",
|
| 3497 |
3497 |
|
"quasi-http",
|
| 3504 |
3504 |
|
|
| 3505 |
3505 |
|
[[package]]
|
| 3506 |
3506 |
|
name = "quasi-tui"
|
| 3507 |
|
- |
version = "0.41.0"
|
|
3507 |
+ |
version = "0.42.0"
|
| 3508 |
3508 |
|
dependencies = [
|
| 3509 |
3509 |
|
"docengine",
|
| 3510 |
3510 |
|
"makeover",
|
| 3516 |
3516 |
|
|
| 3517 |
3517 |
|
[[package]]
|
| 3518 |
3518 |
|
name = "quasi-webview"
|
| 3519 |
|
- |
version = "0.41.0"
|
|
3519 |
+ |
version = "0.42.0"
|
| 3520 |
3520 |
|
dependencies = [
|
| 3521 |
3521 |
|
"docengine",
|
| 3522 |
3522 |
|
"makeover-layout",
|
| 6207 |
6207 |
|
name = "quasi-type"
|
| 6208 |
6208 |
|
version = "0.1.0"
|
| 6209 |
6209 |
|
|
| 6210 |
|
- |
[[patch.unused]]
|
| 6211 |
|
- |
name = "synckit-client"
|
| 6212 |
|
- |
version = "0.8.0"
|
| 6213 |
|
- |
|
| 6214 |
6210 |
|
[[patch.unused]]
|
| 6215 |
6211 |
|
name = "makeover-build"
|
| 6216 |
6212 |
|
version = "0.47.0"
|
|
6213 |
+ |
|
|
6214 |
+ |
[[patch.unused]]
|
|
6215 |
+ |
name = "synckit-client"
|
|
6216 |
+ |
version = "0.8.0"
|
| 108 |
108 |
|
|
| 109 |
109 |
|
`quasi-http` is what the two hosts turned out to share once there were two of
|
| 110 |
110 |
|
them. Decoding a query string and a form body, mapping an error's class to a
|
| 111 |
|
- |
status, naming a fragment's region in an `HX-Retarget` header, and the htmx
|
| 112 |
|
- |
client configuration a classified error needs. Nothing in it knows which host it
|
| 113 |
|
- |
is in, and nothing in it is async.
|
|
111 |
+ |
status, and naming a fragment's region in an `HX-Retarget` header. The transport
|
|
112 |
+ |
is htmx 4, which is what an emitted document links and what the response headers
|
|
113 |
+ |
here are checked against. Nothing in it knows which host it is in, and nothing
|
|
114 |
+ |
in it is async.
|
| 114 |
115 |
|
|
| 115 |
116 |
|
`quasi-axum` is the hosted adapter. What is left in it is axum's own: it mounts
|
| 116 |
117 |
|
as a fallback, so ordinary axum routes merged in front keep serving the things a
|
| 1 |
1 |
|
[package]
|
| 2 |
2 |
|
name = "quasi-axum"
|
| 3 |
|
- |
version = "0.41.0"
|
|
3 |
+ |
version = "0.42.0"
|
| 4 |
4 |
|
description = "The axum host adapter for quasi-router: an HTTP request in, a rendered description out"
|
| 5 |
5 |
|
edition.workspace = true
|
| 6 |
6 |
|
rust-version.workspace = true
|
| 13 |
13 |
|
workspace = true
|
| 14 |
14 |
|
|
| 15 |
15 |
|
[dependencies]
|
| 16 |
|
- |
quasi-router = { path = "../quasi-router", version = "0.41.0" }
|
| 17 |
|
- |
quasi-http = { path = "../quasi-http", version = "0.41.0" }
|
|
16 |
+ |
quasi-router = { path = "../quasi-router", version = "0.42.0" }
|
|
17 |
+ |
quasi-http = { path = "../quasi-http", version = "0.42.0" }
|
| 18 |
18 |
|
axum = "0.8.8"
|
| 19 |
19 |
|
http = "1.3.1"
|
| 20 |
20 |
|
tokio = { version = "1.50.0", features = ["rt"] }
|
| 21 |
21 |
|
|
| 22 |
22 |
|
[dev-dependencies]
|
| 23 |
|
- |
quasi-webview = { path = "../quasi-webview", version = "0.41.0" }
|
|
23 |
+ |
quasi-webview = { path = "../quasi-webview", version = "0.42.0" }
|
| 24 |
24 |
|
tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread"] }
|
| 25 |
25 |
|
tower = { version = "0.5.3", features = ["util"] }
|
| 26 |
26 |
|
http-body-util = "0.1.3"
|
| 1 |
1 |
|
[package]
|
| 2 |
2 |
|
name = "quasi-basics"
|
| 3 |
|
- |
version = "0.41.0"
|
|
3 |
+ |
version = "0.42.0"
|
| 4 |
4 |
|
description = "The first-party widget set: named assemblies of primitives, shared across our apps"
|
| 5 |
5 |
|
edition.workspace = true
|
| 6 |
6 |
|
rust-version.workspace = true
|
| 13 |
13 |
|
workspace = true
|
| 14 |
14 |
|
|
| 15 |
15 |
|
[dependencies]
|
| 16 |
|
- |
quasi-router = { path = "../quasi-router", version = "0.41.0" }
|
|
16 |
+ |
quasi-router = { path = "../quasi-router", version = "0.42.0" }
|
| 17 |
17 |
|
makeover-layout = "0.31.0"
|
| 18 |
18 |
|
|
| 19 |
19 |
|
[dev-dependencies]
|
| 20 |
20 |
|
# A widget's guarantees are claims about what a renderer draws, so they are
|
| 21 |
21 |
|
# tested against a real one rather than by walking the tree the assembly just
|
| 22 |
22 |
|
# built. The webview is the renderer that recognises names today.
|
| 23 |
|
- |
quasi-webview = { path = "../quasi-webview", version = "0.41.0" }
|
| 24 |
|
- |
quasi-http = { path = "../quasi-http", version = "0.41.0" }
|
|
23 |
+ |
quasi-webview = { path = "../quasi-webview", version = "0.42.0" }
|
|
24 |
+ |
quasi-http = { path = "../quasi-http", version = "0.42.0" }
|
| 1 |
1 |
|
[package]
|
| 2 |
2 |
|
name = "quasi-bench"
|
| 3 |
|
- |
version = "0.41.0"
|
|
3 |
+ |
version = "0.42.0"
|
| 4 |
4 |
|
description = "What a described screen costs to render, in time and in allocations"
|
| 5 |
5 |
|
edition.workspace = true
|
| 6 |
6 |
|
rust-version.workspace = true
|
| 22 |
22 |
|
count = ["dep:dhat"]
|
| 23 |
23 |
|
|
| 24 |
24 |
|
[dependencies]
|
| 25 |
|
- |
quasi-router = { path = "../quasi-router", version = "0.41.0" }
|
| 26 |
|
- |
quasi-webview = { path = "../quasi-webview", version = "0.41.0" }
|
| 27 |
|
- |
quasi-tui = { path = "../quasi-tui", version = "0.41.0" }
|
|
25 |
+ |
quasi-router = { path = "../quasi-router", version = "0.42.0" }
|
|
26 |
+ |
quasi-webview = { path = "../quasi-webview", version = "0.42.0" }
|
|
27 |
+ |
quasi-tui = { path = "../quasi-tui", version = "0.42.0" }
|
| 28 |
28 |
|
# `Serves` is the trait carrying `screen` and `fragment`, which is what the
|
| 29 |
29 |
|
# webview is measured through. Taken directly rather than through quasi-webview
|
| 30 |
30 |
|
# because a bench calling a trait method should name the trait it calls.
|
| 31 |
|
- |
quasi-http = { path = "../quasi-http", version = "0.41.0" }
|
|
31 |
+ |
quasi-http = { path = "../quasi-http", version = "0.42.0" }
|
| 32 |
32 |
|
makeover-layout = "0.31.0"
|
| 33 |
33 |
|
makeover-tui = { version = "0.30.0", features = ["theme"] }
|
| 34 |
34 |
|
# Only to load a bundled theme file. `makeover_tui::Theme` is `#[non_exhaustive]`,
|
| 1 |
1 |
|
[package]
|
| 2 |
2 |
|
name = "quasi-http"
|
| 3 |
|
- |
version = "0.41.0"
|
|
3 |
+ |
version = "0.42.0"
|
| 4 |
4 |
|
description = "The http-shaped seam quasi's webview host adapters share: decoding in, a rendered description out"
|
| 5 |
5 |
|
edition.workspace = true
|
| 6 |
6 |
|
rust-version.workspace = true
|
| 13 |
13 |
|
workspace = true
|
| 14 |
14 |
|
|
| 15 |
15 |
|
[dependencies]
|
| 16 |
|
- |
quasi-router = { path = "../quasi-router", version = "0.41.0" }
|
|
16 |
+ |
quasi-router = { path = "../quasi-router", version = "0.42.0" }
|
| 17 |
17 |
|
http = "1.3.1"
|
| 18 |
18 |
|
form_urlencoded = "1.2.2"
|
| 1 |
1 |
|
[package]
|
| 2 |
2 |
|
name = "quasi-immediate"
|
| 3 |
|
- |
version = "0.41.0"
|
|
3 |
+ |
version = "0.42.0"
|
| 4 |
4 |
|
edition = "2024"
|
| 5 |
5 |
|
description = "The immediate-mode renderer for quasi-router: a described screen in, an egui frame out. Immediate mode is the constraint that matters, not the library."
|
| 6 |
6 |
|
license = "MIT"
|
| 12 |
12 |
|
categories = ["gui"]
|
| 13 |
13 |
|
|
| 14 |
14 |
|
[dependencies]
|
| 15 |
|
- |
quasi-router = { path = "../quasi-router", version = "0.41.0" }
|
|
15 |
+ |
quasi-router = { path = "../quasi-router", version = "0.42.0" }
|
| 16 |
16 |
|
# The node drawing, which is the makeover layer's and not this crate's. Every
|
| 17 |
17 |
|
# widget here that is not a container comes from it: a screen walk that painted
|
| 18 |
18 |
|
# its own meter would be the divergence the suite exists to end, one copy per
|