FifeGUI 0.2.0
A C++ GUI library designed for games.
menupopup.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
2// SPDX-FileCopyrightText: 2026 Fifengine contributors
3
4// Corresponding header include
5#include "fifechan/widgets/menupopup.hpp"
6
7// Standard library includes
8#include <algorithm>
9#include <memory>
10#include <unordered_map>
11
12// Project headers
13#include "fifechan/color.hpp"
14#include "fifechan/focushandler.hpp"
15#include "fifechan/font.hpp"
16#include "fifechan/graphics.hpp"
17#include "fifechan/widgets/menubar.hpp"
18#include "fifechan/widgets/menuitem.hpp"
19#include "fifechan/widgets/modalbackdrop.hpp"
20
21namespace fcn
22{
23 // Default gap values for column layout
24 namespace
25 {
26 constexpr int GAP_ICON_CAPTION = 6;
27 constexpr int GAP_CAPTION_SHORTCUT = 16;
28 constexpr int GAP_SHORTCUT_ARROW = 10;
29 } // namespace
30
31 void MenuPopup::layoutItems()
32 {
33 auto children = getChildren();
34 if (children.empty()) {
35 return;
36 }
37
38 // Get font to use for measurements
39 Font const * font = getFont();
40 if (font == nullptr) {
41 return;
42 }
43
44 // === PASS 1: Measure & Aggregate Columns ===
45 MenuColumns cols{};
46
47 // Store per-item metrics so pass 2 can reuse exact heights
48 std::unordered_map<MenuItem*, MenuItemMetrics> metricsMap;
49
50 for (auto* child : children) {
51 if (child == nullptr) {
52 continue;
53 }
54
55 auto* mi = dynamic_cast<MenuItem*>(child);
56 if (mi == nullptr) {
57 continue;
58 }
59
60 if (mi->getType() == MenuItem::Type::Separator) {
61 continue;
62 }
63
64 auto m = mi->measure(*font);
65
66 // store metrics for later (pass 2)
67 metricsMap[mi] = m;
68
69 cols.iconW = std::max(cols.iconW, m.iconW);
70 cols.captionW = std::max(cols.captionW, m.captionW);
71 cols.shortcutW = std::max(cols.shortcutW, m.shortcutW);
72 cols.arrowW = std::max(cols.arrowW, m.arrowW);
73 }
74
75 // Determine actual gaps (collapse if column is empty)
76 int const gapIconCaption = cols.iconW > 0 ? GAP_ICON_CAPTION : 0;
77 int const gapCaptionShortcut = (cols.captionW > 0 && cols.shortcutW > 0) ? GAP_CAPTION_SHORTCUT : 0;
78 int const gapShortcutArrow = (cols.shortcutW > 0 && cols.arrowW > 0) ? GAP_SHORTCUT_ARROW : 0;
79
80 // === Compute Content Width ===
81 int const contentW = cols.iconW + gapIconCaption + cols.captionW + gapCaptionShortcut + cols.shortcutW +
82 gapShortcutArrow + cols.arrowW;
83
84 // === PASS 2: Layout Items ===
85 // Compute X positions for each column
86 int const xIcon = 0;
87 int const xCaption = xIcon + cols.iconW + gapIconCaption;
88 int const xShortcut = xCaption + cols.captionW + gapCaptionShortcut;
89 int const xArrow = xShortcut + cols.shortcutW + gapShortcutArrow;
90
91 // Create column layout struct
92 ColumnLayout layout{};
93 layout.xIcon = xIcon;
94 layout.xCaption = xCaption;
95 layout.xShortcut = xShortcut;
96 layout.xArrow = xArrow;
97 layout.cols = cols;
98
99 // Layout each item — use measured heights when available
100 int contentH = 0;
101 int y = 0;
102
103 for (auto* child : children) {
104 if (child == nullptr) {
105 continue;
106 }
107
108 int h = child->getHeight();
109
110 if (auto* mi = dynamic_cast<MenuItem*>(child)) {
111 auto it = metricsMap.find(mi);
112 if (it != metricsMap.end()) {
113 // prefer measured height (accounts for icon fonts)
114 h = it->second.height;
115 }
116 }
117
118 // Set the child's frame using setPosition and setSize
119 child->setPosition(0, y);
120 child->setSize(contentW, h);
121
122 // Apply column layout to MenuItems
123 if (auto* mi = dynamic_cast<MenuItem*>(child)) {
124 mi->layoutColumns(layout);
125 }
126
127 y += h;
128 contentH += h;
129 }
130
131 // === Compute Final Popup Size ===
132 // Get border and padding
133 int const leftBorder = ((getBorderSides() & Widget::BORDER_LEFT) != 0U) ? static_cast<int>(getBorderSize()) : 0;
134 int const rightBorder =
135 ((getBorderSides() & Widget::BORDER_RIGHT) != 0U) ? static_cast<int>(getBorderSize()) : 0;
136 int const topBorder = ((getBorderSides() & Widget::BORDER_TOP) != 0U) ? static_cast<int>(getBorderSize()) : 0;
137 int const bottomBorder =
138 ((getBorderSides() & Widget::BORDER_BOTTOM) != 0U) ? static_cast<int>(getBorderSize()) : 0;
139
140 int const contentPaddingW = leftBorder + rightBorder + getPaddingLeft() + getPaddingRight();
141 int const contentPaddingH = topBorder + bottomBorder + getPaddingTop() + getPaddingBottom();
142
143 int const popupWidth = contentW + contentPaddingW;
144 int const popupHeight = contentH + contentPaddingH;
145
146 // Apply final size
147 setSize(popupWidth, popupHeight);
148 }
149
151 {
152 // MenuPopup positions children itself via layoutItems(); use AutoSize
153 // so Container::resizeToContent() respects the children's dimensions
154 // rather than reflowing them with the Vertical layout policy.
155 setLayout(LayoutPolicy::AutoSize);
156 setOpaque(true);
157 // Use built-in widget border so children are laid out inside
158 // the border (prevents children from drawing over it).
159 setBorderSize(1);
161
162 // Provide a small horizontal padding so items don't touch the popup
163 // border; increase slightly for better visual spacing.
164 setPaddingLeft(16);
165 setPaddingRight(16);
166
167 // Register for mouse events so the popup can handle clicks
168 // that occur outside the popup (modal dismissal).
169 addMouseListener(this);
170
171 // Initial size: content area 150x10 plus border on both sides
172 setSize(150 + (static_cast<int>(getBorderSize()) * 2), 10 + (static_cast<int>(getBorderSize()) * 2));
173 }
174
175 void MenuPopup::show(int x, int y)
176 {
177 // Find the top-level container and reparent the popup to it
178 // to ensure proper z-ordering (always on top of other widgets).
179 Container* topContainer = nullptr;
180
181 // Try to find top container
182 if (mParentMenuItem != nullptr) {
183 topContainer = dynamic_cast<Container*>(mParentMenuItem->getTop());
184 }
185 if (topContainer == nullptr) {
186 topContainer = dynamic_cast<Container*>(getTop());
187 }
188
189 // If currently in a different parent, remove from that parent first
190 Widget* currentParent = getParent();
191 if (currentParent != nullptr && currentParent != topContainer) {
192 if (auto* parentContainer = dynamic_cast<Container*>(currentParent)) {
193 parentContainer->remove(this);
194 }
195 }
196
197 // Add to top container FIRST before calling moveToTop().
198 // moveToTop() throws if the widget is not already a child of the container.
199 if (topContainer != nullptr && currentParent != topContainer) {
200 topContainer->add(this);
201 }
202
203 // Set position (now relative to topContainer since we just added it)
204 setPosition(x, y);
205
206 // Now move to top for proper z-ordering (widget is guaranteed to be a child)
207 if (topContainer != nullptr) {
208 topContainer->moveToTop(this);
209 }
210
211 // Set visible and ensure on top
212 setVisible(true);
213 mVisible = true;
214
215 // Use RAII ModalScope so the modal is popped automatically when
216 // the popup is hidden or destroyed. Only create for root menus.
217 if (_getFocusHandler() != nullptr && mParentMenu == nullptr) {
218 mModalScope = std::make_unique<FocusHandler::ModalScope>(_getFocusHandler(), this, this);
219
220 // Request focus on the popup for keyboard navigation
221 requestFocus();
222
223 if (topContainer != nullptr) {
224 // Create backdrop, size it to cover top and add it behind menus.
225 ModalBackdrop* backdrop = new ModalBackdrop(this);
226 backdrop->setPosition(0, 0);
227 backdrop->setSize(topContainer->getWidth(), topContainer->getHeight());
228 topContainer->add(backdrop);
229 // Place backdrop above other top-level widgets so it captures
230 // clicks outside the menu, then ensure the popup stays on top.
231 topContainer->moveToTop(backdrop);
232 topContainer->moveToTop(this);
233 mBackdrop = backdrop;
234 }
235 } else if (mParentMenu != nullptr) {
236 // request focus for submenu
237 requestFocus();
238 }
239 }
240
242 {
243 setVisible(false);
244 mVisible = false;
245
246 // Restore focus to parent MenuItem/MenuBar before clearing modal
247 if (mParentMenuItem != nullptr) {
248 mParentMenuItem->requestFocus();
249 }
250
251 // Hide any open child submenu first
252 if (mOpenChild != nullptr) {
253 mOpenChild->hide();
254 mOpenChild = nullptr;
255 }
256
257 // Remove backdrop if we created one for the root popup
258 if (mBackdrop != nullptr) {
259 if (mBackdrop->getParent() != nullptr) {
260 if (auto* parentContainer = dynamic_cast<Container*>(mBackdrop->getParent())) {
261 parentContainer->remove(mBackdrop);
262 }
263 }
264 delete mBackdrop;
265 mBackdrop = nullptr;
266 }
267
268 // Release RAII modal scope for root popup if present
269 if (mModalScope != nullptr) {
270 mModalScope.reset();
271 }
272 }
273
274 // cppcheck-suppress duplInheritedMember
276 {
277 return mVisible;
278 }
279
281 {
282 mParentMenuItem = parent;
283 }
284
286 {
287 return mParentMenuItem;
288 }
289
291 {
292 return mParentMenu;
293 }
294
296 {
297 mParentMenu = parent;
298 }
299
301 {
302 // Add the item to the popup
303 add(item);
304
305 // Let the item size itself according to font/padding
306 try {
307 item->adjustSize();
308 } catch (...) {
309 // ignore if widget doesn't implement adjustSize
310 }
311
312 // Perform two-pass column-based layout
313 layoutItems();
314
315 // Position children according to the container's layout (vertical stacking).
316 // Call resizeToContent with recursion=false so children keep the sizes
317 // assigned by layoutItems() (we don't want each MenuItem to re-measure
318 // itself and overwrite the column-computed widths).
319 resizeToContent(false);
320
321 // Listen for actions from items (so we can close the menu on selection)
322 auto* mi = dynamic_cast<MenuItem*>(item);
323 if (mi != nullptr && mi->getType() != MenuItem::Type::Separator) {
324 mi->addActionListener(this);
325 }
326 }
327
329 {
330 // Create a separator as a special MenuItem
331 auto* sep = new MenuItem("");
332 sep->setType(MenuItem::Type::Separator);
333 sep->setHeight(2);
334 add(sep);
335
336 // Perform two-pass column-based layout
337 layoutItems();
338
339 // Ensure children are laid out after recomputing sizes. Avoid recursive
340 // resize so children don't overwrite sizes set by layoutItems.
341 resizeToContent(false);
342 }
343
344 void MenuPopup::draw(Graphics* graphics)
345 {
346 int const w = getWidth();
347 int const h = getHeight();
348
349 // Draw shadow
350 graphics->setColor(Color(50, 50, 50, 100));
351 graphics->fillRectangle(Rectangle(3, 3, w, h));
352
353 // Draw background - always visible (white)
354 graphics->setColor(Color(255, 255, 255, 255));
355 graphics->fillRectangle(Rectangle(0, 0, w, h));
356
357 // Draw children and built-in borders (Container::draw will call
358 // drawBorder when border size > 0).
359 Container::draw(graphics);
360 }
361
363 {
364 // Handle click on backdrop to close
365 int const relx = event.getX();
366 int const rely = event.getY();
367
368 // Coordinates from MouseEvent are relative to the widget the
369 // listener is registered to (this popup). Convert to absolute
370 // coordinates for hit-testing against top-level children.
371 int absx = relx;
372 int absy = rely;
373 int px = 0;
374 int py = 0;
375 getAbsolutePosition(px, py);
376 absx += px;
377 absy += py;
378
379 if (!contains(relx, rely)) {
380 // If the click landed on a top-level MenuBar/MenuItem, allow the
381 // event to propagate so the MenuBar can toggle the popup. Otherwise
382 // hide the popup and consume the event.
383 bool clickHitsMenuBar = false;
384 Container const * topContainer = dynamic_cast<Container*>(getTop());
385 if (topContainer != nullptr) {
386 for (unsigned i = 0; i < topContainer->getChildrenCount(); ++i) {
387 Widget* child = topContainer->getChild(i);
388 if (child == nullptr || child == this) {
389 continue;
390 }
391 int cx = 0;
392 int cy = 0;
393 child->getAbsolutePosition(cx, cy);
394 if (absx >= cx && absx < cx + child->getWidth() && absy >= cy && absy < cy + child->getHeight()) {
395 if (dynamic_cast<MenuBar*>(child) != nullptr || dynamic_cast<MenuItem*>(child) != nullptr) {
396 clickHitsMenuBar = true;
397 break;
398 }
399 }
400 }
401 }
402
403 if (clickHitsMenuBar) {
404 // Let the MenuBar/MenuItem handle the click (toggle behavior).
405 return;
406 }
407
408 hide();
409 event.consume();
410 }
411 }
412
414 {
415 }
416
418 {
419 // Determine which child was entered and update hover/focus
420 Widget* src = event.getSource();
421 auto children = getChildren();
422 int idx = 0;
423 for (auto* child : children) {
424 if (child == src) {
425 // Focus the hovered item for visual feedback and keyboard nav
426 child->requestFocus();
427 mHoverIndex = idx;
428
429 // If it's a MenuItem with a submenu, open it
430 if (auto* mi = dynamic_cast<MenuItem*>(child)) {
431 MenuPopup* submenu = mi->getSubmenu();
432 if (submenu != nullptr) {
433 // Close previous child if different
434 if (mOpenChild != nullptr && mOpenChild != submenu) {
435 mOpenChild->hide();
436 }
437
438 // Attach submenu to this menu and position to the right
439 submenu->setParentMenu(this);
440 submenu->setParentMenuItem(mi);
441
442 int ax = 0;
443 int ay = 0;
444 mi->getAbsolutePosition(ax, ay);
445 int const sx = ax + mi->getWidth();
446 int const sy = ay;
447 submenu->show(sx, sy);
448 mOpenChild = submenu;
449 }
450 }
451
452 break;
453 }
454 ++idx;
455 }
456 }
457
459 {
460 }
461
463 {
464 Key const key = event.getKey();
465 // ESC closes this menu (and its root will pop modal)
466 if (key.getValue() == Key::Escape) {
467 // If this menu has a parent, close self; otherwise close root
468 hide();
469 event.consume();
470 return;
471 }
472
473 if (mParentMenuItem != nullptr) {
474 if (key.getValue() == Key::Up || key.getValue() == Key::Down) {
475 auto children = getChildren();
476 if (children.empty()) {
477 return;
478 }
479
480 mHoverIndex = std::max(mHoverIndex, 0);
481
482 if (key.getValue() == Key::Up) {
483 mHoverIndex =
484 (mHoverIndex - 1 + static_cast<int>(children.size())) % static_cast<int>(children.size());
485 } else {
486 mHoverIndex = (mHoverIndex + 1) % static_cast<int>(children.size());
487 }
488
489 // Advance iterator to the hovered child
490 int i = 0;
491 for (auto* target : children) {
492 if (i == mHoverIndex) {
493 if (target != nullptr) {
494 target->requestFocus();
495 }
496 break;
497 }
498 ++i;
499 }
500
501 event.consume();
502 return;
503 }
504
505 if (key.getValue() == Key::Right) {
506 auto children = getChildren();
507 if (mHoverIndex >= 0) {
508 int i = 0;
509 for (auto* child : children) {
510 if (i == mHoverIndex) {
511 if (auto* mi = dynamic_cast<MenuItem*>(child)) {
512 if (mi->getSubmenu() != nullptr) {
513 MenuPopup* submenu = mi->getSubmenu();
514 submenu->setParentMenu(this);
515 submenu->setParentMenuItem(mi);
516 int ax = 0;
517 int ay = 0;
518 mi->getAbsolutePosition(ax, ay);
519 submenu->show(ax + mi->getWidth(), ay);
520 mOpenChild = submenu;
521 event.consume();
522 return;
523 }
524 }
525 break;
526 }
527 ++i;
528 }
529 }
530 }
531
532 if (key.getValue() == Key::Left) {
533 if (mParentMenu != nullptr) {
534 hide();
535 event.consume();
536 return;
537 }
538 }
539 }
540 }
541
543 {
544 }
545
546 void MenuPopup::focusLost(Event const & event)
547 {
548 // Close when focus is lost
549 if (isVisible()) {
550 hide();
551 }
552 }
553
554 void MenuPopup::action(ActionEvent const & event)
555 {
556 // Handle selection action from child MenuItems.
557 auto* source = dynamic_cast<MenuItem*>(event.getSource());
558 if (source == nullptr) {
559 return;
560 }
561
562 // Toggle checkable items
563 if (source->getType() == MenuItem::Type::Checkable) {
564 source->setChecked(!source->isChecked());
565 }
566
567 // Close the entire menu tree: find root and hide it
568 MenuPopup* root = this;
569 while (root->getParentMenu() != nullptr) {
570 root = root->getParentMenu();
571 }
572 root->hide();
573 }
574} // namespace fcn
Represents an action trigger (e.g., button click).
Color.
Definition color.hpp:58
void draw(Graphics *graphics) override
Draws the widget.
Definition container.cpp:25
virtual void setLayout(LayoutPolicy policy)
Sets the layout of the container.
Container()
Constructor.
void resizeToContent(bool recursion=true) override
Resize this container to fit its children.
virtual void add(Widget *widget)
Adds a widget to the container.
Definition container.cpp:92
Widget * getChild(unsigned int index) const
Gets child by index.
virtual void setOpaque(bool opaque)
Sets the container to be opaque or not.
Definition container.cpp:82
Base class for all GUI event objects.
Definition event.hpp:25
Abstract interface providing primitive drawing functions (lines, rectangles, etc.).
Definition graphics.hpp:58
virtual void setColor(Color const &color)=0
Sets the color to use when drawing.
virtual void fillRectangle(Rectangle const &rectangle)=0
Draws a filled rectangle.
Represents a key event.
Definition keyevent.hpp:26
Represents a keyboard key or character code.
Definition key.hpp:22
int getValue() const
Gets the value of the key.
Definition key.cpp:31
A menu bar widget that displays menus at the top of a window.
Definition menubar.hpp:32
A menu item widget for use in menus.
Definition menuitem.hpp:124
void setParentMenuItem(Widget *parent)
Sets the parent menu item that opened this popup.
Widget * getParentMenuItem() const
Gets the parent menu item.
void setParentMenu(MenuPopup *parent)
Sets the parent MenuPopup (for nested menus).
void hide()
Hides the popup.
void addItem(Widget *item)
Adds a menu item to the popup.
void mouseEntered(MouseEvent &event) override
Called when the mouse has entered into the widget area.
void action(ActionEvent const &actionEvent) override
Handles an action event emitted by a widget.
void mousePressed(MouseEvent &event) override
Called when a mouse button has been pressed down on the widget area.
void keyPressed(KeyEvent &event) override
Called if a key is pressed when the widget has keyboard focus.
bool isVisible() const
Checks if the popup is visible.
MenuPopup * getParentMenu() const
Gets the parent MenuPopup (for nested menus).
void focusLost(Event const &event) override
Called when a widget loses focus.
void mouseExited(MouseEvent &event) override
Called when the mouse has exited the widget area.
void mouseReleased(MouseEvent &event) override
Called when a mouse button has been released on the widget area.
void draw(Graphics *graphics) override
Draws the widget.
MenuPopup()
Constructor.
void show(int x, int y)
Shows the popup at a specific position.
void addSeparator()
Adds a separator to the popup.
void keyReleased(KeyEvent &event) override
Called if a key is released when the widget has keyboard focus.
A Transparent fullscreen backdrop.
Represents a mouse event.
Represents a rectangular area (X, Y, Width, Height).
Definition rectangle.hpp:22
Abstract base class defining the common behavior, properties, and lifecycle of all GUI elements.
Definition widget.hpp:55
void setVisible(bool visible)
Sets the widget to be visible, or not.
Definition widget.cpp:684
std::list< Widget * > const & getChildren() const
Gets the children of the widget.
Definition widget.cpp:1592
virtual FocusHandler * _getFocusHandler()
Gets the focus handler used.
Definition widget.cpp:837
bool contains(int x, int y) const
Checks if a point is within the widget's bounds.
Definition widget.cpp:269
unsigned int getChildrenCount() const
Gets how many childs the widget have.
Definition widget.cpp:338
virtual void adjustSize()
Resizes the widget's size to fit the content exactly.
Definition widget.hpp:1574
int getWidth() const
Gets the width of the widget.
Definition widget.cpp:251
virtual Widget * getParent() const
Gets the widget's parent container.
Definition widget.cpp:238
void setBorderSize(unsigned int size)
Sets the size of the widget's border.
Definition widget.cpp:468
virtual void moveToTop(Widget *widget)
Moves a widget to the top of this widget.
Definition widget.cpp:1467
unsigned int getPaddingLeft() const
Gets the left padding.
Definition widget.cpp:603
virtual void setSize(int width, int height)
Sets the size of the widget.
Definition widget.cpp:1064
virtual void requestFocus()
Requests focus for the widget.
Definition widget.cpp:659
void setBorderStyle(unsigned int style)
Set border drawing style (bevel or flat).
Definition widget.cpp:488
void setPaddingRight(unsigned int padding)
Sets the right padding.
Definition widget.cpp:578
void addMouseListener(MouseListener *mouseListener)
Adds a mouse listener to the widget.
Definition widget.cpp:906
void setPosition(int x, int y)
Sets position of the widget.
Definition widget.cpp:305
unsigned int getBorderStyle() const
Get the current border drawing style.
Definition widget.cpp:493
void setPaddingLeft(unsigned int padding)
Sets the left padding.
Definition widget.cpp:598
unsigned int getPaddingTop() const
Gets the top padding.
Definition widget.cpp:573
unsigned int getBorderSize() const
Gets the size of the widget's border.
Definition widget.cpp:473
virtual Widget * getTop() const
Gets the top widget, or top parent, of this widget.
Definition widget.cpp:1315
unsigned int getPaddingBottom() const
Gets the bottom padding.
Definition widget.cpp:593
Font * getFont() const
Gets the font set for the widget.
Definition widget.cpp:999
virtual void getAbsolutePosition(int &x, int &y) const
Gets the absolute position on the screen for the widget.
Definition widget.cpp:977
int getHeight() const
Gets the height of the widget.
Definition widget.cpp:264
unsigned int getPaddingRight() const
Gets the right padding.
Definition widget.cpp:583
unsigned int getBorderSides() const
Get the currently selected border sides.
Definition widget.cpp:483
Used replacement tokens by configure_file():