Skip to main content

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;