azul_core/transient.rs
1//! `<transient-window>`: a popup that is a REAL OS window, drawn from a subtree
2//! of the one DOM.
3//!
4//! A colour picker that opens below its swatch, a tooltip with a pointer
5//! arrow, a tear-off tool palette - each wants its own window surface (so it
6//! can escape the parent's bounds, carry a shadow, sit above everything) but
7//! NOT its own application: it needs the parent's state, callbacks and
8//! styling, and it must open and close by flipping one attribute.
9//!
10//! That is what this node type provides. While `open == false` the element
11//! contributes nothing to layout - its subtree is not laid out at all. When
12//! `open == true` the engine materialises the subtree as a transient window
13//! anchored to the node's PARENT, routes input on that surface back into the
14//! same `LayoutWindow`, and tears it down when `open` flips back.
15//!
16//! The app never touches a window. It toggles `open`.
17//!
18//! ## Why not the existing menu path
19//!
20//! Context menus already become real OS windows on every backend, but each
21//! one runs a SEPARATE application window with its own layout callback and a
22//! copied `RefAny`. That is why the Wayland menu renders white (its private
23//! `LayoutWindow` never receives a layout pass from the parent's loop) and why
24//! Escape / outside-click are reimplemented per backend and broken on some. A
25//! transient window owns a SURFACE but renders a subtree of the PARENT's DOM:
26//! one tree, one event loop, one dismiss implementation.
27//!
28//! Tear-off and window shapes build on this.
29
30use alloc::vec::Vec;
31
32use crate::geom::{LogicalSize, OptionLogicalSize};
33
34/// Which edge of the anchor node a transient window opens from.
35///
36/// Expressed as an EDGE, never as coordinates. Wayland clients cannot address
37/// screen positions - the compositor hides them - so the only placement that
38/// works everywhere is "this edge of that rect, with this gravity", which
39/// `xdg_positioner` takes natively and the other backends can compute from.
40/// A design that stored `(x, y)` would work on X11 and be wrong on Wayland.
41#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[repr(C)]
43pub enum TransientAnchor {
44 /// Below the anchor, left edges aligned - what a dropdown or a colour
45 /// picker does. The default, because it is what `<select>` does.
46 #[default]
47 Bottom,
48 /// Above the anchor, left edges aligned.
49 Top,
50 /// To the left of the anchor, top edges aligned.
51 Left,
52 /// To the right of the anchor, top edges aligned - what a submenu does.
53 Right,
54 /// At the pointer position rather than the anchor rect - what a context
55 /// menu does.
56 Cursor,
57}
58
59/// What closes a transient window without the app asking.
60#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub enum TransientDismiss {
63 /// A press outside the window closes it, and so does Escape. The default:
64 /// it is how every popup a user has ever met behaves.
65 #[default]
66 Outside,
67 /// Only Escape closes it. For a popup the user interacts with by clicking
68 /// around it - a floating toolbar.
69 Escape,
70 /// Nothing closes it but the app. For palettes that stay up.
71 None,
72}
73
74/// Whether - and how - the user may drag a transient window away from its
75/// anchor.
76///
77/// A torn-off window is a free toplevel that is STILL the same DOM subtree:
78/// Photoshop palettes, GIMP tear-off menus, Firefox tear-off tabs. The drag
79/// starts on any node inside the window that declares `-azul-app-region:
80/// drag` (the window's own title strip); the engine moves the window with the
81/// pointer and decides on release.
82#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
83#[repr(C)]
84pub enum TransientTearoff {
85 /// The window stays where its anchor put it. The default.
86 #[default]
87 None,
88 /// Dropping it anywhere off its anchor makes it a free toplevel; dragging
89 /// the toplevel back over the anchor docks it again.
90 Free,
91 /// Like `Free`, and dropping it onto a DROP ZONE re-anchors the window
92 /// there instead. Zones are the nodes matching the selector carried in
93 /// the node's `tearoff-zone` attribute (`tearoff="zone:.sidebar"` in
94 /// XML sets both); they are hit-tested in the PARENT's layout.
95 Zone,
96}
97
98impl TransientTearoff {
99 /// `"true"` / `"free"` / `"1"` / `""` -> `Free`, `"zone"` or
100 /// `"zone:<selector>"` -> `Zone`, anything else -> `None`.
101 #[must_use]
102 pub fn parse(value: &str) -> Self {
103 let v = value.trim();
104 if matches!(v, "true" | "free" | "1" | "") {
105 Self::Free
106 } else if v == "zone" || v.starts_with("zone:") {
107 Self::Zone
108 } else {
109 Self::None
110 }
111 }
112}
113
114/// Where a transient window lives while it is NOT torn off.
115#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
116#[repr(C)]
117pub enum TransientDock {
118 /// A popup window anchored to an edge of its parent (or of the drop
119 /// zone it was dropped on). The default: menus, pickers, tooltips.
120 #[default]
121 Popup,
122 /// Laid out INLINE as ordinary content of its parent - or of the drop
123 /// zone it was dropped on, where it then scrolls, clips and reflows
124 /// with that zone's layout. The Visual-Studio tool-window model: drag
125 /// the grip out to float it (`tearoff`), drop it on another zone to move
126 /// it there, and the app's DOM never changes - the engine re-parents
127 /// the subtree in the layout tree.
128 Inline,
129}
130
131impl TransientDock {
132 /// `"inline"` -> `Inline`, anything else -> `Popup`.
133 #[must_use]
134 pub fn parse(value: &str) -> Self {
135 if value.trim() == "inline" {
136 Self::Inline
137 } else {
138 Self::Popup
139 }
140 }
141}
142
143/// The inline configuration of a `NodeType::TransientWindow`.
144///
145/// `Copy` and small on purpose: it rides inside `NodeType` the way
146/// `GeolocationProbeConfig` does, so opening a popup needs no allocation and
147/// `NodeType` (48 bytes, set by its largest payload) does not grow.
148#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
149#[repr(C)]
150pub struct TransientWindowConfig {
151 /// Explicit size, or `None` to size the window to its content - the
152 /// common case, and the reason a colour picker never has to guess how tall
153 /// its own panel is.
154 ///
155 /// `OptionLogicalSize`, the `#[repr(C, u8)]` option, NOT `Option<_>`: this
156 /// struct rides inside `NodeType`, which rides inside every `Dom` the C ABI
157 /// passes by value, and a Rust `Option` has no stable layout - clippy's
158 /// `improper_ctypes_definitions` flagged every `extern "C" fn -> Dom` in
159 /// the tree the moment a plain `Option` went in here.
160 pub size: OptionLogicalSize,
161 /// Which edge of the anchor (the node's parent) it opens from.
162 pub anchor: TransientAnchor,
163 /// What closes it.
164 pub dismiss: TransientDismiss,
165 /// Whether the user may drag the window OUT of its anchor into a free
166 /// toplevel that is still the same DOM subtree (see [`TransientTearoff`]).
167 pub tearoff: TransientTearoff,
168 /// Popup at an anchor edge (the default), or inline content of its
169 /// parent / drop zone that can be torn off and dropped elsewhere
170 /// (`dock="inline"`, see [`TransientDock`]).
171 pub dock: TransientDock,
172 /// The popup window's background material (`material="transparent"`):
173 /// `Transparent` gives the window per-pixel alpha, and its shape follows
174 /// what the content paints - rounded corners are real corners, a
175 /// pointer arrow is part of the window, a click beside it falls through.
176 /// A clip mask on the node (`set_clip_mask`, the same mask any DOM node
177 /// can carry) implies `Transparent`: the mask IS the window's shape.
178 pub material: crate::window::WindowBackgroundMaterial,
179 /// The ONLY thing an application toggles. `true` materialises the subtree
180 /// as a window; `false` tears it down and drops it from layout entirely.
181 pub open: bool,
182 /// The app's word on whether the window is currently torn off. Like
183 /// `open`, this is a REQUEST the engine follows on every change: flipping
184 /// it `true` tears the window off at its anchor position, flipping it
185 /// `false` docks it. In between, the user's own drags win - a palette the
186 /// user parked by the document does not snap back on the next layout
187 /// just because the app still says `torn="false"`. The app learns about
188 /// user tear-offs through `ComponentEventFilter::TornOff` / `Docked`.
189 pub torn: bool,
190}
191
192impl Default for TransientWindowConfig {
193 /// Closed, anchored below, dismissed by outside-click, content-sized.
194 fn default() -> Self {
195 Self {
196 open: false,
197 anchor: TransientAnchor::Bottom,
198 dismiss: TransientDismiss::Outside,
199 size: OptionLogicalSize::None,
200 tearoff: TransientTearoff::None,
201 dock: TransientDock::Popup,
202 material: crate::window::WindowBackgroundMaterial::Opaque,
203 torn: false,
204 }
205 }
206}
207
208impl TransientWindowConfig {
209 /// Closed, with every other field at its default.
210 #[must_use]
211 pub const fn closed() -> Self {
212 Self {
213 open: false,
214 anchor: TransientAnchor::Bottom,
215 dismiss: TransientDismiss::Outside,
216 size: OptionLogicalSize::None,
217 tearoff: TransientTearoff::None,
218 dock: TransientDock::Popup,
219 material: crate::window::WindowBackgroundMaterial::Opaque,
220 torn: false,
221 }
222 }
223
224 /// Open, with every other field at its default.
225 #[must_use]
226 pub const fn opened() -> Self {
227 Self {
228 open: true,
229 ..Self::closed()
230 }
231 }
232
233 #[must_use]
234 pub const fn with_anchor(mut self, anchor: TransientAnchor) -> Self {
235 self.anchor = anchor;
236 self
237 }
238
239 #[must_use]
240 pub const fn with_dismiss(mut self, dismiss: TransientDismiss) -> Self {
241 self.dismiss = dismiss;
242 self
243 }
244
245 #[must_use]
246 pub const fn with_size(mut self, size: LogicalSize) -> Self {
247 self.size = OptionLogicalSize::Some(size);
248 self
249 }
250
251 #[must_use]
252 pub const fn with_tearoff(mut self, tearoff: TransientTearoff) -> Self {
253 self.tearoff = tearoff;
254 self
255 }
256
257 #[must_use]
258 pub const fn with_torn(mut self, torn: bool) -> Self {
259 self.torn = torn;
260 self
261 }
262
263 #[must_use]
264 pub const fn with_material(
265 mut self,
266 material: crate::window::WindowBackgroundMaterial,
267 ) -> Self {
268 self.material = material;
269 self
270 }
271
272 #[must_use]
273 pub const fn with_dock(mut self, dock: TransientDock) -> Self {
274 self.dock = dock;
275 self
276 }
277}
278
279impl TransientWindowConfig {
280 /// Apply one XML/HTML attribute. Returns `true` if the key was one of ours.
281 ///
282 /// `size="WxH"` in logical px; anything else for a known key degrades to
283 /// the default rather than erroring - a typo must not make a popup refuse
284 /// to open. Unknown keys return `false` so the caller can treat them as
285 /// ordinary attributes (id, class, ...).
286 pub fn apply_attr(&mut self, key: &str, value: &str) -> bool {
287 match key {
288 "open" => {
289 self.open = matches!(value.trim(), "true" | "open" | "1" | "");
290 true
291 }
292 "anchor" => {
293 self.anchor = TransientAnchor::parse(value);
294 true
295 }
296 "dismiss" => {
297 self.dismiss = TransientDismiss::parse(value);
298 true
299 }
300 "tearoff" => {
301 self.tearoff = TransientTearoff::parse(value);
302 true
303 }
304 "torn" => {
305 self.torn = matches!(value.trim(), "true" | "1" | "");
306 true
307 }
308 "dock" => {
309 self.dock = TransientDock::parse(value);
310 true
311 }
312 "material" => {
313 self.material = match value.trim() {
314 "transparent" => crate::window::WindowBackgroundMaterial::Transparent,
315 "sidebar" => crate::window::WindowBackgroundMaterial::Sidebar,
316 "menu" => crate::window::WindowBackgroundMaterial::Menu,
317 "hud" => crate::window::WindowBackgroundMaterial::HUD,
318 "titlebar" => crate::window::WindowBackgroundMaterial::Titlebar,
319 "mica-alt" => crate::window::WindowBackgroundMaterial::MicaAlt,
320 _ => crate::window::WindowBackgroundMaterial::Opaque,
321 };
322 true
323 }
324 "size" => {
325 self.size = match value.trim().split_once('x') {
326 Some((w, h)) => match (w.trim().parse::<f32>(), h.trim().parse::<f32>()) {
327 (Ok(w), Ok(h)) if w > 0.0 && h > 0.0 => {
328 OptionLogicalSize::Some(LogicalSize::new(w, h))
329 }
330 _ => OptionLogicalSize::None,
331 },
332 None => OptionLogicalSize::None, // "content" or garbage
333 };
334 true
335 }
336 _ => false,
337 }
338 }
339}
340
341impl TransientAnchor {
342 /// The attribute value, as written in XML: `anchor="bottom"`.
343 #[must_use]
344 pub const fn as_str(self) -> &'static str {
345 match self {
346 Self::Bottom => "bottom",
347 Self::Top => "top",
348 Self::Left => "left",
349 Self::Right => "right",
350 Self::Cursor => "cursor",
351 }
352 }
353
354 /// Parse the attribute value. Unknown strings fall back to the default
355 /// rather than erroring: a typo in a popup's anchor should degrade to
356 /// "opens below", not to "does not open".
357 #[must_use]
358 pub fn parse(s: &str) -> Self {
359 match s.trim() {
360 "top" => Self::Top,
361 "left" => Self::Left,
362 "right" => Self::Right,
363 "cursor" => Self::Cursor,
364 _ => Self::Bottom,
365 }
366 }
367}
368
369impl TransientDismiss {
370 #[must_use]
371 pub const fn as_str(self) -> &'static str {
372 match self {
373 Self::Outside => "outside",
374 Self::Escape => "escape",
375 Self::None => "none",
376 }
377 }
378
379 #[must_use]
380 pub fn parse(s: &str) -> Self {
381 match s.trim() {
382 "escape" => Self::Escape,
383 "none" => Self::None,
384 _ => Self::Outside,
385 }
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 /// The default must be CLOSED. A popup that is open by default would
394 /// materialise a window for every `<transient-window>` in the tree on
395 /// first layout.
396 #[test]
397 fn the_default_is_closed_and_anchored_below() {
398 let c = TransientWindowConfig::default();
399 assert!(!c.open);
400 assert_eq!(c.anchor, TransientAnchor::Bottom);
401 assert_eq!(c.dismiss, TransientDismiss::Outside);
402 assert!(
403 matches!(c.size, OptionLogicalSize::None),
404 "content-sized unless told otherwise"
405 );
406 }
407
408 /// Attribute values round-trip, and unknown ones degrade to the default
409 /// rather than failing.
410 #[test]
411 fn anchor_and_dismiss_round_trip_through_their_attribute_strings() {
412 for a in [
413 TransientAnchor::Bottom,
414 TransientAnchor::Top,
415 TransientAnchor::Left,
416 TransientAnchor::Right,
417 TransientAnchor::Cursor,
418 ] {
419 assert_eq!(TransientAnchor::parse(a.as_str()), a);
420 }
421 for d in [
422 TransientDismiss::Outside,
423 TransientDismiss::Escape,
424 TransientDismiss::None,
425 ] {
426 assert_eq!(TransientDismiss::parse(d.as_str()), d);
427 }
428 assert_eq!(TransientAnchor::parse("sideways"), TransientAnchor::Bottom);
429 assert_eq!(TransientDismiss::parse("maybe"), TransientDismiss::Outside);
430 }
431
432 /// Attributes as written in XML produce the config they name.
433 #[test]
434 fn attributes_apply_onto_the_config() {
435 let mut c = TransientWindowConfig::closed();
436 assert!(c.apply_attr("open", "true"));
437 assert!(c.apply_attr("anchor", "right"));
438 assert!(c.apply_attr("dismiss", "escape"));
439 assert!(c.apply_attr("size", "320x240"));
440 assert!(c.apply_attr("tearoff", "true"));
441 assert!(c.apply_attr("torn", "true"));
442 assert!(c.apply_attr("material", "transparent"));
443 assert!(c.apply_attr("dock", "inline"));
444 assert!(
445 !c.apply_attr("class", "x"),
446 "not ours - the caller keeps it"
447 );
448
449 assert!(c.open);
450 assert_eq!(c.anchor, TransientAnchor::Right);
451 assert_eq!(c.dismiss, TransientDismiss::Escape);
452 assert!(
453 matches!(c.size, OptionLogicalSize::Some(s) if s.width == 320.0 && s.height == 240.0)
454 );
455 assert_eq!(c.tearoff, TransientTearoff::Free);
456 assert!(c.torn);
457 assert_eq!(
458 c.material,
459 crate::window::WindowBackgroundMaterial::Transparent
460 );
461 assert_eq!(c.dock, TransientDock::Inline);
462 c.apply_attr("dock", "popup");
463 assert_eq!(c.dock, TransientDock::Popup);
464 c.apply_attr("material", "opaque");
465 assert_eq!(c.material, crate::window::WindowBackgroundMaterial::Opaque);
466 c.apply_attr("tearoff", "zone:.sidebar");
467 assert_eq!(c.tearoff, TransientTearoff::Zone);
468 c.apply_attr("tearoff", "nope");
469 assert_eq!(c.tearoff, TransientTearoff::None);
470
471 // Degrade, never refuse: a bad size means content-sized.
472 c.apply_attr("size", "big");
473 assert!(matches!(c.size, OptionLogicalSize::None));
474 c.apply_attr("open", "false");
475 assert!(!c.open);
476 }
477
478 /// The config must not GROW `NodeType`.
479 ///
480 /// `NodeType` is 48 bytes today (measured 2026-08-22); the largest payload
481 /// sets that. This config rides inline, so it must fit under the existing
482 /// largest variant rather than under some round number - a first version
483 /// of this test said `<= 16` and failed at 28 bytes, which would have been
484 /// a false alarm about a struct that fits comfortably.
485 #[test]
486 fn the_config_does_not_grow_node_type() {
487 let cfg = core::mem::size_of::<TransientWindowConfig>();
488 let node = core::mem::size_of::<crate::dom::NodeType>();
489 assert!(
490 cfg < node,
491 "TransientWindowConfig is {cfg} bytes, NodeType is {node}: the config \
492 has become the largest payload and is now what sets NodeType's size"
493 );
494 }
495}
496
497/// Rebuild the subtree under `root` as a standalone [`crate::dom::Dom`], for
498/// laying out as the root of a transient window.
499///
500/// This is how a popup stays part of the ONE tree while owning its own
501/// surface. The node data is cloned - and `NodeData::clone` shares every
502/// `RefAny` by refcount, so a callback on the copy fires against the very same
503/// application state as the original. Nothing is re-parented, nothing is
504/// re-registered: the copy is a VIEW of the subtree that a second layout can
505/// consume, not a second widget.
506///
507/// `root` is the `<transient-window>` node itself. Its children become the
508/// popup's content; the transient node's own type is rewritten to a plain
509/// `Div` in the copy, because inside its own window it is just the container -
510/// leaving it as `TransientWindow` would make the popup's layout cut it out
511/// again (see `layout_tree::get_display_type`) and render nothing.
512///
513/// Returns `None` if `root` is not a `TransientWindow` or has no subtree to
514/// show, so a caller cannot open a window onto nothing.
515#[must_use]
516pub fn extract_subtree_as_dom(
517 styled_dom: &crate::styled_dom::StyledDom,
518 root: crate::id::NodeId,
519) -> Option<crate::dom::Dom> {
520 use crate::dom::NodeType;
521
522 {
523 let nodes = styled_dom.node_data.as_container();
524 let root_data = nodes.get(root)?;
525 if !matches!(root_data.get_node_type(), NodeType::TransientWindow(_)) {
526 return None;
527 }
528 }
529
530 let mut dom = build_subtree(styled_dom, root, true, 0)?;
531 // The container inside its own window is just a block.
532 dom.root.set_node_type(NodeType::Div);
533 Some(dom)
534}
535
536/// Clones `id` and its descendants into a fresh `Dom`, styles baked in.
537fn build_subtree(
538 styled_dom: &crate::styled_dom::StyledDom,
539 id: crate::id::NodeId,
540 is_root: bool,
541 depth: usize,
542) -> Option<crate::dom::Dom> {
543 use crate::styled_dom::NodeHierarchyItem;
544
545 if depth > 256 {
546 return None; // a malformed tree must not recurse forever
547 }
548 let nodes = styled_dom.node_data.as_container();
549 let hierarchy = styled_dom.node_hierarchy.as_container();
550 let mut data = nodes.get(id)?.clone();
551 bake_resolved_style(styled_dom, id, is_root, &mut data);
552 let mut dom = crate::dom::Dom::create_from_data(data);
553 let mut child = hierarchy.get(id).and_then(|h| h.first_child_id(id));
554 while let Some(c) = child {
555 if let Some(cd) = build_subtree(styled_dom, c, false, depth + 1) {
556 dom.add_child(cd);
557 }
558 child = hierarchy
559 .get(c)
560 .and_then(NodeHierarchyItem::next_sibling_id);
561 }
562 Some(dom)
563}
564
565/// Copies the style the PARENT tree's cascade resolved for `id` onto the
566/// extracted node as inline properties, so the copy looks exactly as the
567/// original did in place.
568///
569/// A `Dom::with_css(..)` sheet is *scoped*: it lives on the `Dom` subtree it
570/// was attached to and is selector-matched into the property cache when the
571/// `StyledDom` is built, after which the sheet itself is gone. Cloning
572/// `NodeData` alone therefore loses every author rule - the popup would come
573/// up unstyled, block-stretched, in the UA defaults. The resolved result is
574/// still in the cache, per node and per pseudo-state, so that is what travels:
575///
576/// - every node gets its matched author properties (`css_props`), keeping the
577/// `:hover`/`:active`/`:focus` variants as conditional inline rules;
578/// - the ROOT additionally gets every inheritable property it computed - its
579/// ancestors stay behind in the parent tree, so `body { font-family }` or a
580/// panel's `color` would otherwise be cut off at the popup's edge. Inside the
581/// subtree, inheritance is re-derived from the root by the normal cascade.
582///
583/// Inline rules outrank author rules in the new cascade, which is the intent:
584/// these ARE the author's resolved values, nothing in the popup's own tree
585/// should re-match them differently.
586fn bake_resolved_style(
587 styled_dom: &crate::styled_dom::StyledDom,
588 id: crate::id::NodeId,
589 is_root: bool,
590 data: &mut crate::dom::NodeData,
591) {
592 use azul_css::dynamic_selector::{CssPropertyWithConditions, DynamicSelector, PseudoStateType};
593
594 let cache = styled_dom.get_css_property_cache();
595 let i = id.index(); // `get_slice` is empty past the end, so no guard needed
596 let with_state = |p: &crate::prop_cache::StatefulCssProperty| CssPropertyWithConditions {
597 property: p.property.clone(),
598 apply_if: if p.state == PseudoStateType::Normal {
599 Vec::new().into()
600 } else {
601 vec![DynamicSelector::PseudoState(p.state)].into()
602 },
603 };
604 // Inherited first, so the node's own matched rules win on a clash (later
605 // inline rules outrank earlier ones at equal priority). `computed_values`
606 // holds the resolved Normal-state value of every property the node ends
607 // up with; the INHERITABLE ones are exactly "what the ancestors gave it"
608 // (font-size already resolved to px, so an `em` chain stays intact). The
609 // origin tag is not usable here - the UA sheet resolves `inherit` and
610 // re-stamps the result as the node's own. Non-inheritable entries must
611 // not travel: the transient node's own UA `position: absolute; top: 100%`
612 // would otherwise displace the popup's content inside its own window.
613 if is_root {
614 {
615 // The store is inheritable-only by invariant, but the filter stays:
616 // it states the requirement locally rather than trusting a caller.
617 for (prop_type, p) in cache.computed_values.values_for(i) {
618 if prop_type.is_inheritable() {
619 data.add_css_property(CssPropertyWithConditions {
620 property: p.property.clone(),
621 apply_if: Vec::new().into(),
622 });
623 }
624 }
625 }
626 }
627 for p in cache.css_props.get_slice(i) {
628 data.add_css_property(with_state(p));
629 }
630}
631
632#[cfg(test)]
633#[path = "transient_test.rs"]
634mod transient_test;