48 constexpr static auto prefix = Name /
"selection";
76 observer<alignment>
alignment = hi::alignment::middle_flush();
81 delegate->deinit(*
this);
94 _current_label_widget = std::make_unique<label_widget<prefix>>(
this,
alignment);
98 _overlay_widget = std::make_unique<overlay_widget<prefix>>(
this);
103 _off_label_cbt = this->off_label.subscribe([&](
auto...) {
104 ++global_counter<
"selection_widget:off_label:constrain">;
108 _delegate_cbt = this->delegate->subscribe([&] {
109 _notification_from_delegate =
true;
110 ++global_counter<
"selection_widget:delegate:constrain">;
114 this->delegate->init(*
this);
148 different_from<std::shared_ptr<delegate_type>> Value,
149 forward_of<observer<std::vector<std::pair<observer_decay_t<Value>, label>>>> OptionList,
176 different_from<std::shared_ptr<delegate_type>> Value,
177 forward_of<observer<std::vector<std::pair<observer_decay_t<Value>, label>>>> OptionList,
178 forward_of<observer<observer_decay_t<Value>>> OffValue,
179 selection_widget_attribute... Attributes>
183 OptionList&& option_list,
184 OffValue&& off_value,
185 Attributes&&...attributes)
noexcept
196 [[nodiscard]] generator<widget const&> children(
bool include_invisible)
const noexcept override
198 co_yield *_overlay_widget;
199 co_yield *_current_label_widget;
200 co_yield *_off_label_widget;
203 [[nodiscard]] box_constraints update_constraints() noexcept
override
209 if (_notification_from_delegate.
exchange(
false)) {
210 repopulate_options();
213 _off_label_constraints = _off_label_widget->update_constraints();
214 _current_label_constraints = _current_label_widget->update_constraints();
215 _overlay_constraints = _overlay_widget->update_constraints();
219 auto r =
max(_off_label_constraints, _current_label_constraints);
220 r.minimum.width() += theme<prefix>.width(
this) + r.margins.left() + r.margins.right();
221 r.minimum.height() += r.margins.bottom() + r.margins.top();
222 r.preferred.width() += theme<prefix>.width(
this) + r.margins.left() + r.margins.right();
223 r.preferred.height() += r.margins.bottom() + r.margins.top();
224 r.maximum.width() += theme<prefix>.width(
this) + r.margins.left() + r.margins.right();
225 r.maximum.height() += r.margins.bottom() + r.margins.top();
229 _scroll_widget->minimum.copy()->height() = 10;
232 inplace_max(r.minimum.width(), _overlay_constraints.minimum.
width() + theme<prefix>.width(
this));
233 inplace_max(r.preferred.width(), _overlay_constraints.preferred.
width() + theme<prefix>.width(
this));
234 inplace_max(r.maximum.width(), _overlay_constraints.maximum.
width() + theme<prefix>.width(
this));
236 r.alignment = resolve(*
alignment, os_settings::left_to_right());
237 r.margins = theme<prefix>.margin(
this);
242 void set_layout(widget_layout
const& context)
noexcept override
244 hilet label_margins =
max(_off_label_constraints.margins, _current_label_constraints.margins);
245 hilet chevron_box_width = theme<prefix>.width(
this);
246 hilet cap_height = theme<prefix>.cap_height(
this);
249 if (os_settings::left_to_right()) {
250 _chevron_box_rectangle = aarectangle{0.0f, 0.0f, chevron_box_width, context.height()};
253 hilet option_rectangle = aarectangle{
254 chevron_box_width + label_margins.left(),
255 label_margins.bottom(),
256 context.width() - chevron_box_width - label_margins.left() - label_margins.right(),
257 context.height() - label_margins.bottom() - label_margins.top()};
259 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, cap_height};
260 _current_label_shape = box_shape{_current_label_constraints, option_rectangle, cap_height};
263 _chevron_box_rectangle =
264 aarectangle{context.width() - chevron_box_width, 0.0f, chevron_box_width, context.height()};
267 hilet option_rectangle = aarectangle{
268 label_margins.left(),
269 label_margins.bottom(),
270 context.width() - _chevron_box_rectangle.width() - label_margins.left() - label_margins.right(),
271 context.height() - label_margins.bottom() - label_margins.top()};
273 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, cap_height};
274 _current_label_shape = box_shape{_current_label_constraints, option_rectangle, cap_height};
277 _chevron_glyph =
find_glyph(elusive_icon::ChevronUp);
278 hilet chevron_glyph_bbox =_chevron_glyph.get_bounding_rectangle() * theme<prefix>.line_height(
this);
279 _chevron_rectangle =
align(_chevron_box_rectangle, chevron_glyph_bbox, alignment::middle_center());
286 hilet overlay_width = std::clamp(
287 context.width() - chevron_box_width, _overlay_constraints.minimum.
width(), _overlay_constraints.maximum.
width());
288 hilet overlay_height = _overlay_constraints.preferred.
height();
289 hilet overlay_x = os_settings::left_to_right() ? chevron_box_width : context.width() - chevron_box_width - overlay_width;
290 hilet overlay_y = (context.height() - overlay_height) / 2;
291 hilet overlay_rectangle_request = aarectangle{overlay_x, overlay_y, overlay_width, overlay_height};
292 hilet overlay_rectangle = make_overlay_rectangle(overlay_rectangle_request);
293 _overlay_shape = box_shape{_overlay_constraints, overlay_rectangle, cap_height};
294 _overlay_widget->set_layout(context.transform(_overlay_shape, 20.0f));
296 _off_label_widget->set_layout(context.transform(_off_label_shape));
297 _current_label_widget->set_layout(context.transform(_current_label_shape));
300 void draw(widget_draw_context& context)
noexcept override
303 if (overlaps(context, layout)) {
304 draw_outline(context);
305 draw_chevron_box(context);
306 draw_chevron(context);
308 _off_label_widget->draw(context);
309 _current_label_widget->draw(context);
313 _overlay_widget->draw(context);
317 bool handle_event(gui_event
const& event)
noexcept override
319 switch (event.type()) {
320 case gui_event_type::mouse_up:
322 return handle_event(gui_event_type::gui_activate);
326 case gui_event_type::gui_activate_next:
329 case gui_event_type::gui_activate:
335 ++global_counter<
"selection_widget:gui_activate:relayout">;
339 case gui_event_type::gui_cancel:
343 ++global_counter<
"selection_widget:gui_cancel:relayout">;
353 [[nodiscard]] hitbox hitbox_test(point2 position)
const noexcept override
358 auto r = _overlay_widget->hitbox_test_from_parent(position);
360 if (layout.contains(position)) {
361 r =
std::max(r, hitbox{
id, layout.elevation, _has_options ? hitbox_type::button : hitbox_type::_default});
370 [[nodiscard]]
bool accepts_keyboard_focus(keyboard_focus_group group)
const noexcept override
377 notifier<>::callback_token _delegate_cbt;
381 box_constraints _current_label_constraints;
382 box_shape _current_label_shape;
385 box_constraints _off_label_constraints;
386 box_shape _off_label_shape;
388 aarectangle _chevron_box_rectangle;
390 font_book::font_glyph_type _chevron_glyph;
391 aarectangle _chevron_rectangle;
393 bool _selecting =
false;
394 bool _has_options =
false;
397 box_constraints _overlay_constraints;
398 box_shape _overlay_shape;
400 vertical_scroll_widget<prefix> *_scroll_widget =
nullptr;
401 column_widget<prefix> *_column_widget =
nullptr;
403 decltype(
off_label)::callback_token _off_label_cbt;
407 void set_attributes() noexcept {}
408 void set_attributes(label_widget_attribute
auto&& first, label_widget_attribute
auto&&...rest)
noexcept
410 if constexpr (forward_of<
decltype(first), observer<hi::label>>) {
412 }
else if constexpr (forward_of<
decltype(first), observer<hi::alignment>>) {
421 [[nodiscard]] radio_menu_button_widget<prefix>
const *get_first_menu_button() const noexcept
425 if (ssize(_menu_button_widgets) != 0) {
426 return _menu_button_widgets.
front();
432 [[nodiscard]] radio_menu_button_widget<prefix>
const *get_selected_menu_button() const noexcept
436 for (
hilet& button : _menu_button_widgets) {
444 void start_selecting() noexcept
450 if (
auto selected_menu_button = get_selected_menu_button()) {
451 process_event(gui_event::window_set_keyboard_target(selected_menu_button->id, keyboard_focus_group::menu));
453 }
else if (
auto first_menu_button = get_first_menu_button()) {
454 process_event(gui_event::window_set_keyboard_target(first_menu_button->id, keyboard_focus_group::menu));
460 void stop_selecting() noexcept
470 void repopulate_options() noexcept
475 _column_widget->clear();
476 _menu_button_widgets.
clear();
477 _menu_button_tokens.
clear();
479 auto [options, selected] = delegate->options_and_selected(*
this);
481 _has_options = size(options) > 0;
484 auto show_icon =
false;
485 for (
hilet& label : options) {
486 show_icon |= to_bool(label.icon);
489 decltype(selected) index = 0;
490 for (
hilet& label : options) {
491 auto menu_button = &_column_widget->make_widget<radio_menu_button_widget<prefix>>(selected, index, label,
alignment);
493 _menu_button_tokens.
push_back(menu_button->subscribe(
495 hi_assert_not_null(delegate);
496 delegate->set_selected(*this, index);
499 callback_flags::main));
501 _menu_button_widgets.
push_back(menu_button);
506 if (selected == -1) {
512 _current_label_widget->label = options[selected];
517 void draw_outline(widget_draw_context& context)
noexcept
522 theme<prefix>.background_color(
this),
523 theme<prefix>.border_color(
this),
524 theme<prefix>.border_width(
this),
526 theme<prefix>.border_radius(
this));
529 void draw_chevron_box(widget_draw_context& context)
noexcept
531 auto border_radius = theme<prefix>.border_radius(
this);
533 if (os_settings::left_to_right()) {
534 border_radius.right_bottom() = 0;
535 border_radius.right_top() = 0;
537 border_radius.left_bottom() = 0;
538 border_radius.left_top() = 0;
543 translate_z(0.1f) * narrow_cast<aarectangle>(_chevron_box_rectangle),
544 theme<prefix>.border_color(
this),
548 void draw_chevron(widget_draw_context& context)
noexcept
552 translate_z(0.2f) * narrow_cast<aarectangle>(_chevron_rectangle),
553 *_chevron_glyph.font,
554 _chevron_glyph.glyph,
555 theme<prefix>.fill_color(
this));