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 f32x4{_text_width + theme::global->margin * 2.0f, theme::global->smallSize + theme::global->margin * 2.0f},
127 f32x4{std::numeric_limits<float>::infinity(), theme::global->smallSize + theme::global->margin * 2.0f}};
128 _preferred_base_line = relative_base_line{vertical_alignment::middle, 0.0f, 200.0f};
129 _width_resistance = 2;
130
131 return true;
132 } else {
133 return false;
134 }
135 }
136
137 void update_layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept override
138 {
139 tt_axiom(gui_system_mutex.recurse_lock_count());
140
141 if (_focus && display_time_point >= _next_redraw_time_point) {
143 }
144
145 need_layout |= std::exchange(_request_relayout, false);
146 if (need_layout) {
147 _text_field_rectangle =
148 aarect{rectangle().x(), rectangle().y(), _text_width + theme::global->margin * 2.0f, rectangle().height()};
149 auto _text_field_window_rectangle = aarect{
150 _window_rectangle.x() + theme::global->borderWidth * 2.0f,
151 _window_rectangle.y(),
152 _text_width + theme::global->margin * 2.0f - theme::global->borderWidth * 4.0f,
153 _window_rectangle.height()};
154
155 // Set the clipping rectangle to within the border of the input field.
156 // Add another border width, so glyphs do not touch the border.
157 _text_field_clipping_rectangle = intersect(window_clipping_rectangle(), _text_field_window_rectangle);
158
159 _text_rectangle = shrink(_text_field_rectangle, theme::global->margin);
160
161 ttlet field_str = static_cast<std::string>(_field);
162
163 if (auto delegate = _delegate.lock()) {
164 if (_focus) {
165 // Update the optional error value from the string conversion when the
166 // field has keyboard focus.
167 delegate->from_string(*this, field_str, _error);
168
169 } else {
170 // When field is not focused, simply follow the observed_value.
171 _field = delegate->to_string(*this, *value);
172 _error = {};
173 }
174
175 } else {
176 _field = {};
177 _error = l10n("system error: delegate missing");
178 }
179
180 _field.setStyleOfAll(theme::global->labelStyle);
181 _field.setWidth(std::numeric_limits<float>::infinity());
182 _shaped_text = _field.shapedText();
183
184 // Record the last time the text is modified, so that the caret remains lit.
185 _last_update_time_point = display_time_point;
186 }
187
188 super::update_layout(display_time_point, need_layout);
189 }
190
191 void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept override
192 {
193 tt_axiom(gui_system_mutex.recurse_lock_count());
194
195 _next_redraw_time_point = display_time_point + _blink_interval;
196
197 if (overlaps(context, this->window_clipping_rectangle())) {
198 scroll_text();
199
200 draw_background_box(context);
201
202 // After drawing the border around the input field make sure any other
203 // drawing remains inside this border. And change the transform to account
204 // for how much the text has scrolled.
205 context.clipping_rectangle = _text_field_clipping_rectangle;
206 context.transform = (translate3{0.0, 0.0, 0.1f} * _text_translate) * context.transform;
207
208 draw_selection_rectangles(context);
209 draw_partial_grapheme_caret(context);
210 draw_caret(context, display_time_point);
211 draw_text(context);
212 }
213
214 super::draw(std::move(context), display_time_point);
215 }
216
217 bool handle_event(command command) noexcept override
218 {
219 ttlet lock = std::scoped_lock(gui_system_mutex);
220 _request_relayout = true;
221
222 if (*enabled) {
223 switch (command) {
224 case command::text_edit_paste:
225 _field.handlePaste(window.get_text_from_clipboard());
226 commit(false);
227 return true;
228
229 case command::text_edit_copy: window.set_text_on_clipboard(_field.handleCopy()); return true;
230
231 case command::text_edit_cut: window.set_text_on_clipboard(_field.handleCut()); return true;
232
233 case command::gui_escape: revert(true); return true;
234
235 case command::gui_enter:
236 commit(true);
237 this->window.update_keyboard_target(
238 this->shared_from_this(), keyboard_focus_group::normal, keyboard_focus_direction::forward);
239 return true;
240
241 case command::gui_keyboard_enter:
242 revert(true);
243 // More processing of the gui_keyboard_enter command is required.
244 break;
245
246 case command::gui_keyboard_exit:
247 commit(true);
248 // More processing of the gui_keyboard_exit command is required.
249 break;
250
251 default:
252 if (_field.handle_event(command)) {
253 commit(false);
254 return true;
255 }
256 }
257 }
258
259 return super::handle_event(command);
260 }
261
262 bool handle_event(mouse_event const &event) noexcept override
263 {
264 ttlet lock = std::scoped_lock(gui_system_mutex);
265 auto handled = super::handle_event(event);
266
267 // Make sure we only scroll when dragging outside the widget.
268 ttlet position = _from_window_transform * event.position;
269 _drag_scroll_speed_x = 0.0f;
270 _drag_click_count = event.clickCount;
271 _drag_select_position = position;
272
273 if (event.cause.leftButton) {
274 handled = true;
275
276 if (!*enabled) {
277 return true;
278 }
279
280 switch (event.type) {
281 using enum mouse_event::Type;
282 case ButtonDown:
283 if (_text_rectangle.contains(position)) {
284 ttlet mouseInTextPosition = _text_inv_translate * position;
285
286 switch (event.clickCount) {
287 case 1:
288 if (event.down.shiftKey) {
289 _field.dragmouse_cursorAtCoordinate(mouseInTextPosition);
290 } else {
291 _field.setmouse_cursorAtCoordinate(mouseInTextPosition);
292 }
293 break;
294 case 2: _field.selectWordAtCoordinate(mouseInTextPosition); break;
295 case 3: _field.selectParagraphAtCoordinate(mouseInTextPosition); break;
296 default:;
297 }
298
299 // Record the last time the cursor is moved, so that the caret remains lit.
300 _last_update_time_point = event.timePoint;
301
303 }
304 break;
305
306 case Drag:
307 // When the mouse is dragged beyond the line input,
308 // start scrolling the text and select on the edge of the textRectangle.
309 if (position.x() > _text_rectangle.p3().x()) {
310 // The mouse is on the right of the text.
311 _drag_select_position.x() = _text_rectangle.p3().x();
312
313 // Scroll text to the left in points per second.
314 _drag_scroll_speed_x = 50.0f;
315
316 } else if (position.x() < _text_rectangle.x()) {
317 // The mouse is on the left of the text.
318 _drag_select_position.x() = _text_rectangle.x();
319
320 // Scroll text to the right in points per second.
321 _drag_scroll_speed_x = -50.0f;
322 }
323
324 drag_select();
325
327 break;
328
329 default:;
330 }
331 }
332 return handled;
333 }
334
335 bool handle_event(keyboard_event const &event) noexcept override
336 {
337 ttlet lock = std::scoped_lock(gui_system_mutex);
338
339 auto handled = super::handle_event(event);
340
341 if (*enabled) {
342 switch (event.type) {
343 using enum keyboard_event::Type;
344
345 case grapheme:
346 handled = true;
347 _field.insertgrapheme(event.grapheme);
348 commit(false);
349 break;
350
351 case Partialgrapheme:
352 handled = true;
353 _field.insertPartialgrapheme(event.grapheme);
354 commit(false);
355 break;
356
357 default:;
358 }
359 }
360
361 _request_relayout = true;
362 return handled;
363 }
364
365 hit_box hitbox_test(f32x4 window_position) const noexcept override
366 {
367 ttlet lock = std::scoped_lock(gui_system_mutex);
368
369 if (window_clipping_rectangle().contains(window_position)) {
370 return hit_box{weak_from_this(), _draw_layer, *enabled ? hit_box::Type::TextEdit : hit_box::Type::Default};
371 } else {
372 return hit_box{};
373 }
374 }
375
376 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
377 {
378 tt_axiom(gui_system_mutex.recurse_lock_count());
379 return is_normal(group) && *enabled;
380 }
381
382private:
383 typename decltype(value)::callback_ptr_type _value_callback;
384
386
387 bool _continues = false;
388
391 l10n _error;
392
393 float _text_width = 0.0f;
394 aarect _text_rectangle = {};
395
396 aarect _text_field_rectangle;
397 aarect _text_field_clipping_rectangle;
398
399 editable_text _field;
400 shaped_text _shaped_text;
401 aarect _left_to_right_caret = {};
402
406 float _drag_scroll_speed_x = 0.0f;
407
410 int _drag_click_count = 0;
411
412 f32x4 _drag_select_position = {};
413
416 float _text_scroll_x = 0.0f;
417
418 translate2 _text_translate;
419 translate2 _text_inv_translate;
420
421 static constexpr hires_utc_clock::duration _blink_interval = 500ms;
422 hires_utc_clock::time_point _next_redraw_time_point;
423 hires_utc_clock::time_point _last_update_time_point;
424
425 void revert(bool force) noexcept
426 {
427 if (auto delegate = _delegate.lock()) {
428 _field = delegate->to_string(*this, *value);
429 _error = {};
430 } else {
431 _field = std::string{};
432 _error = l10n("missing delegate");
433 }
434 }
435
436 void commit(bool force) noexcept
437 {
438 tt_axiom(gui_system_mutex.recurse_lock_count());
439 if (_continues || force) {
440 if (auto delegate = _delegate.lock()) {
441 auto optional_value = delegate->from_string(*this, static_cast<std::string>(_field), _error);
442 if (optional_value) {
443 value = *optional_value;
444 }
445 }
446 }
447 }
448
449 void drag_select() noexcept
450 {
451 tt_axiom(gui_system_mutex.recurse_lock_count());
452
453 ttlet mouseInTextPosition = _text_inv_translate * _drag_select_position;
454 switch (_drag_click_count) {
455 case 1: _field.dragmouse_cursorAtCoordinate(mouseInTextPosition); break;
456 case 2: _field.dragWordAtCoordinate(mouseInTextPosition); break;
457 case 3: _field.dragParagraphAtCoordinate(mouseInTextPosition); break;
458 default:;
459 }
460 }
461
462 void scroll_text() noexcept
463 {
464 if (_drag_scroll_speed_x != 0.0f) {
465 _text_scroll_x += _drag_scroll_speed_x * (1.0f / 60.0f);
466 drag_select();
467
468 // Once we are scrolling, don't stop.
470
471 } else if (_drag_click_count == 0) {
472 // The following is for scrolling based on keyboard input, ignore mouse drags.
473
474 // Scroll the text a quarter width to the left until the cursor is within the width
475 // of the text field
476 if (_left_to_right_caret.x() - _text_scroll_x > _text_rectangle.width()) {
477 _text_scroll_x = _left_to_right_caret.x() - _text_rectangle.width() * 0.75f;
478 }
479
480 // Scroll the text a quarter width to the right until the cursor is within the width
481 // of the text field
482 while (_left_to_right_caret.x() - _text_scroll_x < 0.0f) {
483 _text_scroll_x = _left_to_right_caret.x() - _text_rectangle.width() * 0.25f;
484 }
485 }
486
487 // cap how far we scroll.
488 ttlet max_scroll_width = std::max(0.0f, _shaped_text.preferred_extent.width() - _text_rectangle.width());
489 _text_scroll_x = std::clamp(_text_scroll_x, 0.0f, max_scroll_width);
490
491 // Calculate how much we need to translate the text.
492 _text_translate = translate2{-_text_scroll_x, 0.0f} * _shaped_text.translate_base_line(f32x4{_text_rectangle.x(), base_line()});
493 _text_inv_translate = ~_text_translate;
494 }
495
496 void draw_background_box(draw_context context) const noexcept
497 {
498 ttlet line_color = context.line_color;
499
500 context.line_color = context.fill_color;
501 context.corner_shapes = {0.0f, 0.0f, theme::global->roundingRadius, theme::global->roundingRadius};
502 context.draw_box_with_border_inside(_text_field_rectangle);
503
504 ttlet line_rectangle = aarect{_text_field_rectangle.p0(), f32x4{_text_field_rectangle.width(), context.line_width}};
505 context.transform = context.transform * translate3{0.0f, 0.0f, 0.1f};
506 if (_error && window.active) {
507 context.fill_color = theme::global->errorLabelStyle.color;
508 } else {
509 context.fill_color = line_color;
510 }
511 context.draw_filled_quad(line_rectangle);
512 }
513
514 void draw_selection_rectangles(draw_context context) const noexcept
515 {
516 ttlet selection_rectangles = _field.selectionRectangles();
517 for (ttlet selection_rectangle : selection_rectangles) {
518 context.fill_color = theme::global->textSelectColor;
519 context.draw_filled_quad(selection_rectangle);
520 }
521 }
522
523 void draw_partial_grapheme_caret(draw_context context) const noexcept
524 {
525 ttlet partial_grapheme_caret = _field.partialgraphemeCaret();
526 if (partial_grapheme_caret) {
527 context.fill_color = theme::global->incompleteGlyphColor;
528 context.draw_filled_quad(partial_grapheme_caret);
529 }
530 }
531
532 void draw_caret(draw_context context, hires_utc_clock::time_point display_time_point) noexcept
533 {
534 // Display the caret and handle blinking.
535 ttlet duration_since_last_update = display_time_point - _last_update_time_point;
536 ttlet nr_half_blinks = static_cast<int64_t>(duration_since_last_update / _blink_interval);
537
538 ttlet blink_is_on = nr_half_blinks % 2 == 0;
539 _left_to_right_caret = _field.leftToRightCaret();
540 if (_left_to_right_caret && blink_is_on && _focus && window.active) {
541 context.fill_color = theme::global->cursorColor;
542 context.draw_filled_quad(_left_to_right_caret);
543 }
544 }
545
546 void draw_text(draw_context context) const noexcept
547 {
548 context.transform = translate3{0.0f, 0.0f, 0.2f} * context.transform;
549 context.draw_text(_shaped_text);
550 }
551};
552
553} // namespace tt
bool contains(numeric_array< T, 4 > const &rhs) const noexcept
Check if a 2D coordinate is inside the rectangle.
Definition aarect.hpp:300
Definition alignment.hpp:104
Definition translate.hpp:14
Draw context for drawing using the TTauri shaders.
Definition draw_context.hpp:33
Definition gui_window.hpp:39
std::atomic< bool > active
Definition gui_window.hpp:71
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.
void request_redraw(aarect rectangle=aarect::infinity()) noexcept
Request a rectangle on the window to be redrawn.
Definition gui_window.hpp:115
Definition hit_box.hpp:15
Definition keyboard_event.hpp:39
Type
Definition keyboard_event.hpp:40
Definition mouse_event.hpp:13
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:332
void insertPartialgrapheme(grapheme character) noexcept
Definition editable_text.hpp:314
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:335
bool handle_event(mouse_event const &event) noexcept override
Definition text_field_widget.hpp:262
void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept override
Draw the widget.
Definition text_field_widget.hpp:191
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:137
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
hit_box hitbox_test(f32x4 window_position) const noexcept override
Find the widget that is under the mouse cursor.
Definition text_field_widget.hpp:365
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
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:217
bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
Check if the widget will accept keyboard focus.
Definition text_field_widget.hpp:376
Definition text_field_delegate.hpp:21
Definition widget.hpp:96
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
virtual bool handle_event(command command) noexcept
Handle command.
gui_window & window
Convenient reference to the Window.
Definition widget.hpp:100
virtual void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept
Draw the widget.
Definition widget.hpp:460
float base_line() const noexcept
Get the base-line in local coordinates.
Definition widget.hpp:350
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.
virtual aarect window_clipping_rectangle() const noexcept
Get the clipping-rectangle in window coordinates.
Definition widget.hpp:320
aarect rectangle() const noexcept
Get the rectangle in local coordinates.
Definition widget.hpp:340
T ceil(T... args)
T infinity(T... args)
T lock(T... args)
T max(T... args)
T move(T... args)