HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
text_field_widget.hpp
Go to the documentation of this file.
1// Copyright Take Vos 2021-2022.
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
8
9#pragma once
10
12#include "widget.hpp"
13#include "label_widget.hpp"
14#include "scroll_widget.hpp"
15#include "../l10n/l10n.hpp"
16#include "../macros.hpp"
17#include <memory>
18#include <string>
19#include <array>
20#include <optional>
21#include <future>
22#include <coroutine>
23
24hi_export_module(hikogui.widgets.text_field_widget);
25
26hi_export namespace hi { inline namespace v1 {
27
28template<typename Context>
30
66class text_field_widget : public widget {
67public:
68 using delegate_type = text_field_delegate;
69 using super = widget;
70
72
77
80 observer<alignment> alignment = alignment::middle_flush();
81
82 virtual ~text_field_widget()
83 {
84 hi_assert_not_null(delegate);
85 delegate->deinit(*this);
86 }
87
89 super(), delegate(std::move(delegate)), _text()
90 {
91 hi_assert_not_null(this->delegate);
92 _delegate_cbt = this->delegate->subscribe([&] {
93 ++global_counter<"text_field_widget:delegate:layout">;
95 });
96 this->delegate->init(*this);
97
98 _scroll_widget = std::make_unique<scroll_widget<axis::none>>();
99 _scroll_widget->set_parent(this);
100
101 _text_widget = &_scroll_widget->emplace<text_widget>(_text, alignment);
102 _text_widget->set_mode(widget_mode::partial);
103
104 _error_label_widget = std::make_unique<label_widget>(_error_label, alignment::top_left());
105 _error_label_widget->set_parent(this);
106
107 _continues_cbt = continues.subscribe([&](auto...) {
108 ++global_counter<"text_field_widget:continues:constrain">;
110 });
111 _text_cbt = _text.subscribe([&](auto...) {
112 ++global_counter<"text_field_widget:text:constrain">;
114 });
115 _error_label_cbt = _error_label.subscribe([&](auto const& new_value) {
116 ++global_counter<"text_field_widget:error_label:constrain">;
118 });
119 }
120
121 template<text_field_widget_attribute... Attributes>
122 text_field_widget(
123 std::shared_ptr<delegate_type> delegate,
124 Attributes&&...attributes) noexcept :
125 text_field_widget(std::move(delegate))
126 {
127 set_attributes(std::forward<Attributes>(attributes)...);
128 }
129
136 template<incompatible_with<std::shared_ptr<delegate_type>> Value, text_field_widget_attribute... Attributes>
138 Value&& value,
139 Attributes&&...attributes) noexcept requires requires
140 {
143
145 [[nodiscard]] generator<widget_intf&> children(bool include_invisible) noexcept override
146 {
147 if (_scroll_widget) {
148 co_yield *_scroll_widget;
149 }
150 if (_error_label_widget) {
151 co_yield *_error_label_widget;
152 }
153 }
154
155 [[nodiscard]] box_constraints update_constraints() noexcept override
156 {
157 hi_assert_not_null(delegate);
158 hi_assert_not_null(_error_label_widget);
159 hi_assert_not_null(_scroll_widget);
160
161 if (_text_widget->focus()) {
162 // Update the optional error value from the string conversion when the text-widget has keyboard focus.
163 _error_label = delegate->validate(*this, *_text);
164
165 } else {
166 // When field is not focused, simply follow the observed_value.
167 revert(false);
168 }
169
170 _layout = {};
171 _scroll_constraints = _scroll_widget->update_constraints();
172
173 auto const scroll_width = 100;
174 auto const box_size = extent2{
175 _scroll_constraints.margins.left() + scroll_width + _scroll_constraints.margins.right(),
176 _scroll_constraints.margins.top() + _scroll_constraints.preferred.height() + _scroll_constraints.margins.bottom()};
177
178 auto size = box_size;
179 auto margins = theme().margin();
180 if (_error_label->empty()) {
181 _error_label_widget->set_mode(widget_mode::invisible);
182 _error_label_constraints = _error_label_widget->update_constraints();
183
184 } else {
185 _error_label_widget->set_mode(widget_mode::display);
186 _error_label_constraints = _error_label_widget->update_constraints();
187 inplace_max(size.width(), _error_label_constraints.preferred.width());
188 size.height() += _error_label_constraints.margins.top() + _error_label_constraints.preferred.height();
189 inplace_max(margins.left(), _error_label_constraints.margins.left());
190 inplace_max(margins.right(), _error_label_constraints.margins.right());
191 inplace_max(margins.bottom(), _error_label_constraints.margins.bottom());
192 }
193
194 // The alignment of a text-field is not based on the text-widget due to the intermediate scroll widget.
195 auto const resolved_alignment = resolve_mirror(*alignment, os_settings::left_to_right());
196
197 return {size, size, size, resolved_alignment, margins};
198 }
199 void set_layout(widget_layout const& context) noexcept override
200 {
201 if (compare_store(_layout, context)) {
202 auto const scroll_size = extent2{
203 context.width(),
204 _scroll_constraints.margins.top() + _scroll_constraints.preferred.height() +
205 _scroll_constraints.margins.bottom()};
206
207 auto const scroll_rectangle = aarectangle{point2{0, context.height() - scroll_size.height()}, scroll_size};
208 _scroll_shape = box_shape{_scroll_constraints, scroll_rectangle, theme().baseline_adjustment()};
209
210 if (_error_label_widget->mode() > widget_mode::invisible) {
211 auto const error_label_rectangle =
212 aarectangle{0, 0, context.rectangle().width(), _error_label_constraints.preferred.height()};
213 _error_label_shape = box_shape{_error_label_constraints, error_label_rectangle, theme().baseline_adjustment()};
214 }
215 }
216
217 if (_error_label_widget->mode() > widget_mode::invisible) {
218 _error_label_widget->set_layout(context.transform(_error_label_shape));
219 }
220 _scroll_widget->set_layout(context.transform(_scroll_shape));
221 }
222 void draw(draw_context const& context) noexcept override
223 {
224 if (mode() > widget_mode::invisible and overlaps(context, layout())) {
225 draw_background_box(context);
226
227 _scroll_widget->draw(context);
228 _error_label_widget->draw(context);
229 }
230 }
231 bool handle_event(gui_event const& event) noexcept override
232 {
233 switch (event.type()) {
234 case gui_event_type::gui_cancel:
235 if (mode() >= widget_mode::partial) {
236 revert(true);
237 return true;
238 }
239 break;
240
241 case gui_event_type::gui_activate:
242 if (mode() >= widget_mode::partial) {
243 commit(true);
244 return super::handle_event(event);
245 }
246 break;
247
248 default:;
249 }
250
251 return super::handle_event(event);
252 }
253 hitbox hitbox_test(point2 position) const noexcept override
254 {
255 if (mode() >= widget_mode::partial) {
256 auto r = hitbox{};
257 r = _scroll_widget->hitbox_test_from_parent(position, r);
258 r = _error_label_widget->hitbox_test_from_parent(position, r);
259 return r;
260 } else {
261 return hitbox{};
262 }
263 }
264 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
265 {
266 if (mode() >= widget_mode::partial) {
267 return _scroll_widget->accepts_keyboard_focus(group);
268 } else {
269 return false;
270 }
271 }
272 [[nodiscard]] color focus_color() const noexcept override
273 {
274 if (mode() >= widget_mode::partial) {
275 if (not _error_label->empty()) {
276 auto const error_style = theme().text_style_set()[{phrasing::error}];
277 return error_style.color();
278 } else if (_text_widget->focus()) {
279 return theme().accent_color();
280 } else if (phase() == widget_phase::hover) {
281 return theme().border_color(_layout.layer + 1);
282 } else {
283 return theme().border_color(_layout.layer);
284 }
285
286 } else {
287 return theme().border_color(_layout.layer - 1);
288 }
289 }
291private:
294 std::unique_ptr<scroll_widget<axis::none>> _scroll_widget;
295 box_constraints _scroll_constraints;
296 box_shape _scroll_shape;
297
300 text_widget *_text_widget = nullptr;
301
304 observer<gstring> _text;
305
308 observer<label> _error_label;
309 std::unique_ptr<label_widget> _error_label_widget;
310 box_constraints _error_label_constraints;
311 box_shape _error_label_shape;
312
313 callback<void()> _delegate_cbt;
314 callback<void(bool)> _continues_cbt;
315 callback<void(gstring)> _text_cbt;
316 callback<void(label)> _error_label_cbt;
317
318 void set_attributes() noexcept {}
319
320 template<text_field_widget_attribute First, text_field_widget_attribute... Rest>
321 void set_attributes(First&& first, Rest&&...rest) noexcept
322 {
323 if constexpr (forward_of<First, observer<hi::alignment>>) {
325 } else {
326 hi_static_no_default();
327 }
328
329 set_attributes(std::forward<Rest>(rest)...);
330 }
331
332 void revert(bool force) noexcept
333 {
334 hi_assert_not_null(delegate);
335 _text = delegate->text(*this);
336 _error_label = label{};
337 }
338 void commit(bool force) noexcept
339 {
340 hi_axiom(loop::main().on_thread());
341 hi_assert_not_null(delegate);
342
343 if (*continues or force) {
344 auto text = *_text;
345
346 if (delegate->validate(*this, text).empty()) {
347 // text is valid.
348 delegate->set_text(*this, text);
349 }
350
351 // After commit get the canonical text to display from the delegate.
352 _text = delegate->text(*this);
353 _error_label = label{};
354 }
355 }
356 void draw_background_box(draw_context const& context) const noexcept
357 {
358 auto const outline = _scroll_shape.rectangle;
359
360 auto const corner_radii = hi::corner_radii(0.0f, 0.0f, theme().rounding_radius<float>(), theme().rounding_radius<float>());
361 context.draw_box(layout(), outline, background_color(), corner_radii);
362
363 auto const line = line_segment(get<0>(outline), get<1>(outline));
364 context.draw_line(layout(), translate3{0.0f, 0.5f, 0.1f} * line, theme().border_width(), focus_color());
365 }
366};
367
368}} // namespace hi::v1
Defines widget.
Defines delegate_field_delegate and some default text field delegates.
Defines label_widget.
Defines scroll_widget.
@ window_relayout
Request that widgets get laid out on the next frame.
Definition gui_event_type.hpp:47
@ window_reconstrain
Request that widget get constraint on the next frame.
Definition gui_event_type.hpp:48
std::shared_ptr< text_field_delegate > make_default_text_field_delegate(Value &&value) noexcept
Create a shared pointer to a default text delegate.
Definition text_field_delegate.hpp:205
@ partial
A widget is partially enabled.
Definition widget_state.hpp:73
@ invisible
The widget is invisible.
Definition widget_state.hpp:41
@ display
The widget is in display-only mode.
Definition widget_state.hpp:55
The HikoGUI namespace.
Definition array_generic.hpp:21
The HikoGUI API version 1.
Definition array_generic.hpp:22
@ color
A color value was modified.
Definition style_modify_mask.hpp:27
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition misc.hpp:53
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget_intf.hpp:241
A observer pointing to the whole or part of a observed_base.
Definition observer_intf.hpp:32
callback< void(value_type)> subscribe(Func &&func, callback_flags flags=callback_flags::synchronous) noexcept
Subscribe a callback to this observer.
Definition observer_intf.hpp:458
A delegate that controls the state of a text_field_widget.
Definition text_field_delegate.hpp:32
A single line text field.
Definition text_field_widget.hpp:66
observer< alignment > alignment
The alignment of the text.
Definition text_field_widget.hpp:80
observer< bool > continues
Continues update mode.
Definition text_field_widget.hpp:76
text_field_widget(Value &&value, Attributes &&...attributes) noexcept
Construct a text field widget.
Definition text_field_widget.hpp:137
widget() noexcept
Constructor for creating sub views.
Definition widget.hpp:50
box_constraints update_constraints() noexcept override
Update the constraints of the widget.
Definition widget.hpp:110
bool process_event(gui_event const &event) const noexcept override
Send a event to the window.
Definition widget.hpp:125
bool handle_event(gui_event const &event) noexcept override
Handle command.
Definition widget.hpp:145
Definition text_field_widget.hpp:29
Definition text_widget.hpp:34
T forward(T... args)
T move(T... args)