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 "label_widget.hpp"
13#include "scroll_widget.hpp"
14#include "../label.hpp"
15#include "../GUI/module.hpp"
16#include <memory>
17#include <string>
18#include <array>
19#include <optional>
20#include <future>
21
22namespace hi { inline namespace v1 {
23
24template<typename Context>
26
62template<fixed_string Name = "">
63class text_field_widget final : public widget {
64public:
65 using delegate_type = text_field_delegate;
66 using super = widget;
67 constexpr static auto prefix = Name / "text-field";
68
70
74 observer<bool> continues = false;
75
78 observer<alignment> alignment = alignment::middle_flush();
79
80 virtual ~text_field_widget()
81 {
82 hi_assert_not_null(delegate);
83 delegate->deinit(*this);
84 }
85
87 super(parent), delegate(std::move(delegate)), _text()
88 {
89 hi_assert_not_null(this->delegate);
90 _delegate_cbt = this->delegate->subscribe([&] {
91 ++global_counter<"text_field_widget:delegate:layout">;
92 process_event({gui_event_type::window_relayout});
93 });
94 this->delegate->init(*this);
95
96 _scroll_widget = std::make_unique<scroll_widget<axis::none, prefix>>(this);
97 _text_widget = &_scroll_widget->make_widget<text_widget<prefix>>(_text, alignment);
98 _text_widget->mode = widget_mode::partial;
99
100 // XXX The error-label should use the text_phrasing::error.
101 _error_label_widget = std::make_unique<label_widget<prefix / "error">>(this, _error_label, alignment::top_left());
102
103 _continues_cbt = continues.subscribe([&](auto...) {
104 ++global_counter<"text_field_widget:continues:constrain">;
105 process_event({gui_event_type::window_reconstrain});
106 });
107 _text_cbt = _text.subscribe([&](auto...) {
108 ++global_counter<"text_field_widget:text:constrain">;
109 process_event({gui_event_type::window_reconstrain});
110 });
111 _error_label_cbt = _error_label.subscribe([&](auto const& new_value) {
112 ++global_counter<"text_field_widget:error_label:constrain">;
113 process_event({gui_event_type::window_reconstrain});
114 });
115 }
116
117 text_field_widget(
118 widget *parent,
120 text_field_widget_attribute auto&&...attributes) noexcept :
121 text_field_widget(parent, std::move(delegate))
122 {
123 set_attributes(hi_forward(attributes)...);
124 }
125
133 widget *parent,
134 different_from<std::shared_ptr<delegate_type>> auto&& value,
135 text_field_widget_attribute auto&&...attributes) noexcept
136 requires requires { make_default_text_field_delegate(hi_forward(value)); }
138 {
139 }
140
142 [[nodiscard]] generator<widget const&> children(bool include_invisible) const noexcept override
143 {
144 co_yield *_scroll_widget;
145 }
146
147 [[nodiscard]] box_constraints update_constraints() noexcept override
148 {
149 hi_assert_not_null(delegate);
150 hi_assert_not_null(_error_label_widget);
151 hi_assert_not_null(_scroll_widget);
152
153 if (*_text_widget->focus) {
154 // Update the optional error value from the string conversion when
155 // the text-widget has keyboard focus.
156 if (auto error_label = delegate->validate(*this, *_text)) {
157 _error_label = *error_label;
158 } else {
159 // Error label is used by a widget and can not be an optional,
160 // we use an empty string to denote no-error.
161 _error_label = label{};
162 }
163
164 } else {
165 // When field is not focused, simply follow the observed_value.
166 revert(false);
167 }
168
169 _scroll_constraints = _scroll_widget->update_constraints();
170
171 hilet scroll_width = 100;
172 hilet box_size = extent2i{
173 _scroll_constraints.margins.left() + scroll_width + _scroll_constraints.margins.right(),
174 _scroll_constraints.margins.top() + _scroll_constraints.preferred.height() + _scroll_constraints.margins.bottom()};
175
176 auto size = box_size;
177 auto margins = theme<prefix>.margin(this);
178 if (_error_label->empty()) {
179 _error_label_widget->mode = widget_mode::invisible;
180 _error_label_constraints = _error_label_widget->update_constraints();
181
182 } else {
183 _error_label_widget->mode = widget_mode::display;
184 _error_label_constraints = _error_label_widget->update_constraints();
185 inplace_max(size.width(), _error_label_constraints.preferred.width());
186 size.height() += _error_label_constraints.margins.top() + _error_label_constraints.preferred.height();
187 inplace_max(margins.left(), _error_label_constraints.margins.left());
188 inplace_max(margins.right(), _error_label_constraints.margins.right());
189 inplace_max(margins.bottom(), _error_label_constraints.margins.bottom());
190 }
191
192 // The alignment of a text-field is not based on the text-widget due to the intermediate scroll widget.
193 hilet resolved_alignment = resolve_mirror(*alignment, os_settings::left_to_right());
194
195 return {size, size, size, resolved_alignment, margins};
196 }
197
198 void set_layout(widget_layout const& context) noexcept override
199 {
200 if (compare_store(layout, context)) {
201 hilet scroll_size = extent2i{
202 context.width(),
203 _scroll_constraints.margins.top() + _scroll_constraints.preferred.height() +
204 _scroll_constraints.margins.bottom()};
205
206 hilet scroll_rectangle = aarectanglei{point2i{0, context.height() - scroll_size.height()}, scroll_size};
207 _scroll_shape = box_shape{_scroll_constraints, scroll_rectangle, theme<prefix>.cap_height(this)};
208
209 if (*_error_label_widget->mode > widget_mode::invisible) {
210 hilet error_label_rectangle =
211 aarectanglei{0, 0, context.rectangle().width(), _error_label_constraints.preferred.height()};
212 _error_label_shape = box_shape{_error_label_constraints, error_label_rectangle, theme<prefix>.cap_height(this)};
213 }
214 }
215
216 if (*_error_label_widget->mode > widget_mode::invisible) {
217 _error_label_widget->set_layout(context.transform(_error_label_shape));
218 }
219 _scroll_widget->set_layout(context.transform(_scroll_shape));
220 }
221
222 void draw(widget_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
232 bool handle_event(gui_event const& event) noexcept override
233 {
234 switch (event.type()) {
235 case gui_event_type::gui_cancel:
236 if (*mode >= widget_mode::partial) {
237 revert(true);
238 return true;
239 }
240 break;
241
242 case gui_event_type::gui_activate:
243 if (*mode >= widget_mode::partial) {
244 commit(true);
245 return super::handle_event(event);
246 }
247 break;
248
249 default:;
250 }
251
252 return super::handle_event(event);
253 }
254
255 hitbox hitbox_test(point2i position) const noexcept override
256 {
257 if (*mode >= widget_mode::partial) {
258 auto r = hitbox{};
259 r = _scroll_widget->hitbox_test_from_parent(position, r);
260 r = _error_label_widget->hitbox_test_from_parent(position, r);
261 return r;
262 } else {
263 return hitbox{};
264 }
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 }
276private:
277 notifier<>::callback_token _delegate_cbt;
278
282 box_constraints _scroll_constraints;
283 box_shape _scroll_shape;
284
287 text_widget<prefix> *_text_widget = nullptr;
288
291 observer<hi::text> _text;
292
295 observer<label> _error_label;
296 std::unique_ptr<label_widget<join_path(prefix, "error")>> _error_label_widget;
297 box_constraints _error_label_constraints;
298 box_shape _error_label_shape;
299
300 typename decltype(continues)::callback_token _continues_cbt;
301 typename decltype(_text)::callback_token _text_cbt;
302 typename decltype(_error_label)::callback_token _error_label_cbt;
303
304 void set_attributes() noexcept {}
305 void set_attributes(text_field_widget_attribute auto&& first, text_field_widget_attribute auto&&...rest) noexcept
306 {
307 if constexpr (forward_of<decltype(first), observer<hi::alignment>>) {
308 alignment = hi_forward(first);
309 } else {
311 }
312
313 set_attributes(hi_forward(rest)...);
314 }
315
316 void revert(bool force) noexcept
317 {
318 hi_assert_not_null(delegate);
319 _text = delegate->text(*this);
320 _error_label = label{};
321 }
322
323 void commit(bool force) noexcept
324 {
325 hi_axiom(loop::main().on_thread());
326 hi_assert_not_null(delegate);
327
328 if (*continues or force) {
329 if (not delegate->validate(*this, *_text)) {
330 // text is valid.
331 delegate->set_text(*this, *_text);
332 }
333
334 // After commit get the canonical text to display from the delegate.
335 _text = delegate->text(*this);
336 _error_label = label{};
337 }
338 }
339
340 void draw_background_box(widget_draw_context const& context) const noexcept
341 {
342 hilet outline = narrow_cast<aarectangle>(_scroll_shape.rectangle);
343
344 context.draw_box(layout, outline, theme<prefix>.background_color(this), theme<prefix>.corner_radius(this));
345
346 // A text field has a line under the box, which changes color on error.
347 hilet line = line_segment(get<0>(outline), get<1>(outline));
348
349 hilet outline_color = _error_label->empty() ? theme<prefix>.border_color(this) : color::red();
350 context.draw_line(layout, translate3{0.0f, 0.5f, 0.1f} * line, theme<prefix>.border_width(this), outline_color);
351 }
352};
353
354}} // namespace hi::v1
Functionality for labels, text and icons.
Defines scroll_widget.
Defines delegate_field_delegate and some default text field delegates.
Defines label_widget.
#define hi_static_no_default(...)
This part of the code should not be reachable, unless a programming bug.
Definition assert.hpp:323
#define hi_axiom(expression,...)
Specify an axiom; an expression that is true.
Definition assert.hpp:253
#define hi_assert_not_null(x,...)
Assert if an expression is not nullptr.
Definition assert.hpp:238
#define hilet
Invariant should be the default for variables.
Definition utility.hpp:23
#define hi_forward(x)
Forward a value, based on the decltype of the value.
Definition utility.hpp:29
geo::extent< int, 2 > extent2i
A 2D extent.
Definition extent.hpp:512
@ 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(auto &&value) noexcept
Create a shared pointer to a default text delegate.
Definition text_field_delegate.hpp:196
@ partial
A widget is partially enabled.
@ invisible
The widget is invisible.
@ display
The widget is in display-only mode.
DOXYGEN BUG.
Definition algorithm.hpp:13
constexpr auto join_path(fixed_string< L > const &lhs, fixed_string< R > const &rhs) noexcept
lhs / rhs
Definition fixed_string.hpp:273
geometry/margins.hpp
Definition cache.hpp:11
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition utility.hpp:212
constexpr value_type & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:166
constexpr value_type & height() noexcept
Access the y-as-height element from the extent.
Definition extent.hpp:177
Definition widget.hpp:26
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
Definition widget.hpp:279
widget * parent
Pointer to the parent widget.
Definition widget.hpp:40
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:53
A delegate that controls the state of a text_field_widget.
Definition text_field_delegate.hpp:24
A single line text field.
Definition text_field_widget.hpp:63
observer< alignment > alignment
The alignment of the text.
Definition text_field_widget.hpp:78
observer< bool > continues
Continues update mode.
Definition text_field_widget.hpp:74
text_field_widget(widget *parent, different_from< std::shared_ptr< delegate_type > > auto &&value, text_field_widget_attribute auto &&...attributes) noexcept
Construct a text field widget.
Definition text_field_widget.hpp:132
Definition text_field_widget.hpp:25
Definition text_widget.hpp:30
T move(T... args)