MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Scrollable.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "LayoutCursor.hpp"
5
7
8namespace MayaFlux::Buffers {
9class FormaBuffer;
10}
11
13
14class Layer;
15class Surface;
16
17/**
18 * @struct Tracked
19 * @brief One element registered with a Scrollable for reflow on scroll.
20 */
21struct Tracked {
22 uint32_t id {};
24 std::function<void(Kinesis::AABB2D)> reposition;
25};
26
27/**
28 * @struct Scrollable
29 * @brief A clipped, scroll-offset viewport over content taller than it.
30 *
31 * Before place() is called, the struct carries only configuration. After
32 * place(), viewport_id, scroll, offset, buf, and cursor_out are populated.
33 * This mirrors the Element/Collapsible pattern: configure with setters,
34 * register with a single explicit call.
35 *
36 * Scrollable owns a background/hit-test element sized to the viewport and
37 * a LayoutCursor (cursor_out) scoped to that viewport, already bound to the
38 * live scroll offset via LayoutCursor::bind_scroll, so content placed
39 * through it lands at the correct on-screen position immediately, scrolled
40 * or not. It does not own or know what kind of content is placed inside
41 * it: track() registers an element id plus the caller's own reposition
42 * callback, invoked with that element's scrolled bounds on every scroll.
43 * This is the same shape as Collapsible::attach(): Scrollable moves bounds
44 * and asks the caller's callback to keep visuals in sync, it does not
45 * reach into arbitrary geometry functions to do that itself. indicator()
46 * follows the same principle for a scroll readout: Scrollable computes the
47 * two numbers (position, extent) and drives sync() on the caller's own
48 * element, but draws nothing itself.
49 *
50 * Wheel input over the viewport is wired by place() as the default
51 * trigger, but scroll_by() is a plain method: any other input source (a
52 * dragged indicator, a key, MIDI, anything) can drive scrolling by calling
53 * it directly, with no dependency on Context::on_scroll at all.
54 *
55 * @code
56 * auto panel = Scrollable {}
57 * .background_color({ 0.08F, 0.08F, 0.1F })
58 * .place(buf, surface, viewport);
59 *
60 * const Kinesis::AABB2D bounds = panel.cursor_out.advance(row_h);
61 * auto row_buf = FormaTarget::slice_of(
62 * Portal::Forma::create_buffer(surface.render_config()),
63 * Portal::Graphics::PrimitiveTopology::TRIANGLE_STRIP);
64 * row_buf.submit(Kinesis::filled_rect(bounds, color));
65 * const uint32_t id = surface.layer().add(
66 * Element {}.with_bounds(bounds).with_buffer(row_buf).non_interactive());
67 *
68 * panel.track(surface.layer(), id, bounds,
69 * [row_buf, color](Kinesis::AABB2D shifted) {
70 * row_buf.submit(Kinesis::filled_rect(shifted, color));
71 * },
72 * row_buf);
73 * @endcode
74 */
75struct Scrollable {
76 // =========================================================================
77 // Results - populated by place()
78 // =========================================================================
79
80 /// @brief Element id of the viewport background. Valid after place().
81 uint32_t viewport_id {};
82
83 /// @brief Clamped scroll offset and content/viewport extents. Valid after place().
84 std::shared_ptr<Kinesis::ScrollState> scroll;
85
86 /// @brief Reactive mirror of scroll->offset, bound into cursor_out. Valid after place().
87 std::shared_ptr<MappedState<glm::vec2>> offset;
88
89 /// @brief FormaBuffer backing the viewport background. Valid after place().
91
92 /// @brief Cursor scoped to the viewport, bound to offset. Valid after place().
94
95 /// @brief NDC region occupied by the viewport. Valid after place().
97
98 /// @brief Viewport region, satisfying Kinesis::HasBounds.
99 [[nodiscard]] Kinesis::AABB2D bounds() const noexcept { return viewport_bounds; }
100
101 // =========================================================================
102 // Configuration - set before place()
103 // =========================================================================
104
105 /// @brief Background fill color. Default: glm::vec3(0.08F, 0.08F, 0.1F).
106 glm::vec3 m_background { 0.08F, 0.08F, 0.1F };
107
108 /// @brief Content-space units scrolled per wheel tick. Default: 0.05F.
109 float m_wheel_speed { 0.05F };
110
111 // =========================================================================
112 // Configuration setters
113 // =========================================================================
114
116 {
117 m_background = c;
118 return *this;
119 }
120
122 {
123 m_wheel_speed = s;
124 return *this;
125 }
126
127 // =========================================================================
128 // Placement
129 // =========================================================================
130
131 /**
132 * @brief Register the viewport element, wire wheel scrolling, and
133 * populate scroll, offset, buf, and cursor_out.
134 *
135 * @param in_buf Pre-created buffer for the viewport background. Its
136 * render processor is scissored to @p viewport, so give a
137 * slice of a create_buffer() buffer and create the tracked
138 * elements as slices of the same buffer: one scissor then
139 * clips them all. Elements on buffers of their own are
140 * scissored individually by track(), as before. Do not
141 * share this buffer with anything that must draw outside
142 * the viewport.
143 * @param surface Surface to register the viewport on.
144 * @param viewport NDC region: hit-test bounds, wheel-scroll trigger
145 * region, background quad, and the Scissor clip every
146 * track()ed element with a buffer gets.
147 * @return *this, with results populated.
148 */
149 MAYAFLUX_API Scrollable& place(
150 FormaTarget in_buf,
151 Surface& surface,
152 Kinesis::AABB2D viewport);
153
154 // =========================================================================
155 // Post-placement
156 // =========================================================================
157
158 /**
159 * @brief Register an element for scroll reflow.
160 *
161 * Expands scroll->content_bounds to include base_bounds, so the clamp
162 * in scroll_by() always reflects everything tracked so far - no manual
163 * content-height bookkeeping needed. On every scroll, this element's
164 * bounds_hint is updated to base_bounds.translated(offset) and
165 * reposition is called with that same shifted region: reposition owns
166 * whatever resubmission its own content needs (a raw buffer resubmit,
167 * rebuilding a Mapped<T>'s geometry, anything). Scrollable does not
168 * inspect or constrain what kind of content id is.
169 *
170 * base_bounds is normalized against the current offset before it is
171 * stored, so it is always safe to pass cursor_out.advance()'s return
172 * directly - that return is already shifted by whatever offset is
173 * live right now (that is the whole point of LayoutCursor::bind_scroll:
174 * an element placed after scrolling already happened lands in the right
175 * spot immediately). Without this normalization an element tracked
176 * while scrolled would have that offset baked into its anchor and
177 * drift further out of place on every scroll after.
178 *
179 * @param layer Layer both the viewport and @p id were registered
180 * on. relate()s id under viewport_id so a cascading
181 * Layer::remove(viewport_id) tears this element down
182 * too - no separate tracked-ids bookkeeping needed.
183 * @param id Element id, already registered on the same Layer
184 * place() used.
185 * @param base_bounds This element's current on-screen NDC bounds - the
186 * direct return of cursor_out.advance(), scrolled or not.
187 * @param reposition Called with the scrolled bounds on every scroll.
188 * @param clip_buf Optional. When given, Kinesis::Scissor::from(viewport_bounds)
189 * is set on its render processor immediately, so the
190 * element is clipped to the viewport from the start.
191 * @return *this, so place()/track()/indicator() can chain into one
192 * expression for the common case of a single tracked item -
193 * see Portal::Forma::TextField::scrollable() for exactly that.
194 */
195 MAYAFLUX_API Scrollable& track(
196 Layer& layer,
197 uint32_t id,
198 Kinesis::AABB2D base_bounds,
199 std::function<void(Kinesis::AABB2D)> reposition,
200 const FormaTarget& clip_buf = {});
201
202 /**
203 * @brief Apply a scroll delta and reflow every tracked element.
204 *
205 * The input-agnostic core: place() wires wheel input to this by
206 * default, but any other trigger can call it directly with its own
207 * delta - nothing here assumes the source.
208 *
209 * @param layer Layer the viewport and every tracked element were
210 * registered on.
211 * @param delta Content-space scroll delta, added to the current
212 * offset and clamped to scroll->content_bounds.
213 */
214 MAYAFLUX_API void scroll_by(Layer& layer, glm::vec2 delta);
215
216 /**
217 * @brief A self-contained wheel-scroll handler bound to this
218 * Scrollable's own state.
219 *
220 * place() wires exactly this behind the viewport's own on_scroll, but
221 * Context::handle_scroll dispatches to only the topmost element under
222 * the cursor (Layer::hit_test), so content drawn on top of the
223 * viewport - anything track()ed, since it registers after and so hit-
224 * tests first - never reaches the viewport's own on_scroll while the
225 * cursor sits over it. A caller in that position (TextField::
226 * scrollable() is exactly this case) registers the returned function
227 * on its own element's on_scroll instead of duplicating the wheel-
228 * delta-to-content-delta conversion by hand.
229 *
230 * Captures only this Scrollable's own shared state (scroll, offset,
231 * the tracked list, the indicator), the same shared_ptrs place()'s own
232 * wiring captures - safe to keep registered after this Scrollable
233 * value itself is destroyed, same as every other Context callback this
234 * struct wires.
235 *
236 * @param surface Surface owning the same Layer/Context place() used.
237 * @return A Context::ScrollFn-shaped callable, ready for on_scroll.
238 */
239 [[nodiscard]] MAYAFLUX_API std::function<void(uint32_t, glm::vec2, double, double)>
240 wheel_handler(Surface& surface) const;
241
242 /**
243 * @brief Attach a caller-built element as a live, draggable scroll
244 * indicator.
245 *
246 * Scrollable draws nothing itself. Build whatever Mapped<glm::vec2>
247 * your own geometry function produces - a bar, a squiggle, a sampled
248 * texture, anything computable from (position_frac, extent_frac) - via
249 * Portal::Forma::create<glm::vec2>, register it, and hand it here.
250 * position_frac is 0 at the top of content_bounds and 1 at the fully
251 * scrolled bottom; extent_frac is the viewport/content height ratio
252 * (how much of the content is visible at once), so a geometry function
253 * can size its own drawing without asking Scrollable anything further.
254 *
255 * On every scroll - wheel (place()'s default wiring), scroll_by(), or
256 * dragging this same element - the live fraction pair is written to
257 * el.state and el.sync() is called, the same "call sync() straight
258 * from the handler that changed the value, no polling" idiom
259 * Collapsible's own on_press already uses. track() also refreshes
260 * extent_frac (without a Layer, so el's hit region only, not
261 * bounds_hint, can lag very slightly until the next scroll -
262 * self-corrects on the first interaction after).
263 *
264 * Also wires on_drag on el.element.id: dragging maps the cursor's NDC
265 * y absolutely onto content offset within @p track, the same
266 * inverse-mapping idiom Geometry::vertical_fader uses for its own
267 * handle, routed through scroll_by() so drag and wheel scrolling stay
268 * in sync through one path. No handle-size compensation is applied,
269 * unlike vertical_fader - an indicator has no fixed rendered size for
270 * Scrollable to know about.
271 *
272 * @param surface Surface owning the same Layer/Context place() used.
273 * @param el Caller's Mapped<glm::vec2>, already registered on
274 * surface's Layer.
275 * @param track NDC region the drag is measured against; its full
276 * height maps to the full scrollable range.
277 * @return *this, for chaining - see track()'s @return.
278 */
279 MAYAFLUX_API Scrollable& indicator(Surface& surface, Mapped<glm::vec2> el, Kinesis::AABB2D track);
280
281private:
282 std::shared_ptr<std::vector<Tracked>> m_tracked;
283 std::shared_ptr<Mapped<glm::vec2>> m_indicator;
284};
285
286// =============================================================================
287// Free function escape hatch
288// =============================================================================
289
290/**
291 * @brief Construct a scrollable viewport.
292 *
293 * Thin wrapper delegating to Scrollable::place(). Preserved as an escape
294 * hatch, matching make_collapsible.
295 */
296[[nodiscard]] MAYAFLUX_API Scrollable make_scrollable(
297 FormaTarget buf,
298 Surface& surface,
299 Kinesis::AABB2D viewport,
300 glm::vec3 background_color = glm::vec3(0.08F, 0.08F, 0.1F),
301 float wheel_speed = 0.05F);
302
303} // namespace MayaFlux::Portal::Forma
Where an element's geometry and texture are written.
Definition Target.hpp:22
Flat registry of Elements on a surface.
Definition Layer.hpp:24
Reactive Y-position accumulator for vertical primitive stacking.
Named owner of a (Window, Layer, Context) triple: the Forma canvas.
Definition Surface.hpp:121
Scrollable make_scrollable(FormaTarget buf, Surface &surface, Kinesis::AABB2D viewport, glm::vec3 background_color, float wheel_speed)
Construct a scrollable viewport.
Axis-aligned bounding rectangle in a 2D coordinate space.
Definition Bounds.hpp:63
MAYAFLUX_API std::function< void(uint32_t, glm::vec2, double, double)> wheel_handler(Surface &surface) const
A self-contained wheel-scroll handler bound to this Scrollable's own state.
Kinesis::AABB2D viewport_bounds
NDC region occupied by the viewport. Valid after place().
std::shared_ptr< MappedState< glm::vec2 > > offset
Reactive mirror of scroll->offset, bound into cursor_out. Valid after place().
std::shared_ptr< Mapped< glm::vec2 > > m_indicator
uint32_t viewport_id
Element id of the viewport background. Valid after place().
FormaTarget buf
FormaBuffer backing the viewport background. Valid after place().
MAYAFLUX_API Scrollable & indicator(Surface &surface, Mapped< glm::vec2 > el, Kinesis::AABB2D track)
Attach a caller-built element as a live, draggable scroll indicator.
MAYAFLUX_API void scroll_by(Layer &layer, glm::vec2 delta)
Apply a scroll delta and reflow every tracked element.
MAYAFLUX_API Scrollable & track(Layer &layer, uint32_t id, Kinesis::AABB2D base_bounds, std::function< void(Kinesis::AABB2D)> reposition, const FormaTarget &clip_buf={})
Register an element for scroll reflow.
MAYAFLUX_API Scrollable & place(FormaTarget in_buf, Surface &surface, Kinesis::AABB2D viewport)
Register the viewport element, wire wheel scrolling, and populate scroll, offset, buf,...
glm::vec3 m_background
Background fill color. Default: glm::vec3(0.08F, 0.08F, 0.1F).
std::shared_ptr< std::vector< Tracked > > m_tracked
float m_wheel_speed
Content-space units scrolled per wheel tick. Default: 0.05F.
Scrollable & wheel_speed(float s)
std::shared_ptr< Kinesis::ScrollState > scroll
Clamped scroll offset and content/viewport extents. Valid after place().
Kinesis::AABB2D bounds() const noexcept
Viewport region, satisfying Kinesis::HasBounds.
LayoutCursor cursor_out
Cursor scoped to the viewport, bound to offset. Valid after place().
Scrollable & background_color(glm::vec3 c)
A clipped, scroll-offset viewport over content taller than it.
std::function< void(Kinesis::AABB2D)> reposition
One element registered with a Scrollable for reflow on scroll.