From 3f7ad72d691e21025c8fee8a0de78a4463a73335 Mon Sep 17 00:00:00 2001 From: Tomas Maly Date: Sat, 4 Nov 2023 01:56:10 +0100 Subject: [PATCH] split combo box into individual widgets to allow separate tooltips --- sources/include/cage-engine/guiComponents.h | 2 +- sources/libengine/gui/graphics.cpp | 2 +- sources/libengine/gui/skin.cpp | 4 +- sources/libengine/gui/tooltips.cpp | 14 +- sources/libengine/gui/widgets/comboBox.cpp | 173 ++++++++++++-------- 5 files changed, 113 insertions(+), 82 deletions(-) diff --git a/sources/include/cage-engine/guiComponents.h b/sources/include/cage-engine/guiComponents.h index b5db8f31..f0b7ad82 100644 --- a/sources/include/cage-engine/guiComponents.h +++ b/sources/include/cage-engine/guiComponents.h @@ -110,7 +110,7 @@ namespace cage using TooltipCallback = Delegate; TooltipCallback tooltip; uint64 delay = 500000; // duration to hold cursor over the widget before showing the tooltip - bool enableForDisabled = false; + bool enableForDisabled = false; // allow showing the tooltip even if the widget is disabled }; enum class LineEdgeModeEnum : uint32 diff --git a/sources/libengine/gui/graphics.cpp b/sources/libengine/gui/graphics.cpp index 5dd0c848..a697aa70 100644 --- a/sources/libengine/gui/graphics.cpp +++ b/sources/libengine/gui/graphics.cpp @@ -88,7 +88,7 @@ namespace cage } data.cursor = item->cursor; data.format.size *= item->hierarchy->impl->pointsScale; - const Vec2i &orr = item->hierarchy->impl->outputResolution; + const Vec2i orr = item->hierarchy->impl->outputResolution; position *= item->hierarchy->impl->pointsScale; data.transform = transpose(Mat4(2.0 / orr[0], 0, 0, 2.0 * position[0] / orr[0] - 1.0, 0, 2.0 / orr[1], 0, 1.0 - 2.0 * position[1] / orr[1], 0, 0, 1, 0, 0, 0, 0, 1)); data.format.wrapWidth = size[0] * item->hierarchy->impl->pointsScale; diff --git a/sources/libengine/gui/skin.cpp b/sources/libengine/gui/skin.cpp index 37e5a897..41228241 100644 --- a/sources/libengine/gui/skin.cpp +++ b/sources/libengine/gui/skin.cpp @@ -234,7 +234,7 @@ namespace cage GuiSkinWidgetDefaults::Input::Input() : textValidFormat(textInit), textInvalidFormat(textInit), placeholderFormat(textInit) { textInvalidFormat.color = Vec3(1, 0, 0); - placeholderFormat.color = Vec3(0.5, 0.5, 0.5); + placeholderFormat.color = Vec3(0.5); } GuiSkinWidgetDefaults::TextArea::TextArea() : textFormat(textInit) {} @@ -251,7 +251,7 @@ namespace cage GuiSkinWidgetDefaults::ComboBox::ComboBox() : placeholderFormat(textInit), itemsFormat(textInit), selectedFormat(textInit) { - placeholderFormat.color = Vec3(0.5, 0.5, 0.5); + placeholderFormat.color = Vec3(0.5); placeholderFormat.align = TextAlignEnum::Center; itemsFormat.align = TextAlignEnum::Center; selectedFormat.align = TextAlignEnum::Center; diff --git a/sources/libengine/gui/tooltips.cpp b/sources/libengine/gui/tooltips.cpp index 6ee78c70..62e6a05e 100644 --- a/sources/libengine/gui/tooltips.cpp +++ b/sources/libengine/gui/tooltips.cpp @@ -31,9 +31,8 @@ namespace cage { for (const auto &it : impl->mouseEventReceivers) { - if (!it.pointInside(impl->outputMouse, GuiEventsTypesFlags::Default | GuiEventsTypesFlags::Tooltips)) - continue; // this widget is not under the cursor - completely ignore and continue searching - return { it.widget, any(it.mask & GuiEventsTypesFlags::Tooltips) }; + if (it.pointInside(impl->outputMouse, GuiEventsTypesFlags::Default | GuiEventsTypesFlags::Tooltips)) + return { it.widget, any(it.mask & GuiEventsTypesFlags::Tooltips) }; } return {}; } @@ -149,8 +148,9 @@ namespace cage tt.tooltip->value(); tt.rootTooltip = tt.tooltip; tt.cursorPosition = outputMouse; - if (const HierarchyItem *h = findHierarchy(+root, ent)) { + CAGE_ASSERT(findHierarchy(+root, ent)); + const HierarchyItem *h = findHierarchy(+root, ent); tt.invokerPosition = h->renderPos; tt.invokerSize = h->renderSize; } @@ -178,15 +178,13 @@ namespace cage TooltipData &it = ttData.back(); if (it.placement == TooltipPlacementEnum::Manual) return; - const HierarchyItem *h = findHierarchy(+root, it.tooltip); - if (!h) - return; // update new tooltip position { it.rootTooltip = entityMgr->createUnique(); it.rootTooltip->value(); - const Vec2 s = h->requestedSize; + CAGE_ASSERT(findHierarchy(+root, it.tooltip)); + const Vec2 s = findHierarchy(+root, it.tooltip)->requestedSize; it.tooltip->value().size = s + Vec2(1e-5); it.tooltip->value().parent = it.rootTooltip->name(); Vec2 &al = it.rootTooltip->value().alignment; diff --git a/sources/libengine/gui/widgets/comboBox.cpp b/sources/libengine/gui/widgets/comboBox.cpp index 8d7104c3..81504155 100644 --- a/sources/libengine/gui/widgets/comboBox.cpp +++ b/sources/libengine/gui/widgets/comboBox.cpp @@ -4,6 +4,19 @@ namespace cage { namespace { + void consolidateSelection(HierarchyItem *hierarchy, uint32 selected) + { + EntityComponent *sel = hierarchy->impl->entityMgr->component(); + uint32 idx = 0; + for (const auto &c : hierarchy->children) + { + if (selected == idx++) + c->ent->add(sel); + else + c->ent->remove(sel); + } + } + struct ComboBoxImpl; struct ComboListImpl : public WidgetItem @@ -19,12 +32,25 @@ namespace cage bool mousePress(MouseButtonsFlags buttons, ModifiersFlags modifiers, Vec2 point) override; }; + struct ComboOptionImpl : public WidgetItem + { + ComboBoxImpl *combo = nullptr; + uint32 index = m; + + ComboOptionImpl(HierarchyItem *hierarchy, ComboBoxImpl *combo, uint32 index) : WidgetItem(hierarchy), combo(combo), index(index) {} + + void initialize() override; + void findRequestedSize() override; + void emit() override; + bool mousePress(MouseButtonsFlags buttons, ModifiersFlags modifiers, Vec2 point) override; + }; + struct ComboBoxImpl : public WidgetItem { GuiComboBoxComponent &data; ComboListImpl *list = nullptr; uint32 count = 0; - uint32 selected = m; + uint32 selected = m; // must keep copy of data.selected here because data may not be access while emitting ComboBoxImpl(HierarchyItem *hierarchy) : WidgetItem(hierarchy), data(GUI_REF_COMPONENT(ComboBox)) {} @@ -32,27 +58,22 @@ namespace cage { CAGE_ASSERT(!hierarchy->image); CAGE_ASSERT(areChildrenValid()); - if (hierarchy->text) - hierarchy->text->apply(skin->defaults.comboBox.placeholderFormat); - count = 0; - for (const auto &c : hierarchy->children) - { - if (count == data.selected) - c->text->apply(skin->defaults.comboBox.selectedFormat); - else - c->text->apply(skin->defaults.comboBox.itemsFormat); - count++; - } + count = numeric_cast(hierarchy->children.size()); if (data.selected >= count) data.selected = m; selected = data.selected; - consolidateSelection(); + consolidateSelection(hierarchy, selected); + if (hierarchy->text) + hierarchy->text->apply(skin->defaults.comboBox.placeholderFormat); + for (const auto &c : hierarchy->children) + c->text->apply(skin->defaults.comboBox.selectedFormat); if (hasFocus()) { Holder item = hierarchy->impl->memory->createHolder(hierarchy->impl, hierarchy->ent); item->item = hierarchy->impl->memory->createHolder(+item, this).cast(); list = class_cast(+item->item); list->widgetState = widgetState; + list->skin = skin; hierarchy->impl->root->children.push_back(std::move(item)); } } @@ -106,108 +127,120 @@ namespace cage } return true; } - - void consolidateSelection() - { - EntityComponent *sel = hierarchy->impl->entityMgr->component(); - uint32 idx = 0; - for (const auto &c : hierarchy->children) - { - if (selected == idx++) - c->ent->add(sel); - else - c->ent->remove(sel); - } - } }; + // list + void ComboListImpl::initialize() { - skin = combo->skin; + uint32 idx = 0; + for (const auto &c : combo->hierarchy->children) + { + Holder h = hierarchy->impl->memory->createHolder(c->impl, c->ent); + h->item = hierarchy->impl->memory->createHolder(+h, combo, idx++).cast(); + auto opt = class_cast(+h->item); + opt->widgetState = widgetState; + opt->skin = skin; + h->text = c->text.share(); + const_cast(h->text->hierarchy) = +h; + const_cast(c->ent) = nullptr; + hierarchy->children.push_back(std::move(h)); + } } void ComboListImpl::findRequestedSize() { hierarchy->requestedSize = Vec2(); offsetSize(hierarchy->requestedSize, skin->layouts[(uint32)GuiElementTypeEnum::ComboBoxList].border + skin->defaults.comboBox.listPadding); - const Vec4 os = skin->layouts[(uint32)GuiElementTypeEnum::ComboBoxItemUnchecked].border + skin->defaults.comboBox.itemPadding; - for (const auto &c : combo->hierarchy->children) + const Vec4 itemFrame = skin->layouts[(uint32)GuiElementTypeEnum::ComboBoxItemUnchecked].border + skin->defaults.comboBox.itemPadding; + for (const auto &c : hierarchy->children) { - // todo limit text wrap width to the combo box item - c->requestedSize = c->text->findRequestedSize(); - offsetSize(c->requestedSize, os); + c->findRequestedSize(); hierarchy->requestedSize[1] += c->requestedSize[1]; } hierarchy->requestedSize[1] += skin->defaults.comboBox.itemSpacing * (max(combo->count, 1u) - 1); const Vec4 margin = skin->defaults.comboBox.baseMargin; hierarchy->requestedSize[0] = combo->hierarchy->requestedSize[0] - margin[0] - margin[2]; + CAGE_ASSERT(hierarchy->requestedSize.valid()); } void ComboListImpl::findFinalPosition(const FinalPosition &update) { const Vec4 margin = skin->defaults.comboBox.baseMargin; - const Real spacing = skin->defaults.comboBox.itemSpacing; hierarchy->renderSize = hierarchy->requestedSize; hierarchy->renderPos = combo->hierarchy->renderPos; hierarchy->renderPos[0] += margin[0]; hierarchy->renderPos[1] += combo->hierarchy->renderSize[1] + skin->defaults.comboBox.listOffset - margin[3]; + CAGE_ASSERT(hierarchy->renderSize.valid()); + CAGE_ASSERT(hierarchy->renderPos.valid()); + + const Real spacing = skin->defaults.comboBox.itemSpacing; Vec2 p = hierarchy->renderPos; Vec2 s = hierarchy->renderSize; offset(p, s, -skin->defaults.comboBox.baseMargin * Vec4(1, 0, 1, 0) - skin->layouts[(uint32)GuiElementTypeEnum::ComboBoxList].border - skin->defaults.comboBox.listPadding); - for (const auto &c : combo->hierarchy->children) + for (const auto &c : hierarchy->children) { - c->renderPos = p; - c->renderSize = Vec2(s[0], c->requestedSize[1]); - p[1] += c->renderSize[1] + spacing; + FinalPosition u(update); + u.renderPos = p; + u.renderSize = Vec2(s[0], c->requestedSize[1]); + c->findFinalPosition(u); + p[1] += c->requestedSize[1] + spacing; } } void ComboListImpl::emit() { emitElement(GuiElementTypeEnum::ComboBoxList, ElementModeEnum::Default, hierarchy->renderPos, hierarchy->renderSize); - const Vec4 itemFrame = -skin->layouts[(uint32)GuiElementTypeEnum::ComboBoxItemUnchecked].border - skin->defaults.comboBox.itemPadding; - uint32 idx = 0; - bool allowHover = true; - for (const auto &c : combo->hierarchy->children) - { - Vec2 p = c->renderPos; - Vec2 s = c->renderSize; - const ElementModeEnum md = allowHover ? mode(p, s, 0) : ElementModeEnum::Default; - allowHover &= md == ElementModeEnum::Default; // items may have small overlap, this will ensure that only one item has hover - const bool disabled = c->ent->has() && c->ent->value().disabled; - emitElement(idx == combo->selected ? GuiElementTypeEnum::ComboBoxItemChecked : GuiElementTypeEnum::ComboBoxItemUnchecked, disabled ? ElementModeEnum::Disabled : md, p, s); - offset(p, s, itemFrame); - c->text->emit(p, s, disabled).setClip(hierarchy); - idx++; - } + hierarchy->childrenEmit(); } bool ComboListImpl::mousePress(MouseButtonsFlags buttons, ModifiersFlags modifiers, Vec2 point) { - makeFocused(); + // does not take focus + return true; + } + + // option + + void ComboOptionImpl::initialize() + { + if (hierarchy->ent->has() && hierarchy->ent->value().disabled) + widgetState.disabled = true; + hierarchy->text->apply(index == combo->selected ? skin->defaults.comboBox.selectedFormat : skin->defaults.comboBox.itemsFormat); + } + + void ComboOptionImpl::findRequestedSize() + { + hierarchy->requestedSize = hierarchy->text->findRequestedSize(); + const Vec4 itemFrame = skin->layouts[(uint32)GuiElementTypeEnum::ComboBoxItemUnchecked].border + skin->defaults.comboBox.itemPadding; + offsetSize(hierarchy->requestedSize, itemFrame); + } + + void ComboOptionImpl::emit() + { + Vec2 p = hierarchy->renderPos; + Vec2 s = hierarchy->renderSize; + emitElement(index == combo->selected ? GuiElementTypeEnum::ComboBoxItemChecked : GuiElementTypeEnum::ComboBoxItemUnchecked, mode(), p, s); + const Vec4 itemFrame = -skin->layouts[(uint32)GuiElementTypeEnum::ComboBoxItemUnchecked].border - skin->defaults.comboBox.itemPadding; + offset(p, s, itemFrame); + hierarchy->text->emit(p, s, widgetState.disabled); + } + + bool ComboOptionImpl::mousePress(MouseButtonsFlags buttons, ModifiersFlags modifiers, Vec2 point) + { + // does not take focus if (buttons != MouseButtonsFlags::Left) return true; if (modifiers != ModifiersFlags::None) return true; - uint32 idx = 0; - HierarchyItem *newlySelected = nullptr; - for (const auto &c : combo->hierarchy->children) - { - const bool disabled = c->ent->has() && c->ent->value().disabled; - if (!disabled && pointInside(c->renderPos, c->renderSize, point)) - { - combo->data.selected = idx; - newlySelected = +c; - hierarchy->impl->focusName = 0; // give up focus (this will close the popup) - break; - } - idx++; - } - combo->consolidateSelection(); - if (newlySelected) + if (!widgetState.disabled && pointInside(hierarchy->renderPos, hierarchy->renderSize, point)) { - newlySelected->fireWidgetEvent(); + combo->data.selected = index; + combo->selected = index; + consolidateSelection(combo->list->hierarchy, combo->selected); + hierarchy->impl->focusName = 0; // give up focus (this will close the popup) hierarchy->fireWidgetEvent(); + combo->hierarchy->fireWidgetEvent(); } return true; }