MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
TextField.cpp
Go to the documentation of this file.
1#include "TextField.hpp"
2
5
7
10
12
13namespace {
14
15 /// @brief Caret width in pixels, same space as PressParams::render_bounds.
16 constexpr float k_caret_width_px = 2.F;
17
18 /**
19 * @brief Redraw the caret for the current cursor, after the glyphs.
20 *
21 * The slice's run already holds the field's text, so its own caret rect
22 * and map put the caret exactly where the glyphs were laid out. Caret
23 * color reuses params.color (the text tint) rather than adding a new
24 * config field. This is the one write that also shows any text change
25 * made to the run just before it.
26 */
27 void render_caret(
28 internal::TextSlice& slice,
29 const Portal::Text::EditableText& edit,
30 const Portal::Text::PressParams& params)
31 {
32 const Kinesis::AABB2D caret = slice.run().caret(edit.cursor, k_caret_width_px);
33 const auto map = slice.map();
34 const glm::vec2 a = map(caret.min);
35 const glm::vec2 b = map(caret.max);
36
37 const auto quad = Kinesis::textured_mesh_rect(
38 { .min = glm::min(a, b), .max = glm::max(a, b) }, 0.F, glm::vec3(params.color));
39 slice.set_tail({ quad.begin(), quad.end() });
40 }
41
42 /// @brief Lay the edited text out in the slice's run. Nothing is written
43 /// until render_caret(), which follows every call.
44 void render_text(
45 internal::TextSlice& slice,
46 const Portal::Text::EditableText& edit,
47 const Portal::Text::PressParams& params)
48 {
49 slice.run().assign(edit.text, params);
50 }
51
52 /// @brief Resolve a cursor position on a line that may have no glyph quads.
53 size_t index_on_line(Portal::Text::TextRun& run, glm::vec2 px, float target_top)
54 {
55 const size_t nearest = run.index_at(px);
56 const auto* atlas = run.atlas();
57 if (!atlas || atlas->line_height() == 0)
58 return nearest;
59
60 const float half_line = static_cast<float>(atlas->line_height()) * 0.5F;
61 if (std::abs(run.caret(nearest, 0.F).min.y - target_top) < half_line)
62 return nearest;
63
64 const auto& text = run.text();
65 size_t line_start = 0;
66 while (true) {
67 if (std::abs(run.caret(line_start, 0.F).min.y - target_top) < half_line)
68 return line_start;
69 const size_t newline = text.find('\n', line_start);
70 if (newline == std::string::npos)
71 break;
72 line_start = newline + 1;
73 }
74
75 return nearest;
76 }
77
78 /**
79 * @brief Move the cursor to the nearest codepoint boundary one line
80 * above (line_delta = -1) or below (+1) its current line, at
81 * the same x position it currently sits at.
82 *
83 * Uses the run's caret positions to reach lines with no glyph quads.
84 */
85 void move_vertical(
86 Portal::Text::EditableText& edit,
87 internal::TextSlice& slice,
88 int line_delta)
89 {
90 auto& run = slice.run();
91 const auto* atlas = run.atlas();
92 if (!atlas)
93 return;
94
95 const auto caret = run.caret(edit.cursor, 0.F);
96 const float line_height = static_cast<float>(atlas->line_height());
97 const float target_top = caret.min.y + static_cast<float>(line_delta) * line_height;
98 const float target_y = target_top + static_cast<float>(atlas->ascender());
99 edit.cursor = index_on_line(run, { caret.min.x, target_y }, target_top);
100 }
101
102 /// @brief Inverse of the slice map's pixel-to-NDC mapping.
103 glm::vec2 pixel_at(glm::vec2 ndc, Kinesis::AABB2D field_bounds, glm::vec2 extent)
104 {
105 return {
106 (ndc.x - field_bounds.min.x) / field_bounds.width() * extent.x,
107 (field_bounds.max.y - ndc.y) / field_bounds.height() * extent.y
108 };
109 }
110
111 /// @brief Move the cursor to the codepoint boundary nearest an NDC
112 /// click position.
113 void move_to_click(
114 Portal::Text::EditableText& edit,
115 internal::TextSlice& slice,
116 Kinesis::AABB2D field_bounds,
117 glm::vec2 ndc)
118 {
119 auto& run = slice.run();
120 const glm::vec2 px = pixel_at(ndc, field_bounds, run.extent());
121 const auto* atlas = run.atlas();
122 if (!atlas || atlas->line_height() == 0) {
123 edit.cursor = run.index_at(px);
124 return;
125 }
126
127 const float line_height = static_cast<float>(atlas->line_height());
128 const float target_top = std::floor(px.y / line_height) * line_height;
129 edit.cursor = index_on_line(run, px, target_top);
130 }
131
132} // namespace
133
135 FormaTarget in_buf,
136 Surface& surface,
137 Kinesis::AABB2D bounds,
138 std::shared_ptr<Portal::Text::PressParams> in_params,
139 std::string initial_text)
140{
141 buf = std::move(in_buf);
143 m_live_bounds = std::make_shared<Kinesis::AABB2D>(bounds);
144
145 params = std::move(in_params);
146 if (!params)
147 params = std::make_shared<Portal::Text::PressParams>();
148
149 m_element = Element {}
150 .with_name("text_field")
153 .with_text(initial_text, *params, bounds);
155
156 element_id = surface.layer().add(m_element);
158
159 state = std::make_shared<MappedState<Portal::Text::EditableText>>();
160 state->value.cursor = initial_text.size();
161 state->value.text = std::move(initial_text);
162
163 render_caret(*slice, state->value, *params);
164
165 wire(surface.ctx());
166
167 return *this;
168}
169
171{
172 ctx.on_text(element_id, [sl = slice, s = state, p = params](uint32_t, uint32_t codepoint) mutable {
173 Portal::Text::EditableText edit = s->value;
174 Portal::Text::insert_codepoint(edit, codepoint);
175 s->write(edit);
176 render_text(*sl, edit, *p);
177 render_caret(*sl, edit, *p);
178 });
179
180 ctx.press_and_hold(element_id, IO::Keys::Backspace, [sl = slice, s = state, p = params](uint32_t) mutable {
181 Portal::Text::EditableText edit = s->value;
182 Portal::Text::erase(edit, -1);
183 s->write(edit);
184 render_text(*sl, edit, *p);
185 render_caret(*sl, edit, *p);
186 });
187
188 ctx.press_and_hold(element_id, IO::Keys::Delete, [sl = slice, s = state, p = params](uint32_t) mutable {
189 Portal::Text::EditableText edit = s->value;
190 Portal::Text::erase(edit, 1);
191 s->write(edit);
192 render_text(*sl, edit, *p);
193 render_caret(*sl, edit, *p);
194 });
195
196 ctx.press_and_hold(element_id, IO::Keys::Tab, [sl = slice, s = state, p = params](uint32_t) mutable {
197 Portal::Text::EditableText edit = s->value;
199 s->write(edit);
200 render_text(*sl, edit, *p);
201 render_caret(*sl, edit, *p);
202 });
203
204 ctx.press_and_hold(element_id, IO::Keys::Enter, [sl = slice, s = state, p = params](uint32_t) mutable {
205 Portal::Text::EditableText edit = s->value;
207 s->write(edit);
208 render_text(*sl, edit, *p);
209 render_caret(*sl, edit, *p);
210 });
211
212 ctx.press_and_hold(element_id, IO::Keys::Left, [sl = slice, s = state, p = params](uint32_t) mutable {
213 Portal::Text::EditableText edit = s->value;
214 Portal::Text::move(edit, -1);
215 s->write(edit);
216 render_caret(*sl, edit, *p);
217 });
218
219 ctx.press_and_hold(element_id, IO::Keys::Right, [sl = slice, s = state, p = params](uint32_t) mutable {
220 Portal::Text::EditableText edit = s->value;
221 Portal::Text::move(edit, 1);
222 s->write(edit);
223 render_caret(*sl, edit, *p);
224 });
225
226 ctx.press_and_hold(element_id, IO::Keys::Up, [sl = slice, s = state, p = params](uint32_t) mutable {
227 Portal::Text::EditableText edit = s->value;
228 move_vertical(edit, *sl, -1);
229 s->write(edit);
230 render_caret(*sl, edit, *p);
231 });
232
233 ctx.press_and_hold(element_id, IO::Keys::Down, [sl = slice, s = state, p = params](uint32_t) mutable {
234 Portal::Text::EditableText edit = s->value;
235 move_vertical(edit, *sl, 1);
236 s->write(edit);
237 render_caret(*sl, edit, *p);
238 });
239
241 [sl = slice, s = state, p = params, fb = m_live_bounds](uint32_t, glm::vec2 ndc) mutable {
242 Portal::Text::EditableText edit = s->value;
243 move_to_click(edit, *sl, *fb, ndc);
244 s->write(edit);
245 render_caret(*sl, edit, *p);
246 });
247}
248
250 FormaTarget in_buf,
251 Surface& surface,
252 Scrollable& scroller,
253 std::shared_ptr<Portal::Text::PressParams> in_params,
254 std::string initial_text,
255 float content_multiplier)
256{
257 const Kinesis::AABB2D viewport = scroller.bounds();
258 const Kinesis::AABB2D content_bounds {
259 .min = glm::vec2(viewport.min.x, viewport.max.y - viewport.height() * content_multiplier),
260 .max = glm::vec2(viewport.max.x, viewport.max.y)
261 };
262
263 place(std::move(in_buf), surface, content_bounds, std::move(in_params), std::move(initial_text));
264
265 TextField field = *this;
266 scroller.track(
267 surface.layer(), element_id, content_bounds,
268 [field](Kinesis::AABB2D shifted) mutable { field.reposition(shifted); },
269 buf);
270
271 surface.layer().relate(element_id, scroller.viewport_id);
272 surface.ctx().on_scroll(element_id, scroller.wheel_handler(surface));
273
274 return *this;
275}
276
278{
279 field_bounds = new_bounds;
280 if (m_live_bounds)
281 *m_live_bounds = new_bounds;
282
283 if (slice && state && params) {
284 slice->set_bounds(new_bounds);
285 render_caret(*slice, state->value, *params);
286 }
287}
288
289} // namespace MayaFlux::Portal::Forma
size_t a
size_t b
uint32_t px
void on_scroll(uint32_t id, ScrollFn fn)
Called on scroll while the cursor is over an element.
Definition Context.cpp:68
Context & press_and_hold(uint32_t id, IO::Keys key, KeyFn fn)
Register a key handler that fires on the initial press and on every repeat, with a deterministic firs...
Definition Context.cpp:213
void on_text(uint32_t id, TextFn fn)
Called with each resolved Unicode codepoint while the element has keyboard focus.
Definition Context.cpp:121
void on_press(uint32_t id, IO::MouseButtons btn, PressFn fn)
Called when a mouse button is pressed over an element.
Definition Context.cpp:38
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
bool relate(uint32_t primary_id, uint32_t related_id)
Record that related_id belongs with primary_id.
Definition Layer.cpp:205
Slot add(Element element)
Add an element to the layer.
Definition Layer.cpp:9
Context & ctx() noexcept
Access the event router.
Definition Surface.hpp:170
Layer & layer() noexcept
Access the spatial registry.
Definition Surface.hpp:159
Named owner of a (Window, Layer, Context) triple: the Forma canvas.
Definition Surface.hpp:121
void run()
Definition main.cpp:21
std::vector< double > slice(std::span< const double > data, double start_ratio, double end_ratio)
Extract a contiguous slice by ratio, returning a new vector.
std::array< Kakshya::MeshVertex, 6 > textured_mesh_rect(Kinesis::AABB2D region, float weight, glm::vec3 color)
Generate a UV-mapped TRIANGLE_LIST quad (6 vertices, 2 triangles) from an AABB2D, as Kakshya::MeshVer...
constexpr T map(T x, T in_lo, T in_hi, T out_lo, T out_hi) noexcept
Map x from [in_lo, in_hi] to [out_lo, out_hi], unclamped.
Definition Scalar.hpp:18
void move(EditableText &s, int n)
Move the cursor by n codepoints.
Definition TextEdit.cpp:20
void erase(EditableText &s, int n)
Erase n codepoints from the cursor.
Definition TextEdit.cpp:31
void insert_codepoint(EditableText &s, uint32_t codepoint)
Insert one codepoint at the cursor, advancing it past the insert.
Definition TextEdit.cpp:5
void insert_literal(EditableText &s, char c)
Insert one raw ASCII byte at the cursor, advancing it past the insert.
Definition TextEdit.cpp:14
float height() const noexcept
Definition Bounds.hpp:80
Axis-aligned bounding rectangle in a 2D coordinate space.
Definition Bounds.hpp:63
Element & with_bounds(Kinesis::AABB2D b)
Set the fast-reject AABB.
Definition Element.hpp:109
Element & with_buffer(FormaTarget target)
Attach a FormaBuffer, or a slice of one, as the rendered output for this region.
Definition Element.hpp:195
Element & with_text(std::string_view text, std::optional< Portal::Text::PressParams > params, Kinesis::AABB2D region={ .min=glm::vec2(-1.F),.max=glm::vec2(1.F) })
Draw text as glyph quads into the attached buffer.
Definition Element.cpp:41
std::shared_ptr< internal::TextSlice > text_slice
Text drawn into the attached buffer by with_text().
Definition Element.hpp:89
uint32_t id
Stable id assigned by Layer::add. Never zero.
Definition Element.hpp:63
Element & with_name(std::string n)
Set the human-readable name used in Lila introspection and debug output.
Definition Element.hpp:304
A bounded, renderable region on a window surface.
Definition Element.hpp:61
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.
uint32_t viewport_id
Element id of the viewport background. Valid after place().
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.
Kinesis::AABB2D bounds() const noexcept
Viewport region, satisfying Kinesis::HasBounds.
A clipped, scroll-offset viewport over content taller than it.
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
Mutable UTF-8 text plus a byte-offset cursor.
Definition TextEdit.hpp:31