Skip to content

Commit

Permalink
split combo box into individual widgets to allow separate tooltips
Browse files Browse the repository at this point in the history
  • Loading branch information
malytomas committed Nov 4, 2023
1 parent 3afbf35 commit 3f7ad72
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 82 deletions.
2 changes: 1 addition & 1 deletion sources/include/cage-engine/guiComponents.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ namespace cage
using TooltipCallback = Delegate<void(const GuiTooltipConfig &)>;
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
Expand Down
2 changes: 1 addition & 1 deletion sources/libengine/gui/graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions sources/libengine/gui/skin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -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;
Expand Down
14 changes: 6 additions & 8 deletions sources/libengine/gui/tooltips.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
}
Expand Down Expand Up @@ -149,8 +148,9 @@ namespace cage
tt.tooltip->value<GuiTooltipMarkerComponent>();
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;
}
Expand Down Expand Up @@ -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<GuiTooltipMarkerComponent>();
const Vec2 s = h->requestedSize;
CAGE_ASSERT(findHierarchy(+root, it.tooltip));
const Vec2 s = findHierarchy(+root, it.tooltip)->requestedSize;
it.tooltip->value<GuiExplicitSizeComponent>().size = s + Vec2(1e-5);
it.tooltip->value<GuiParentComponent>().parent = it.rootTooltip->name();
Vec2 &al = it.rootTooltip->value<GuiLayoutAlignmentComponent>().alignment;
Expand Down
173 changes: 103 additions & 70 deletions sources/libengine/gui/widgets/comboBox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ namespace cage
{
namespace
{
void consolidateSelection(HierarchyItem *hierarchy, uint32 selected)
{
EntityComponent *sel = hierarchy->impl->entityMgr->component<GuiSelectedItemComponent>();
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
Expand All @@ -19,40 +32,48 @@ 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)) {}

void initialize() override
{
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<uint32>(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<HierarchyItem> item = hierarchy->impl->memory->createHolder<HierarchyItem>(hierarchy->impl, hierarchy->ent);
item->item = hierarchy->impl->memory->createHolder<ComboListImpl>(+item, this).cast<BaseItem>();
list = class_cast<ComboListImpl *>(+item->item);
list->widgetState = widgetState;
list->skin = skin;
hierarchy->impl->root->children.push_back(std::move(item));
}
}
Expand Down Expand Up @@ -106,108 +127,120 @@ namespace cage
}
return true;
}

void consolidateSelection()
{
EntityComponent *sel = hierarchy->impl->entityMgr->component<GuiSelectedItemComponent>();
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<HierarchyItem> h = hierarchy->impl->memory->createHolder<HierarchyItem>(c->impl, c->ent);
h->item = hierarchy->impl->memory->createHolder<ComboOptionImpl>(+h, combo, idx++).cast<BaseItem>();
auto opt = class_cast<ComboOptionImpl *>(+h->item);
opt->widgetState = widgetState;
opt->skin = skin;
h->text = c->text.share();
const_cast<HierarchyItem *&>(h->text->hierarchy) = +h;
const_cast<Entity *&>(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<GuiWidgetStateComponent>() && c->ent->value<GuiWidgetStateComponent>().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<GuiWidgetStateComponent>() && hierarchy->ent->value<GuiWidgetStateComponent>().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<GuiWidgetStateComponent>() && c->ent->value<GuiWidgetStateComponent>().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;
}
Expand Down

0 comments on commit 3f7ad72

Please sign in to comment.