MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
TextField.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "Scrollable.hpp"
4
7
8namespace MayaFlux::Buffers {
9class FormaBuffer;
10}
11
13
14class Context;
15class Surface;
16
17/**
18 * @struct TextField
19 * @brief An editable text region: keyboard/text-input wiring over an
20 * Element the caller already has a buffer for.
21 *
22 * Before place() is called, the struct carries no state. After place(),
23 * element_id, state, params, and field_bounds are populated. This mirrors
24 * the Scrollable/Collapsible pattern: place() takes a pre-built buffer
25 * (Portal::Forma::create_buffer at the call site) and does not itself
26 * depend on Forma.hpp - no file under Portal/Forma/ may, except Forma.cpp
27 * itself. Portal::Forma::create_text_field (in Forma.hpp) is the one-call
28 * convenience that builds the buffer and delegates here, the same split
29 * Portal::Forma::create<T> already has between itself and a Form<T>.
30 *
31 * Editing state lives in a Portal::Text::EditableText, mutated through
32 * Portal::Text::move()/erase()/insert_codepoint()/insert_literal() - this
33 * struct only wires Context callbacks to those operations and re-renders.
34 * It does not manage focus visuals; wire on_focus_gained/on_focus_lost
35 * separately, same as any other Forma element.
36 *
37 * Up/Down preserve the caret's horizontal position across lines. Clicking
38 * places the caret at the nearest text boundary. Both use the layout already
39 * drawn by the field's text run.
40 *
41 * The text is drawn as glyph quads from the glyph atlas, and a caret rides
42 * in the same slice rather than owning a buffer, Element, or Layer entry of
43 * its own: it is one flat Kakshya::MeshVertex quad (Kinesis::textured_mesh_rect
44 * at weight 0, so forma_multi.frag draws the vertex colour with no texture
45 * read) appended after the glyphs. Every keystroke and every reposition()
46 * writes glyphs and caret together in one submit, since FormaBuffer::submit()
47 * has no partial-range write. Caret position comes from Portal::Text::TextRun::caret(),
48 * which shares the layout origin of the glyphs.
49 *
50 * params is a shared, mutable Portal::Text::PressParams: every keystroke's
51 * re-render reads *params fresh, so a caller can change it once (e.g. a
52 * focus-driven background color) and have every subsequent keystroke see
53 * it, without re-wiring anything.
54 *
55 * buf is exposed (same convention Scrollable/Collapsible use) so a field
56 * can be clipped or scrolled with no TextField-specific plumbing: pass it
57 * directly as Scrollable::track()'s clip_buf to get scissor-to-viewport
58 * clipping for free, and wire reposition() as the track() reposition
59 * callback to move the field on scroll:
60 *
61 * @code
62 * auto buf = Portal::Forma::create_buffer(
63 * surface.render_config({ .additional_textures = { { "text", nullptr } } }));
64 *
65 * auto params = std::make_shared<Portal::Text::PressParams>(
66 * Portal::Text::PressParams { .color = { 0.9F, 0.9F, 0.9F, 1.F }, .render_bounds = { 800, 64 } });
67 *
68 * auto field = TextField {}.place(buf, surface, bounds, params, "hello");
69 *
70 * // Living inside a Scrollable, composed by hand:
71 * auto panel = Scrollable {}.place(viewport_buf, surface, viewport_bounds);
72 * panel.track(surface.layer(), field.element_id, field.bounds(),
73 * [field](Kinesis::AABB2D shifted) mutable { field.reposition(shifted); },
74 * field.buf);
75 * @endcode
76 *
77 * scrollable() is the fluent equivalent of that same composition, for the
78 * common single-field case - but the Scrollable itself is entirely the
79 * caller's: build and configure it however (place(), indicator(),
80 * background_color(), wheel_speed() - all fluent) before calling
81 * scrollable(), which only needs a buffer for itself, that already-placed
82 * Scrollable to track into, and how tall to grow relative to its viewport.
83 * Everything Scrollable already owns (the viewport buffer, its bounds, any
84 * indicator) stays there - this struct has no reason to know about it a
85 * second time. Portal::Forma::create_text_field(..., scrollable=true) in
86 * Forma.hpp is nothing more than a call to this.
87 */
88struct TextField {
89 // =========================================================================
90 // Results - populated by place()
91 // =========================================================================
92
93 /// @brief Element id of the text field. Valid after place().
94 uint32_t element_id {};
95
96 /// @brief Live editable text and cursor. Valid after place().
97 std::shared_ptr<MappedState<Portal::Text::EditableText>> state;
98
99 /// @brief Shared, mutable render params - every re-render reads this
100 /// fresh, so a caller can change it once and have every
101 /// subsequent keystroke see it. Valid after place().
102 std::shared_ptr<Portal::Text::PressParams> params;
103
104 /// @brief The text and caret drawn into buf. Valid after place().
105 std::shared_ptr<internal::TextSlice> slice;
106
107 /// @brief Target the text and caret draw into. Valid after place(). Pass
108 /// directly as Scrollable::track()'s clip_buf for scissor
109 /// clipping - no separate scissor call needed.
111
112 /// @brief NDC region occupied by the field. Valid after place().
114
115 /// @brief Field region, satisfying Kinesis::HasBounds.
116 [[nodiscard]] Kinesis::AABB2D bounds() const noexcept { return field_bounds; }
117
118 // =========================================================================
119 // Placement
120 // =========================================================================
121
122 /**
123 * @brief Register the text field element and wire text editing onto it.
124 *
125 * @param in_buf Pre-created target for the text - must have
126 * an additional_textures slot at index 0
127 * (Element::with_text's requirement).
128 * @param surface Surface to register the element on.
129 * @param bounds NDC region: hit-test bounds and the text.
130 * @param in_params Shared render params. A default-constructed one
131 * is allocated if null.
132 * @param initial_text Starting text. Cursor starts at its end.
133 * @return *this, with results populated.
134 */
135 MAYAFLUX_API TextField& place(
136 FormaTarget in_buf,
137 Surface& surface,
139 std::shared_ptr<Portal::Text::PressParams> in_params,
140 std::string initial_text = {});
141
142 /**
143 * @brief Fluent creator: place this field inside an already-placed
144 * Scrollable, growing itself taller than its viewport and
145 * track()ing itself in, wired to scroll past a fixed clip.
146 *
147 * scroller is entirely the caller's: build and configure it first -
148 * Scrollable::place(), and optionally indicator()/background_color()/
149 * wheel_speed(), all fluent - then pass it here by reference. This
150 * method reads scroller.bounds() for the viewport region, places this
151 * field at a content region content_multiplier times that viewport's
152 * height (top-aligned, same width), and calls scroller.track() with
153 * this field's own buf as clip_buf and reposition() as the reflow
154 * callback. The same three-call composition shown in the class doc,
155 * done once here so callers building the common single-field case do
156 * not have to repeat it - nothing about the viewport, its buffer, or
157 * any indicator passes through this call, because Scrollable already
158 * owns all of that.
159 *
160 * Also wires scroller.wheel_handler() onto this field's own element:
161 * the field sits on top of the viewport and is added to the Layer
162 * after it, so it hit-tests first, and Context::handle_scroll only
163 * ever dispatches to the topmost element under the cursor - without
164 * this, the wheel would do nothing while the cursor sits directly over
165 * the field's own text instead of the viewport's empty margin.
166 *
167 * Also relate()s element_id under scroller.viewport_id, so
168 * Portal::Forma::destroy(surface, element_id) tears down the
169 * scroller's viewport too, even though scroller itself is never owned
170 * or stored here - only its Layer-side element needs freeing this way.
171 *
172 * @param in_buf Pre-created target for the field's text.
173 * @param surface Surface to register the field's element on -
174 * must be the same Surface scroller was placed on.
175 * @param scroller Already-placed Scrollable to grow and track into.
176 * @param in_params Shared render params. A default-constructed
177 * one is allocated if null.
178 * @param initial_text Starting text. Cursor starts at its end.
179 * @param content_multiplier Content height as a multiple of scroller's
180 * viewport height. Default 3x: enough headroom
181 * to type past the viewport before hitting the
182 * limit, not so much that most content sits
183 * unreachable below a near-empty field.
184 * @return *this, with results populated.
185 */
186 MAYAFLUX_API TextField& scrollable(
187 FormaTarget in_buf,
188 Surface& surface,
189 Scrollable& scroller,
190 std::shared_ptr<Portal::Text::PressParams> in_params,
191 std::string initial_text = {},
192 float content_multiplier = 3.F);
193
194 // =========================================================================
195 // Post-placement
196 // =========================================================================
197
198 /**
199 * @brief Move the field to a new NDC region, keeping its content.
200 *
201 * The visual half of a scroll reflow - lays the text out again and
202 * resubmits it with the caret together at the new region (see the class doc's note on
203 * why both are always resubmitted together), nothing else. Wire
204 * directly as a Scrollable::track() reposition callback (see the class
205 * doc example); Scrollable's own reflow() already pushes the matching
206 * bounds_hint to the Layer, so hit-testing stays correct without this
207 * needing a Layer of its own.
208 *
209 * @param new_bounds Shifted NDC region.
210 */
211 MAYAFLUX_API void reposition(Kinesis::AABB2D new_bounds);
212
213private:
215
216 /// @brief Same value as field_bounds, behind a shared_ptr so wire()'s
217 /// already-captured Context closures see a reposition() that
218 /// happens after they were registered - the same reason params
219 /// is a shared_ptr instead of a captured-by-value copy.
220 std::shared_ptr<Kinesis::AABB2D> m_live_bounds;
221
222 void wire(Context& ctx);
223};
224
225} // namespace MayaFlux::Portal::Forma
Text editing operations: a mutable buffer plus a byte-offset cursor, and the operations that keep it ...
Event wiring between a Layer and a window surface.
Definition Context.hpp:41
Where an element's geometry and texture are written.
Definition Target.hpp:22
Named owner of a (Window, Layer, Context) triple: the Forma canvas.
Definition Surface.hpp:121
Context
Execution contexts for log messages.
Axis-aligned bounding rectangle in a 2D coordinate space.
Definition Bounds.hpp:63
A bounded, renderable region on a window surface.
Definition Element.hpp:61
std::shared_ptr< MappedState< Portal::Text::EditableText > > state
Live editable text and cursor. Valid after place().
Definition TextField.hpp:97
uint32_t element_id
Element id of the text field. Valid after place().
Definition TextField.hpp:94
MAYAFLUX_API TextField & place(FormaTarget in_buf, Surface &surface, Kinesis::AABB2D bounds, std::shared_ptr< Portal::Text::PressParams > in_params, std::string initial_text={})
Register the text field element and wire text editing onto it.
std::shared_ptr< Portal::Text::PressParams > params
Shared, mutable render params - every re-render reads this fresh, so a caller can change it once and ...
Kinesis::AABB2D bounds() const noexcept
Field region, satisfying Kinesis::HasBounds.
MAYAFLUX_API void reposition(Kinesis::AABB2D new_bounds)
Move the field to a new NDC region, keeping its content.
std::shared_ptr< internal::TextSlice > slice
The text and caret drawn into buf. Valid after place().
FormaTarget buf
Target the text and caret draw into.
std::shared_ptr< Kinesis::AABB2D > m_live_bounds
Same value as field_bounds, behind a shared_ptr so wire()'s already-captured Context closures see a r...
Kinesis::AABB2D field_bounds
NDC region occupied by the field. Valid after place().
MAYAFLUX_API TextField & scrollable(FormaTarget in_buf, Surface &surface, Scrollable &scroller, std::shared_ptr< Portal::Text::PressParams > in_params, std::string initial_text={}, float content_multiplier=3.F)
Fluent creator: place this field inside an already-placed Scrollable, growing itself taller than its ...
An editable text region: keyboard/text-input wiring over an Element the caller already has a buffer f...
Definition TextField.hpp:88