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 = aarectanglei{0, 0, chevron_box_width, context.height()};
253 hilet option_rectangle = aarectanglei{
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 aarectanglei{context.width() - chevron_box_width, 0, chevron_box_width, context.height()};
267 hilet option_rectangle = aarectanglei{
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 =
279 narrow_cast<aarectanglei>(_chevron_glyph.get_bounding_rectangle() * theme<prefix>.line_height(
this));
280 _chevron_rectangle =
align(_chevron_box_rectangle, chevron_glyph_bbox, alignment::middle_center());
287 hilet overlay_width = std::clamp(
288 context.width() - chevron_box_width, _overlay_constraints.minimum.
width(), _overlay_constraints.maximum.
width());
289 hilet overlay_height = _overlay_constraints.preferred.
height();
290 hilet overlay_x = os_settings::left_to_right() ? chevron_box_width : context.width() - chevron_box_width - overlay_width;
291 hilet overlay_y = (context.height() - overlay_height) / 2;
292 hilet overlay_rectangle_request = aarectanglei{overlay_x, overlay_y, overlay_width, overlay_height};
293 hilet overlay_rectangle = make_overlay_rectangle(overlay_rectangle_request);
294 _overlay_shape = box_shape{_overlay_constraints, overlay_rectangle, cap_height};
295 _overlay_widget->set_layout(context.transform(_overlay_shape, 20.0f));
297 _off_label_widget->set_layout(context.transform(_off_label_shape));
298 _current_label_widget->set_layout(context.transform(_current_label_shape));
301 void draw(widget_draw_context& context)
noexcept override
304 if (overlaps(context, layout)) {
305 draw_outline(context);
306 draw_chevron_box(context);
307 draw_chevron(context);
309 _off_label_widget->draw(context);
310 _current_label_widget->draw(context);
314 _overlay_widget->draw(context);
318 bool handle_event(gui_event
const& event)
noexcept override
320 switch (event.type()) {
321 case gui_event_type::mouse_up:
323 return handle_event(gui_event_type::gui_activate);
327 case gui_event_type::gui_activate_next:
330 case gui_event_type::gui_activate:
336 ++global_counter<
"selection_widget:gui_activate:relayout">;
340 case gui_event_type::gui_cancel:
344 ++global_counter<
"selection_widget:gui_cancel:relayout">;
354 [[nodiscard]] hitbox hitbox_test(point2i position)
const noexcept override
359 auto r = _overlay_widget->hitbox_test_from_parent(position);
362 r =
std::max(r, hitbox{
id, layout.
elevation, _has_options ? hitbox_type::button : hitbox_type::_default});
371 [[nodiscard]]
bool accepts_keyboard_focus(keyboard_focus_group group)
const noexcept override
378 notifier<>::callback_token _delegate_cbt;
382 box_constraints _current_label_constraints;
383 box_shape _current_label_shape;
386 box_constraints _off_label_constraints;
387 box_shape _off_label_shape;
389 aarectanglei _chevron_box_rectangle;
391 font_book::font_glyph_type _chevron_glyph;
392 aarectanglei _chevron_rectangle;
394 bool _selecting =
false;
395 bool _has_options =
false;
398 box_constraints _overlay_constraints;
399 box_shape _overlay_shape;
401 vertical_scroll_widget<prefix> *_scroll_widget =
nullptr;
402 column_widget<prefix> *_column_widget =
nullptr;
404 decltype(
off_label)::callback_token _off_label_cbt;
408 void set_attributes() noexcept {}
409 void set_attributes(label_widget_attribute
auto&& first, label_widget_attribute
auto&&...rest)
noexcept
411 if constexpr (forward_of<
decltype(first), observer<hi::label>>) {
413 }
else if constexpr (forward_of<
decltype(first), observer<hi::alignment>>) {
422 [[nodiscard]] menu_button_widget<prefix>
const *get_first_menu_button() const noexcept
426 if (ssize(_menu_button_widgets) != 0) {
427 return _menu_button_widgets.
front();
433 [[nodiscard]] menu_button_widget<prefix>
const *get_selected_menu_button() const noexcept
437 for (
hilet& button : _menu_button_widgets) {
445 void start_selecting() noexcept
451 if (
auto selected_menu_button = get_selected_menu_button()) {
452 process_event(gui_event::window_set_keyboard_target(selected_menu_button->id, keyboard_focus_group::menu));
454 }
else if (
auto first_menu_button = get_first_menu_button()) {
455 process_event(gui_event::window_set_keyboard_target(first_menu_button->id, keyboard_focus_group::menu));
461 void stop_selecting() noexcept
471 void repopulate_options() noexcept
476 _column_widget->clear();
477 _menu_button_widgets.
clear();
478 _menu_button_tokens.
clear();
480 auto [options, selected] = delegate->options_and_selected(*
this);
482 _has_options = size(options) > 0;
485 auto show_icon =
false;
486 for (
hilet& label : options) {
487 show_icon |= to_bool(label.icon);
490 decltype(selected) index = 0;
491 for (
hilet& label : options) {
492 auto menu_button = &_column_widget->make_widget<menu_button_widget<prefix>>(selected, index, label,
alignment);
494 _menu_button_tokens.
push_back(menu_button->subscribe(
496 hi_assert_not_null(delegate);
497 delegate->set_selected(*this, index);
500 callback_flags::main));
502 _menu_button_widgets.
push_back(menu_button);
507 if (selected == -1) {
513 _current_label_widget->label = options[selected];
518 void draw_outline(widget_draw_context& context)
noexcept
523 theme<prefix>.background_color(
this),
524 theme<prefix>.border_color(
this),
525 theme<prefix>.border_width(
this),
527 theme<prefix>.border_radius(
this));
530 void draw_chevron_box(widget_draw_context& context)
noexcept
532 auto border_radius = theme<prefix>.border_radius(
this);
534 if (os_settings::left_to_right()) {
535 border_radius.right_bottom() = 0;
536 border_radius.right_top() = 0;
538 border_radius.left_bottom() = 0;
539 border_radius.left_top() = 0;
544 translate_z(0.1f) * narrow_cast<aarectangle>(_chevron_box_rectangle),
545 theme<prefix>.border_color(
this),
549 void draw_chevron(widget_draw_context& context)
noexcept
553 translate_z(0.2f) * narrow_cast<aarectangle>(_chevron_rectangle),
554 *_chevron_glyph.font,
555 _chevron_glyph.glyph,
556 theme<prefix>.fill_color(
this));