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
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<semantic_text_style> text_style = semantic_text_style::label;
81
84 observer<alignment> alignment = alignment::middle_flush();
85
86 virtual ~text_field_widget()
87 {
88 hi_assert_not_null(delegate);
89 delegate->deinit(*this);
90 }
91
93 super(parent), delegate(std::move(delegate)), _text()
94 {
95 hi_assert_not_null(this->delegate);
96 _delegate_cbt = this->delegate->subscribe([&] {
97 ++global_counter<"text_field_widget:delegate:layout">;
99 });
100 this->delegate->init(*this);
101
102 _scroll_widget = std::make_unique<scroll_widget<axis::none>>(this);
103 _text_widget = &_scroll_widget->emplace<text_widget>(_text, alignment, text_style);
104 _text_widget->set_mode(widget_mode::partial);
105
106 _error_label_widget =
107 std::make_unique<label_widget>(this, _error_label, alignment::top_left(), semantic_text_style::error);
108
109 _continues_cbt = continues.subscribe([&](auto...) {
110 ++global_counter<"text_field_widget:continues:constrain">;
112 });
113 _text_style_cbt = text_style.subscribe([&](auto...) {
114 ++global_counter<"text_field_widget:text_style:constrain">;
116 });
117 _text_cbt = _text.subscribe([&](auto...) {
118 ++global_counter<"text_field_widget:text:constrain">;
120 });
121 _error_label_cbt = _error_label.subscribe([&](auto const& new_value) {
122 ++global_counter<"text_field_widget:error_label:constrain">;
124 });
125 }
126
127 template<text_field_widget_attribute... Attributes>
128 text_field_widget(
129 widget_intf const* parent,
131 Attributes&&...attributes) noexcept :
132 text_field_widget(parent, std::move(delegate))
133 {
134 set_attributes(std::forward<Attributes>(attributes)...);
135 }
136
143 template<incompatible_with<std::shared_ptr<delegate_type>> Value, text_field_widget_attribute... Attributes>
145 widget_intf const* parent,
146 Value&& value,
147 Attributes&&...attributes) noexcept requires requires
148 {
149 make_default_text_field_delegate(std::forward<Value>(value));
150 } : text_field_widget(parent, make_default_text_field_delegate(std::forward<Value>(value)), std::forward<Attributes>(attributes)...) {}
151
153 [[nodiscard]] generator<widget_intf&> children(bool include_invisible) noexcept override
154 {
155 co_yield *_scroll_widget;
156 }
157
158 [[nodiscard]] box_constraints update_constraints() noexcept override
159 {
160 hi_assert_not_null(delegate);
161 hi_assert_not_null(_error_label_widget);
162 hi_assert_not_null(_scroll_widget);
163
164 if (_text_widget->focus()) {
165 // Update the optional error value from the string conversion when the text-widget has keyboard focus.
166 _error_label = delegate->validate(*this, *_text);
167
168 } else {
169 // When field is not focused, simply follow the observed_value.
170 revert(false);
171 }
172
173 _layout = {};
174 _scroll_constraints = _scroll_widget->update_constraints();
175
176 auto const scroll_width = 100;
177 auto const box_size = extent2{
178 _scroll_constraints.margins.left() + scroll_width + _scroll_constraints.margins.right(),
179 _scroll_constraints.margins.top() + _scroll_constraints.preferred.height() + _scroll_constraints.margins.bottom()};
180
181 auto size = box_size;
182 auto margins = theme().margin();
183 if (_error_label->empty()) {
184 _error_label_widget->set_mode(widget_mode::invisible);
185 _error_label_constraints = _error_label_widget->update_constraints();
186
187 } else {
188 _error_label_widget->set_mode(widget_mode::display);
189 _error_label_constraints = _error_label_widget->update_constraints();
190 inplace_max(size.width(), _error_label_constraints.preferred.width());
191 size.height() += _error_label_constraints.margins.top() + _error_label_constraints.preferred.height();
192 inplace_max(margins.left(), _error_label_constraints.margins.left());
193 inplace_max(margins.right(), _error_label_constraints.margins.right());
194 inplace_max(margins.bottom(), _error_label_constraints.margins.bottom());
195 }
196
197 // The alignment of a text-field is not based on the text-widget due to the intermediate scroll widget.
198 auto const resolved_alignment = resolve_mirror(*alignment, os_settings::left_to_right());
199
200 return {size, size, size, resolved_alignment, margins};
201 }
202 void set_layout(widget_layout const& context) noexcept override
203 {
204 if (compare_store(_layout, context)) {
205 auto const scroll_size = extent2{
206 context.width(),
207 _scroll_constraints.margins.top() + _scroll_constraints.preferred.height() +
208 _scroll_constraints.margins.bottom()};
209
210 auto const scroll_rectangle = aarectangle{point2{0, context.height() - scroll_size.height()}, scroll_size};
211 _scroll_shape = box_shape{_scroll_constraints, scroll_rectangle, theme().baseline_adjustment()};
212
213 if (_error_label_widget->mode() > widget_mode::invisible) {
214 auto const error_label_rectangle =
215 aarectangle{0, 0, context.rectangle().width(), _error_label_constraints.preferred.height()};
216 _error_label_shape = box_shape{_error_label_constraints, error_label_rectangle, theme().baseline_adjustment()};
217 }
218 }
219
220 if (_error_label_widget->mode() > widget_mode::invisible) {
221 _error_label_widget->set_layout(context.transform(_error_label_shape));
222 }
223 _scroll_widget->set_layout(context.transform(_scroll_shape));
224 }
225 void draw(draw_context const& context) noexcept override
226 {
227 if (mode() > widget_mode::invisible and overlaps(context, layout())) {
228 draw_background_box(context);
229
230 _scroll_widget->draw(context);
231 _error_label_widget->draw(context);
232 }
233 }
234 bool handle_event(gui_event const& event) noexcept override
235 {
236 switch (event.type()) {
237 case gui_event_type::gui_cancel:
238 if (mode() >= widget_mode::partial) {
239 revert(true);
240 return true;
241 }
242 break;
243
244 case gui_event_type::gui_activate:
245 if (mode() >= widget_mode::partial) {
246 commit(true);
247 return super::handle_event(event);
248 }
249 break;
250
251 default:;
252 }
253
254 return super::handle_event(event);
255 }
256 hitbox hitbox_test(point2 position) const noexcept override
257 {
258 if (mode() >= widget_mode::partial) {
259 auto r = hitbox{};
260 r = _scroll_widget->hitbox_test_from_parent(position, r);
261 r = _error_label_widget->hitbox_test_from_parent(position, r);
262 return r;
263 } else {
264 return hitbox{};
265 }
266 }
267 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
268 {
269 if (mode() >= widget_mode::partial) {
270 return _scroll_widget->accepts_keyboard_focus(group);
271 } else {
272 return false;
273 }
274 }
275 [[nodiscard]] color focus_color() const noexcept override
276 {
277 if (mode() >= widget_mode::partial) {
278 if (not _error_label->empty()) {
279 return theme().text_style(semantic_text_style::error)->color;
280 } else if (_text_widget->focus()) {
281 return theme().color(semantic_color::accent);
282 } else if (phase() == widget_phase::hover) {
283 return theme().color(semantic_color::border, _layout.layer + 1);
284 } else {
285 return theme().color(semantic_color::border, _layout.layer);
286 }
287
288 } else {
289 return theme().color(semantic_color::border, _layout.layer - 1);
290 }
291 }
293private:
297 box_constraints _scroll_constraints;
298 box_shape _scroll_shape;
299
302 text_widget *_text_widget = nullptr;
303
306 observer<gstring> _text;
307
310 observer<label> _error_label;
311 std::unique_ptr<label_widget> _error_label_widget;
312 box_constraints _error_label_constraints;
313 box_shape _error_label_shape;
314
315 callback<void()> _delegate_cbt;
316 callback<void(bool)> _continues_cbt;
317 callback<void(semantic_text_style)> _text_style_cbt;
318 callback<void(gstring)> _text_cbt;
319 callback<void(label)> _error_label_cbt;
320
321 void set_attributes() noexcept {}
322
323 template<text_field_widget_attribute First, text_field_widget_attribute... Rest>
324 void set_attributes(First&& first, Rest&&...rest) noexcept
325 {
326 if constexpr (forward_of<First, observer<hi::alignment>>) {
327 alignment = std::forward<First>(first);
328 } else if constexpr (forward_of<First, observer<hi::semantic_text_style>>) {
329 text_style = std::forward<First>(first);
330 } else {
331 hi_static_no_default();
332 }
333
334 set_attributes(std::forward<Rest>(rest)...);
335 }
336
337 void revert(bool force) noexcept
338 {
339 hi_assert_not_null(delegate);
340 _text = delegate->text(*this);
341 _error_label = label{};
342 }
343 void commit(bool force) noexcept
344 {
345 hi_axiom(loop::main().on_thread());
346 hi_assert_not_null(delegate);
347
348 if (*continues or force) {
349 auto text = *_text;
350
351 if (delegate->validate(*this, text).empty()) {
352 // text is valid.
353 delegate->set_text(*this, text);
354 }
355
356 // After commit get the canonical text to display from the delegate.
357 _text = delegate->text(*this);
358 _error_label = label{};
359 }
360 }
361 void draw_background_box(draw_context const& context) const noexcept
362 {
363 auto const outline = _scroll_shape.rectangle;
364
365 auto const corner_radii = hi::corner_radii(0.0f, 0.0f, theme().rounding_radius<float>(), theme().rounding_radius<float>());
366 context.draw_box(layout(), outline, background_color(), corner_radii);
367
368 auto const line = line_segment(get<0>(outline), get<1>(outline));
369 context.draw_line(layout(), translate3{0.0f, 0.5f, 0.1f} * line, theme().border_width(), focus_color());
370 }
371};
372
373}} // namespace hi::v1
Defines scroll_widget.
Defines delegate_field_delegate and some default text field delegates.
Defines widget.
Defines label_widget.
@ window_relayout
Request that widgets get laid out on the next frame.
@ window_reconstrain
Request that widget get constraint on the next frame.
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.
@ invisible
The widget is invisible.
@ display
The widget is in display-only mode.
The HikoGUI namespace.
Definition array_generic.hpp:20
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition misc.hpp:53
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
Horizontal/Vertical alignment combination.
Definition alignment.hpp:244
The 4 radii of the corners of a quad or rectangle.
Definition corner_radii.hpp:26
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent2.hpp:107
constexpr float & height() noexcept
Access the y-as-height element from the extent.
Definition extent2.hpp:118
Definition widget_intf.hpp:24
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget_intf.hpp:206
widget_intf * parent
Pointer to the parent widget.
Definition widget_intf.hpp:35
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:456
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:84
observer< bool > continues
Continues update mode.
Definition text_field_widget.hpp:76
text_field_widget(widget_intf const *parent, Value &&value, Attributes &&...attributes) noexcept
Construct a text field widget.
Definition text_field_widget.hpp:144
observer< semantic_text_style > text_style
The style of the text.
Definition text_field_widget.hpp:80
An interactive graphical object as part of the user-interface.
Definition widget.hpp:37
widget() noexcept
Constructor for creating sub views.
Definition widget.hpp:55
bool process_event(gui_event const &event) const noexcept override
Send a event to the window.
Definition widget.hpp:130
bool handle_event(gui_event const &event) noexcept override
Handle command.
Definition widget.hpp:150
Definition text_field_widget.hpp:29
Definition text_widget.hpp:34
T move(T... args)