HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
text_field_widget.hpp
1// Copyright Take Vos 2021.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "text_field_delegate.hpp"
8#include "widget.hpp"
9#include "../text/editable_text.hpp"
10#include "../format.hpp"
11#include "../label.hpp"
12#include <memory>
13#include <string>
14#include <array>
15#include <optional>
16#include <future>
17
18namespace tt {
19
53template<typename T>
54class text_field_widget final : public widget {
55public:
56 using value_type = T;
58 using super = widget;
59
61
62 template<typename Value = observable<value_type>>
67 Value &&value = {}) noexcept :
69 value(std::forward<Value>(value)),
70 _delegate(delegate),
71 _field(theme::global->labelStyle),
72 _shaped_text()
73 {
74 _value_callback = this->value.subscribe([this](auto...) {
75 ttlet lock = std::scoped_lock(gui_system_mutex);
76 _request_relayout = true;
77 });
78 }
79
80 template<typename Value = observable<value_type>>
82 text_field_widget(window, parent, text_field_delegate_default<value_type>(), std::forward<Value>(value))
83 {
84 }
85
87
94 {
95 ttlet lock = std::scoped_lock(gui_system_mutex);
96 _delegate = delegate;
97 }
98
103 void set_continues(bool flag) noexcept
104 {
105 ttlet lock = std::scoped_lock(gui_system_mutex);
106 _continues = flag;
107 }
108
109 [[nodiscard]] bool update_constraints(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept override
110 {
111 tt_axiom(gui_system_mutex.recurse_lock_count());
112
113 if (super::update_constraints(display_time_point, need_reconstrain)) {
114 ttlet text_style = theme::global->labelStyle;
115 ttlet text_font_id = font_book::global->find_font(text_style.family_id, text_style.variant);
116 ttlet &text_font = font_book::global->get_font(text_font_id);
117 ttlet text_digit_width = text_font.description.DigitWidth * text_style.scaled_size();
118
119 if (auto delegate = _delegate.lock()) {
120 _text_width = std::ceil(text_digit_width * narrow_cast<float>(delegate->text_width(*this)));
121 } else {
122 _text_width = 100.0;
123 }
124
125 _preferred_size = {
126 extent2{_text_width + theme::global->margin * 2.0f, theme::global->smallSize + theme::global->margin * 2.0f},
127 extent2{std::numeric_limits<float>::infinity(), theme::global->smallSize + theme::global->margin * 2.0f}};
128 _width_resistance = 2;
129
130 return true;
131 } else {
132 return false;
133 }
134 }
135
136 void update_layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept override
137 {
138 tt_axiom(gui_system_mutex.recurse_lock_count());
139
140 if (_focus && display_time_point >= _next_redraw_time_point) {
141 request_redraw();
142 }
143
144 need_layout |= std::exchange(_request_relayout, false);
145 if (need_layout) {
146 _text_field_rectangle = aarectangle{extent2{_text_width + theme::global->margin * 2.0f, _size.height()}};
147
148 // Set the clipping rectangle to within the border of the input field.
149 // Add another border width, so glyphs do not touch the border.
150 _text_field_clipping_rectangle = intersect(_clipping_rectangle, _text_field_rectangle);
151
152 _text_rectangle = shrink(_text_field_rectangle, theme::global->margin);
153
154 ttlet field_str = static_cast<std::string>(_field);
155
156 if (auto delegate = _delegate.lock()) {
157 if (_focus) {
158 // Update the optional error value from the string conversion when the
159 // field has keyboard focus.
160 delegate->from_string(*this, field_str, _error);
161
162 } else {
163 // When field is not focused, simply follow the observed_value.
164 _field = delegate->to_string(*this, *value);
165 _error = {};
166 }
167
168 } else {
169 _field = {};
170 _error = l10n("system error: delegate missing");
171 }
172
173 _field.setStyleOfAll(theme::global->labelStyle);
174 _field.setWidth(std::numeric_limits<float>::infinity());
175 _shaped_text = _field.shapedText();
176
177 // Record the last time the text is modified, so that the caret remains lit.
178 _last_update_time_point = display_time_point;
179 }
180
181 super::update_layout(display_time_point, need_layout);
182 }
183
184 void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept override
185 {
186 tt_axiom(gui_system_mutex.recurse_lock_count());
187
188 _next_redraw_time_point = display_time_point + _blink_interval;
189
190 if (overlaps(context, this->_clipping_rectangle)) {
191 scroll_text();
192
193 draw_background_box(context);
194
195 // After drawing the border around the input field make sure any other
196 // drawing remains inside this border. And change the transform to account
197 // for how much the text has scrolled.
198 context.set_clipping_rectangle(_text_field_clipping_rectangle);
199 draw_selection_rectangles(context);
200 draw_partial_grapheme_caret(context);
201 draw_caret(context, display_time_point);
202 draw_text(context);
203 }
204
205 super::draw(std::move(context), display_time_point);
206 }
207
208 bool handle_event(command command) noexcept override
209 {
210 ttlet lock = std::scoped_lock(gui_system_mutex);
211 _request_relayout = true;
212
213 if (*enabled) {
214 switch (command) {
215 case command::text_edit_paste:
216 _field.handlePaste(window.get_text_from_clipboard());
217 commit(false);
218 return true;
219
220 case command::text_edit_copy: window.set_text_on_clipboard(_field.handleCopy()); return true;
221
222 case command::text_edit_cut: window.set_text_on_clipboard(_field.handleCut()); return true;
223
224 case command::gui_escape: revert(true); return true;
225
226 case command::gui_enter:
227 commit(true);
228 this->window.update_keyboard_target(
229 this->shared_from_this(), keyboard_focus_group::normal, keyboard_focus_direction::forward);
230 return true;
231
232 case command::gui_keyboard_enter:
233 revert(true);
234 // More processing of the gui_keyboard_enter command is required.
235 break;
236
237 case command::gui_keyboard_exit:
238 commit(true);
239 // More processing of the gui_keyboard_exit command is required.
240 break;
241
242 default:
243 if (_field.handle_event(command)) {
244 commit(false);
245 return true;
246 }
247 }
248 }
249
250 return super::handle_event(command);
251 }
252
253 bool handle_event(mouse_event const &event) noexcept override
254 {
255 ttlet lock = std::scoped_lock(gui_system_mutex);
256 auto handled = super::handle_event(event);
257
258 // Make sure we only scroll when dragging outside the widget.
259 _drag_scroll_speed_x = 0.0f;
260 _drag_click_count = event.clickCount;
261 _drag_select_position = event.position;
262
263 if (event.cause.leftButton) {
264 handled = true;
265
266 if (!*enabled) {
267 return true;
268 }
269
270 switch (event.type) {
271 using enum mouse_event::Type;
272 case ButtonDown:
273 if (_text_rectangle.contains(event.position)) {
274 ttlet mouseInTextPosition = _text_inv_translate * event.position;
275
276 switch (event.clickCount) {
277 case 1:
278 if (event.down.shiftKey) {
279 _field.dragmouse_cursorAtCoordinate(mouseInTextPosition);
280 } else {
281 _field.setmouse_cursorAtCoordinate(mouseInTextPosition);
282 }
283 break;
284 case 2: _field.selectWordAtCoordinate(mouseInTextPosition); break;
285 case 3: _field.selectParagraphAtCoordinate(mouseInTextPosition); break;
286 default:;
287 }
288
289 // Record the last time the cursor is moved, so that the caret remains lit.
290 _last_update_time_point = event.timePoint;
291
292 request_redraw();
293 }
294 break;
295
296 case Drag:
297 // When the mouse is dragged beyond the line input,
298 // start scrolling the text and select on the edge of the textRectangle.
299 if (event.position.x() > _text_rectangle.right()) {
300 // The mouse is on the right of the text.
301 _drag_select_position.x() = _text_rectangle.right();
302
303 // Scroll text to the left in points per second.
304 _drag_scroll_speed_x = 50.0f;
305
306 } else if (event.position.x() < _text_rectangle.left()) {
307 // The mouse is on the left of the text.
308 _drag_select_position.x() = _text_rectangle.left();
309
310 // Scroll text to the right in points per second.
311 _drag_scroll_speed_x = -50.0f;
312 }
313
314 drag_select();
315
316 request_redraw();
317 break;
318
319 default:;
320 }
321 }
322 return handled;
323 }
324
325 bool handle_event(keyboard_event const &event) noexcept override
326 {
327 ttlet lock = std::scoped_lock(gui_system_mutex);
328
329 auto handled = super::handle_event(event);
330
331 if (*enabled) {
332 switch (event.type) {
333 using enum keyboard_event::Type;
334
335 case grapheme:
336 handled = true;
337 _field.insertgrapheme(event.grapheme);
338 commit(false);
339 break;
340
341 case Partialgrapheme:
342 handled = true;
343 _field.insertPartialgrapheme(event.grapheme);
344 commit(false);
345 break;
346
347 default:;
348 }
349 }
350
351 _request_relayout = true;
352 return handled;
353 }
354
355 hit_box hitbox_test(point2 position) const noexcept override
356 {
357 tt_axiom(gui_system_mutex.recurse_lock_count());
358
359 if (_visible_rectangle.contains(position)) {
360 return hit_box{weak_from_this(), _draw_layer, *enabled ? hit_box::Type::TextEdit : hit_box::Type::Default};
361 } else {
362 return hit_box{};
363 }
364 }
365
366 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
367 {
368 tt_axiom(gui_system_mutex.recurse_lock_count());
369 return is_normal(group) && *enabled;
370 }
371
372 [[nodiscard]] color focus_color() const noexcept override
373 {
374 if (*enabled && window.active && _error) {
375 return theme::global->errorLabelStyle.color;
376 } else {
377 return super::focus_color();
378 }
379 }
380
381private:
382 typename decltype(value)::callback_ptr_type _value_callback;
383
385
386 bool _continues = false;
387
390 l10n _error;
391
392 float _text_width = 0.0f;
393 aarectangle _text_rectangle = {};
394
395 aarectangle _text_field_rectangle;
396 aarectangle _text_field_clipping_rectangle;
397
398 editable_text _field;
399 shaped_text _shaped_text;
400 aarectangle _left_to_right_caret = {};
401
405 float _drag_scroll_speed_x = 0.0f;
406
409 int _drag_click_count = 0;
410
411 point2 _drag_select_position = {};
412
415 float _text_scroll_x = 0.0f;
416
417 translate2 _text_translate;
418 translate2 _text_inv_translate;
419
420 static constexpr hires_utc_clock::duration _blink_interval = 500ms;
421 hires_utc_clock::time_point _next_redraw_time_point;
422 hires_utc_clock::time_point _last_update_time_point;
423
424 void revert(bool force) noexcept
425 {
426 if (auto delegate = _delegate.lock()) {
427 _field = delegate->to_string(*this, *value);
428 _error = {};
429 } else {
430 _field = std::string{};
431 _error = l10n("missing delegate");
432 }
433 }
434
435 void commit(bool force) noexcept
436 {
437 tt_axiom(gui_system_mutex.recurse_lock_count());
438 if (_continues || force) {
439 if (auto delegate = _delegate.lock()) {
440 auto optional_value = delegate->from_string(*this, static_cast<std::string>(_field), _error);
441 if (optional_value) {
442 value = *optional_value;
443 }
444 }
445 }
446 }
447
448 void drag_select() noexcept
449 {
450 tt_axiom(gui_system_mutex.recurse_lock_count());
451
452 ttlet mouseInTextPosition = _text_inv_translate * _drag_select_position;
453 switch (_drag_click_count) {
454 case 1: _field.dragmouse_cursorAtCoordinate(mouseInTextPosition); break;
455 case 2: _field.dragWordAtCoordinate(mouseInTextPosition); break;
456 case 3: _field.dragParagraphAtCoordinate(mouseInTextPosition); break;
457 default:;
458 }
459 }
460
461 void scroll_text() noexcept
462 {
463 if (_drag_scroll_speed_x != 0.0f) {
464 _text_scroll_x += _drag_scroll_speed_x * (1.0f / 60.0f);
465 drag_select();
466
467 // Once we are scrolling, don't stop.
468 request_redraw();
469
470 } else if (_drag_click_count == 0) {
471 // The following is for scrolling based on keyboard input, ignore mouse drags.
472
473 // Scroll the text a quarter width to the left until the cursor is within the width
474 // of the text field
475 if (_left_to_right_caret.left() - _text_scroll_x > _text_rectangle.width()) {
476 _text_scroll_x = _left_to_right_caret.left() - _text_rectangle.width() * 0.75f;
477 }
478
479 // Scroll the text a quarter width to the right until the cursor is within the width
480 // of the text field
481 while (_left_to_right_caret.left() - _text_scroll_x < 0.0f) {
482 _text_scroll_x = _left_to_right_caret.left() - _text_rectangle.width() * 0.25f;
483 }
484 }
485
486 // cap how far we scroll.
487 ttlet max_scroll_width = std::max(0.0f, _shaped_text.preferred_extent.width() - _text_rectangle.width());
488 _text_scroll_x = std::clamp(_text_scroll_x, 0.0f, max_scroll_width);
489
490 // Calculate how much we need to translate the text.
491 _text_translate = translate2{-_text_scroll_x, 0.0f} *
492 _shaped_text.translate_base_line(point2{_text_rectangle.left(), rectangle().middle()});
493 _text_inv_translate = ~_text_translate;
494 }
495
496 void draw_background_box(draw_context context) const noexcept
497 {
498 ttlet corner_shapes = tt::corner_shapes{0.0f, 0.0f, theme::global->roundingRadius, theme::global->roundingRadius};
499 context.draw_box(_text_field_rectangle, background_color(), corner_shapes);
500
501 ttlet line_rectangle = aarectangle{get<0>(_text_field_rectangle), extent2{_text_field_rectangle.width(), 1.0f}};
502 context.draw_filled_quad(translate3{0.0f, 0.0f, 0.1f} * line_rectangle, focus_color());
503 }
504
505 void draw_selection_rectangles(draw_context context) const noexcept
506 {
507 ttlet selection_rectangles = _field.selectionRectangles();
508 for (ttlet selection_rectangle : selection_rectangles) {
509 context.draw_filled_quad(_text_translate * translate_z(0.1f) * selection_rectangle, theme::global->textSelectColor);
510 }
511 }
512
513 void draw_partial_grapheme_caret(draw_context context) const noexcept
514 {
515 ttlet partial_grapheme_caret = _field.partialgraphemeCaret();
516 if (partial_grapheme_caret) {
517 context.draw_filled_quad(
518 _text_translate * translate_z(0.1f) * partial_grapheme_caret, theme::global->incompleteGlyphColor);
519 }
520 }
521
522 void draw_caret(draw_context context, hires_utc_clock::time_point display_time_point) noexcept
523 {
524 // Display the caret and handle blinking.
525 ttlet duration_since_last_update = display_time_point - _last_update_time_point;
526 ttlet nr_half_blinks = static_cast<int64_t>(duration_since_last_update / _blink_interval);
527
528 ttlet blink_is_on = nr_half_blinks % 2 == 0;
529 _left_to_right_caret = _field.leftToRightCaret();
530 if (_left_to_right_caret && blink_is_on && _focus && window.active) {
531 context.draw_filled_quad(_text_translate * translate_z(0.1f) * _left_to_right_caret, theme::global->cursorColor);
532 }
533 }
534
535 void draw_text(draw_context context) const noexcept
536 {
537 context.draw_text(_shaped_text, label_color(), _text_translate * translate_z(0.2f));
538 }
539};
540
541} // namespace tt
This is a RGBA floating point color.
Definition color.hpp:39
Class which represents an axis-aligned rectangle.
Definition axis_aligned_rectangle.hpp:18
bool contains(point2 const &rhs) const noexcept
Check if a 2D coordinate is inside the rectangle.
Definition axis_aligned_rectangle.hpp:222
Definition corner_shapes.hpp:9
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:91
constexpr float & x() noexcept
Access the x element from the point.
Definition point.hpp:85
Draw context for drawing using the TTauri shaders.
Definition draw_context.hpp:33
Definition gui_window.hpp:37
std::atomic< bool > active
Definition gui_window.hpp:69
virtual void set_text_on_clipboard(std::string str) noexcept=0
Place a text string on the operating system's clip-board.
void update_keyboard_target(std::shared_ptr< tt::widget > widget, keyboard_focus_group group=keyboard_focus_group::normal) noexcept
Change the keyboard focus to the given widget.
virtual std::string get_text_from_clipboard() const noexcept=0
Retrieve a text string from the operating system's clip-board.
Definition hit_box.hpp:15
Definition keyboard_event.hpp:40
Type
Definition keyboard_event.hpp:41
Definition mouse_event.hpp:15
A localizable string.
Definition l10n.hpp:12
Definition observable.hpp:20
void setStyleOfAll(text_style style) noexcept
Change the text style of all graphemes.
Definition editable_text.hpp:113
void insertgrapheme(grapheme character) noexcept
Definition editable_text.hpp:338
void insertPartialgrapheme(grapheme character) noexcept
Definition editable_text.hpp:320
Definition grapheme.hpp:21
Definition text_style.hpp:16
int recurse_lock_count() const noexcept
This function should be used in tt_axiom() to check if the lock is held by current thread.
Definition unfair_recursive_mutex.hpp:60
A single line text field.
Definition text_field_widget.hpp:54
bool handle_event(keyboard_event const &event) noexcept override
Handle keyboard event.
Definition text_field_widget.hpp:325
bool handle_event(mouse_event const &event) noexcept override
Definition text_field_widget.hpp:253
void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept override
Draw the widget.
Definition text_field_widget.hpp:184
void update_layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept override
Update the internal layout of the widget.
Definition text_field_widget.hpp:136
bool update_constraints(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept override
Update the constraints of the widget.
Definition text_field_widget.hpp:109
void set_delegate(std::weak_ptr< delegate_type > &&delegate) noexcept
Set the delegate The delegate is used to convert between the value type and the string the user sees ...
Definition text_field_widget.hpp:93
hit_box hitbox_test(point2 position) const noexcept override
Find the widget that is under the mouse cursor.
Definition text_field_widget.hpp:355
void set_continues(bool flag) noexcept
Set or unset continues mode.
Definition text_field_widget.hpp:103
bool handle_event(command command) noexcept override
Handle command.
Definition text_field_widget.hpp:208
bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
Check if the widget will accept keyboard focus.
Definition text_field_widget.hpp:366
Definition text_field_delegate.hpp:21
Definition widget.hpp:97
float margin() const noexcept
Get the margin around the Widget.
Definition widget.hpp:128
virtual void update_layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept
Update the internal layout of the widget.
observable< bool > enabled
The widget is enabled.
Definition widget.hpp:105
widget(gui_window &window, std::shared_ptr< abstract_container_widget > parent) noexcept
aarectangle rectangle() const noexcept
Get the rectangle in local coordinates.
Definition widget.hpp:342
virtual bool handle_event(command command) noexcept
Handle command.
gui_window & window
Convenient reference to the Window.
Definition widget.hpp:101
virtual void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept
Draw the widget.
Definition widget.hpp:462
virtual bool update_constraints(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept
Update the constraints of the widget.
abstract_container_widget const & parent() const noexcept
Get a reference to the parent.
T ceil(T... args)
T infinity(T... args)
T lock(T... args)
T max(T... args)
T move(T... args)