Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
viewport_reconcile.hpp
Go to the documentation of this file.
1// pulp #1899 — viewport reconciliation for runtime-imported content.
2//
3// Header-only helper for clamping oversize absolute-positioned
4// descendants to fit a viewport. Generic enough to live in core/view
5// because both the headless screenshot tool and the live host (and
6// any future live host running runtime-imported React trees) need
7// the same reconciliation.
8//
9// Background: imports (Spectr, v0.dev, Stitch, Figma exports) routinely
10// ship a top-level container with literal-CSS hardcoded dimensions that
11// exceed the runtime viewport. Canonical Spectr case
12// (`spectr-editor-extracted.js:4140`, originating in
13// `dom-adapter.tsx:440-441` as a workaround for what dom-adapter
14// perceived as a Yoga absolute-pin bug):
15// `<div style={{ position:'absolute', top:0, left:0,
16// width:1320, height:860, … }}>`
17// and the same hardcoded 1320×860 propagates several layers deep —
18// App (depth 1), FilterBank wrap (depth 2), individual canvases
19// (depth 3). In a 1280×800 viewport, anything `bottom:0`-anchored at
20// any of those depths falls off the captured frame.
21//
22// In a real browser the same content renders inside the same viewport
23// because the editor.html body uses
24// `display:flex; align-items:center; justify-content:center;
25// min-height:100vh`
26// with `flex-shrink: 1` (default), so the App flex item shrinks on its
27// main axis to fit the body's content box.
28//
29// This helper emulates that flex-shrink behavior for runtime-import
30// hosts. For any descendant of root_ with
31// `position:absolute|fixed` AND a `preferred_width|height` exceeding
32// the viewport AND no opposite-edge anchor (`right` for width,
33// `bottom` for height — i.e. the source told us a concrete size, not
34// "stretch me between two edges"), clamp the explicit size down to
35// the viewport size on that axis. Descendants anchored at `bottom:0`
36// / `right:0` then anchor to the visible edge instead of falling off.
37//
38// Scoped strictly to oversize-content + runtime-import usage; never
39// fires when content already fits, never modifies anchored content.
40
41#pragma once
42
43#include <pulp/view/view.hpp>
44#include <cstdint>
45#include <cstdio>
46#include <cstdlib>
47#include <string_view>
48
49namespace pulp::view {
50
52 float vw, float vh,
53 std::uint32_t viewport_width,
54 std::uint32_t viewport_height,
55 int depth,
56 const char* parent_label) {
57 using pulp::view::View;
58 if (view.position() != View::Position::absolute &&
59 view.position() != View::Position::fixed) {
60 return;
61 }
62 const float cw = view.flex().preferred_width;
63 const float ch = view.flex().preferred_height;
64 if (cw <= 0 && ch <= 0) return;
65 // Only act when the size is explicit (preferred_* set) AND the
66 // opposite edge isn't anchored — i.e. the source said "size me
67 // explicitly", not "stretch me from edge to edge".
68 //
69 // pulp #1906 — distinguish `right:auto` from explicit
70 // `right:0`. The previous predicate `(!has_right() || right==0)`
71 // treated `auto` and `0` identically, so an oversize child pinned
72 // with explicit `right:0` / `bottom:0` got force-clamped even
73 // though the source explicitly anchored it to the opposite edge.
74 // The correct test: only clamp when the opposite edge is truly
75 // unset (`auto`). Any explicit value — including 0 — is the
76 // source declaring edge-anchoring intent, which Yoga will honor
77 // via the inset → size derivation; defer to that.
78 const bool size_x_is_explicit = cw > 0 && !view.has_right();
79 const bool size_y_is_explicit = ch > 0 && !view.has_bottom();
80 bool clamped = false;
81 if (cw > vw && size_x_is_explicit) {
82 view.flex().preferred_width = vw;
83 clamped = true;
84 }
85 if (ch > vh && size_y_is_explicit) {
86 view.flex().preferred_height = vh;
87 clamped = true;
88 }
89 if (clamped && std::getenv("PULP_DUMP_BOUNDS")) {
90 std::fprintf(stderr,
91 "[viewport-reconcile] clamped oversize node depth=%d parent=%s: "
92 "(%.0fx%.0f) -> (%.0fx%.0f) in %ux%u viewport\n",
93 depth,
94 parent_label ? parent_label : "<root>",
95 cw, ch,
96 view.flex().preferred_width,
97 view.flex().preferred_height,
98 viewport_width, viewport_height);
99 }
100}
101
103 float vw, float vh,
104 std::uint32_t viewport_width,
105 std::uint32_t viewport_height,
106 int depth,
107 const char* parent_label) {
108 using pulp::view::View;
110 viewport_width, viewport_height,
111 depth, parent_label);
112 char child_label[64];
113 std::snprintf(child_label, sizeof(child_label), "depth%d", depth);
114 for (std::size_t i = 0; i < view.child_count(); ++i) {
115 auto* child = const_cast<View*>(view.child_at(i));
116 if (!child) continue;
117 walk_and_clamp(*child, vw, vh,
118 viewport_width, viewport_height,
119 depth + 1, child_label);
120 }
121}
122
141inline bool reconcile_disabled_by_env(const char* raw) {
142 if (!raw) return false;
143 const std::string_view value{raw};
144 if (value.empty()) return false;
145 return value != "0" && value != "false";
146}
147
149 std::uint32_t viewport_width,
150 std::uint32_t viewport_height) {
151 using pulp::view::View;
152 const float vw = static_cast<float>(viewport_width);
153 const float vh = static_cast<float>(viewport_height);
154 // root_ itself is always relative; start the recursive walk at its
155 // direct children (depth 1) and descend through every subtree node.
156 for (std::size_t i = 0; i < root.child_count(); ++i) {
157 auto* child = const_cast<View*>(root.child_at(i));
158 if (!child) continue;
159 walk_and_clamp(*child, vw, vh,
160 viewport_width, viewport_height,
161 /*depth=*/1, /*parent_label=*/"root");
162 }
163}
164
165} // namespace pulp::view
Definition view.hpp:73
View * child_at(size_t index)
Definition view.hpp:161
size_t child_count() const
Definition view.hpp:160
@ fixed
Definition view.hpp:1753
@ absolute
Definition view.hpp:1753
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
void walk_and_clamp(pulp::view::View &view, float vw, float vh, std::uint32_t viewport_width, std::uint32_t viewport_height, int depth, const char *parent_label)
Definition viewport_reconcile.hpp:102
bool reconcile_disabled_by_env(const char *raw)
Whether a caller has opted out of reconciliation, given the raw value of PULP_SHOT_NO_RECONCILE (null...
Definition viewport_reconcile.hpp:141
@ vh
Percentage of viewport height.
Definition geometry.hpp:396
@ vw
Percentage of viewport width.
Definition geometry.hpp:395
void reconcile_oversize_absolute_subtree(pulp::view::View &root, std::uint32_t viewport_width, std::uint32_t viewport_height)
Definition viewport_reconcile.hpp:148
void clamp_oversize_absolute_view(pulp::view::View &view, float vw, float vh, std::uint32_t viewport_width, std::uint32_t viewport_height, int depth, const char *parent_label)
Definition viewport_reconcile.hpp:51
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ i
Definition input_events.hpp:40