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