#include "StarPaneManager.hpp" #include "StarGameTypes.hpp" #include "StarJsonExtra.hpp" #include "StarAssets.hpp" #include "StarRoot.hpp" namespace Star { EnumMap const PaneLayerNames{ {PaneLayer::Tooltip, "Tooltip"}, {PaneLayer::ModalWindow, "ModalWindow"}, {PaneLayer::Window, "Window"}, {PaneLayer::Hud, "Hud"}, {PaneLayer::World, "World"} }; PaneManager::PaneManager() : m_context(GuiContext::singletonPtr()), m_prevInterfaceScale(1) { auto assets = Root::singleton().assets(); m_tooltipMouseoverTime = assets->json("/panes.config:tooltipMouseoverTime").toFloat(); m_tooltipMouseoverRadius = assets->json("/panes.config:tooltipMouseoverRadius").toFloat(); m_tooltipMouseOffset = jsonToVec2I(assets->json("/panes.config:tooltipMouseoverOffset")); m_tooltipShowTimer = m_tooltipMouseoverTime; } void PaneManager::displayPane(PaneLayer paneLayer, PanePtr const& pane, DismissCallback onDismiss) { if (!m_displayedPanes[paneLayer].insertFront(pane, move(onDismiss)).second) throw GuiException("Pane displayed twice in PaneManager::displayPane"); if (!pane->hasDisplayed() && pane->anchor() == PaneAnchor::None) pane->setPosition(Vec2I((windowSize() - pane->size()) / 2) + pane->centerOffset()); // center it pane->displayed(); } bool PaneManager::isDisplayed(PanePtr const& pane) const { for (auto const& layerPair : m_displayedPanes) { if (layerPair.second.contains(pane)) return true; } return false; } void PaneManager::dismissPane(PanePtr const& pane) { if (!dismiss(pane)) throw GuiException("No such pane in PaneManager::dismissPane"); } void PaneManager::dismissAllPanes(Set const& paneLayers) { for (auto const& paneLayer : paneLayers) { for (auto const& panePair : copy(m_displayedPanes[paneLayer])) dismiss(panePair.first); } } void PaneManager::dismissAllPanes() { for (auto layerPair : copy(m_displayedPanes)) { for (auto const& panePair : layerPair.second) dismiss(panePair.first); } } PanePtr PaneManager::topPane(Set const& paneLayers) const { for (auto const& layerPair : m_displayedPanes) { if (paneLayers.contains(layerPair.first) && !layerPair.second.empty()) return layerPair.second.firstKey(); } return {}; } PanePtr PaneManager::topPane() const { for (auto const& layerPair : m_displayedPanes) { if (!layerPair.second.empty()) return layerPair.second.firstKey(); } return {}; } void PaneManager::bringToTop(PanePtr const& pane) { for (auto& layerPair : m_displayedPanes) { if (layerPair.second.contains(pane)) { layerPair.second.toFront(pane); return; } } throw GuiException("Pane was not displayed in PaneManager::bringToTop"); } void PaneManager::bringPaneAdjacent(PanePtr const& anchor, PanePtr const& adjacent, int gap) { Vec2I centerAdjacent = anchor->position() + (anchor->size() / 2) - (adjacent->size() / 2); centerAdjacent = centerAdjacent.piecewiseClamp(Vec2I(), windowSize() - adjacent->size()); // keeps pane inside window if (anchor->position()[0] + anchor->size()[0] + gap + adjacent->size()[0] <= windowSize()[0]) adjacent->setPosition(Vec2I(anchor->position()[0] + anchor->size()[0] + gap, centerAdjacent[1])); // place to the right else if (anchor->position()[0] - gap - adjacent->size()[0] >= 0) adjacent->setPosition(Vec2I(anchor->position()[0] - gap - adjacent->size()[0], centerAdjacent[1])); // place to the left else if (anchor->position()[1] + anchor->size()[1] + gap + adjacent->size()[1] <= windowSize()[1]) adjacent->setPosition(Vec2I(centerAdjacent[0], anchor->position()[1] + anchor->size()[1] + gap)); // place above else if (anchor->position()[1] - gap - adjacent->size()[1] >= 0) adjacent->setPosition(Vec2I(centerAdjacent[0], anchor->position()[1] - gap - adjacent->size()[1])); // place below else adjacent->setPosition(centerAdjacent); bringToTop(adjacent); } PanePtr PaneManager::getPaneAt(Set const& paneLayers, Vec2I const& position) const { for (auto const& layerPair : m_displayedPanes) { if (!paneLayers.contains(layerPair.first)) continue; for (auto const& panePair : layerPair.second) { if (panePair.first->inWindow(position) && panePair.first->active()) return panePair.first; } } return {}; } PanePtr PaneManager::getPaneAt(Vec2I const& position) const { for (auto const& layerPair : m_displayedPanes) { for (auto const& panePair : layerPair.second) { if (panePair.first->inWindow(position) && panePair.first->active()) return panePair.first; } } return {}; } void PaneManager::setBackgroundWidget(WidgetPtr bg) { m_backgroundWidget = bg; } void PaneManager::dismissWhere(function func) { if (!func) return; for (auto& layerPair : m_displayedPanes) { eraseWhere(layerPair.second, [&](auto& panePair) { if (func(panePair.first)) { panePair.first->dismissed(); if (panePair.second) panePair.second(panePair.first); return true; } return false; }); } } PanePtr PaneManager::keyboardCapturedPane() const { for (auto const& layerPair : m_displayedPanes) { for (auto const& panePair : layerPair.second) { if (panePair.first->active() && panePair.first->keyboardCaptured() != KeyboardCaptureMode::None) return panePair.first; } } return {}; } bool PaneManager::keyboardCapturedForTextInput() const { if (auto pane = keyboardCapturedPane()) return pane->keyboardCaptured() == KeyboardCaptureMode::TextInput; return false; } bool PaneManager::sendInputEvent(InputEvent const& event) { if (event.is()) { m_tooltipLastMousePos = *m_context->mousePosition(event); for (auto const& layerPair : m_displayedPanes) { for (auto const& panePair : layerPair.second) { if (panePair.first->dragActive()) { panePair.first->drag(*m_context->mousePosition(event)); return true; } } } } if (event.is() || vmag(m_tooltipInitialPosition - m_tooltipLastMousePos) > m_tooltipMouseoverRadius) { m_tooltipShowTimer = m_tooltipMouseoverTime; if (m_activeTooltip) { dismiss(m_activeTooltip); m_activeTooltip.reset(); m_tooltipParentPane.reset(); m_tooltipShowTimer = m_tooltipMouseoverTime; } } if (event.is()) { for (auto const& layerPair : m_displayedPanes) { for (auto const& panePair : layerPair.second) { if (panePair.first->dragActive()) { panePair.first->setDragActive(false, {}); return true; } } } } // The gui close event can only be intercepted by a pane that has captured // the keyboard otherwise it will always be used to close first before being // a normal event. This is so a window can control its own closing if it // really needs to (like the keybindings window). if (event.is() && m_context->actions(event).contains(InterfaceAction::GuiClose)) { if (auto top = topPane({PaneLayer::ModalWindow, PaneLayer::Window})) { dismiss(top); return true; } } // If there is a pane that has captured the keyboard, keyboard events will // ONLY be sent to it. auto keyCapturePane = keyboardCapturedPane(); if (keyCapturePane && (event.is() || event.is() || event.is())) return keyCapturePane->sendEvent(event); bool foundModal = false; for (auto& layerPair : m_displayedPanes) { for (auto const& panePair : copy(layerPair.second)) { if (panePair.first->sendEvent(event)) { if (event.is()) layerPair.second.toFront(panePair.first); return true; } // If any modal windows are shown, Only the first modal window should // have a chance to consume the input event and all other panes below it // including different layers should ignore it. if (layerPair.first == PaneLayer::ModalWindow) { foundModal = true; break; } } if (foundModal) break; } return false; } void PaneManager::render() { if (m_backgroundWidget) { auto size = m_backgroundWidget->size(); m_backgroundWidget->setPosition(Vec2I((windowSize()[0] - size[0]) / 2, (windowSize()[1] - size[1]) / 2)); m_backgroundWidget->render(RectI(Vec2I(), windowSize())); } for (auto const& layerPair : reverseIterate(m_displayedPanes)) { for (auto const& panePair : reverseIterate(layerPair.second)) { if (panePair.first->active()) { if (m_prevInterfaceScale != m_context->interfaceScale()) panePair.first->setPosition( calculateNewInterfacePosition(panePair.first, (float)m_context->interfaceScale() / m_prevInterfaceScale)); panePair.first->setDrawingOffset(calculatePaneOffset(panePair.first)); panePair.first->render(RectI(Vec2I(), windowSize())); } } } m_context->resetInterfaceScissorRect(); m_prevInterfaceScale = m_context->interfaceScale(); } void PaneManager::update(float dt) { m_tooltipShowTimer -= WorldTimestep; if (m_tooltipShowTimer < 0 && !m_activeTooltip) { if (auto parentPane = getPaneAt(m_tooltipLastMousePos)) { if (auto tooltip = parentPane->createTooltip(m_tooltipLastMousePos)) { m_activeTooltip = move(tooltip); m_tooltipParentPane = move(parentPane); m_tooltipInitialPosition = m_tooltipLastMousePos; displayPane(PaneLayer::Tooltip, m_activeTooltip); Vec2I offsetDirection = Vec2I::filled(1); Vec2I offsetAdjust = Vec2I(); if (m_tooltipLastMousePos[0] + m_tooltipMouseOffset[0] + m_activeTooltip->size()[0] > (int)m_context->windowWidth() / m_context->interfaceScale()) { offsetDirection[0] = -1; offsetAdjust[0] = -m_activeTooltip->size()[0]; } if (m_tooltipLastMousePos[1] + m_tooltipMouseOffset[1] - m_activeTooltip->size()[1] < 0) offsetDirection[1] = -1; else offsetAdjust[1] = -m_activeTooltip->size()[1]; m_activeTooltip->setPosition(m_tooltipLastMousePos + (offsetAdjust + m_tooltipMouseOffset.piecewiseMultiply(offsetDirection))); } else { m_tooltipShowTimer = m_tooltipMouseoverTime; } } } else if (m_activeTooltip && !m_tooltipParentPane->isDisplayed()) { dismiss(m_activeTooltip); m_activeTooltip.reset(); m_tooltipParentPane.reset(); } for (auto const& layerPair : m_displayedPanes) { for (auto const& panePair : copy(layerPair.second)) { if (panePair.first->isDismissed()) dismiss(panePair.first); } } for (auto const& layerPair : reverseIterate(m_displayedPanes)) { for (auto const& panePair : reverseIterate(layerPair.second)) { panePair.first->tick(dt); if (panePair.first->active()) panePair.first->update(dt); } } } Vec2I PaneManager::windowSize() const { return Vec2I(m_context->windowInterfaceSize()); } Vec2I PaneManager::calculatePaneOffset(PanePtr const& pane) const { Vec2I size = pane->size(); switch (pane->anchor()) { case PaneAnchor::None: return pane->anchorOffset(); case PaneAnchor::BottomLeft: return pane->anchorOffset(); case PaneAnchor::BottomRight: return pane->anchorOffset() + Vec2I{windowSize()[0] - size[0], 0}; case PaneAnchor::TopLeft: return pane->anchorOffset() + Vec2I{0, windowSize()[1] - size[1]}; case PaneAnchor::TopRight: return pane->anchorOffset() + (windowSize() - size); case PaneAnchor::CenterTop: return pane->anchorOffset() + Vec2I{(windowSize()[0] - size[0]) / 2, windowSize()[1] - size[1]}; case PaneAnchor::CenterBottom: return pane->anchorOffset() + Vec2I{(windowSize()[0] - size[0]) / 2, 0}; case PaneAnchor::CenterLeft: return pane->anchorOffset() + Vec2I{0, (windowSize()[1] - size[1]) / 2}; case PaneAnchor::CenterRight: return pane->anchorOffset() + Vec2I{windowSize()[0] - size[0], (windowSize()[1] - size[1]) / 2}; case PaneAnchor::Center: return pane->anchorOffset() + ((windowSize() - size) / 2); default: return pane->anchorOffset(); } } Vec2I PaneManager::calculateNewInterfacePosition(PanePtr const& pane, float interfaceScaleRatio) const { Vec2F position(pane->relativePosition()); Vec2F size(pane->size()); Mat3F scale; switch (pane->anchor()) { case PaneAnchor::None: scale = Mat3F::scaling(interfaceScaleRatio, Vec2F(windowSize()) / 2); break; case PaneAnchor::BottomLeft: scale = Mat3F::scaling(interfaceScaleRatio); break; case PaneAnchor::BottomRight: scale = Mat3F::scaling(interfaceScaleRatio, {size[0], 0}); break; case PaneAnchor::TopLeft: scale = Mat3F::scaling(interfaceScaleRatio, {0, size[1]}); break; case PaneAnchor::TopRight: scale = Mat3F::scaling(interfaceScaleRatio, size); break; case PaneAnchor::CenterTop: scale = Mat3F::scaling(interfaceScaleRatio, {size[0] / 2, size[1]}); break; case PaneAnchor::CenterBottom: scale = Mat3F::scaling(interfaceScaleRatio, {size[0] / 2, 0}); break; case PaneAnchor::CenterLeft: scale = Mat3F::scaling(interfaceScaleRatio, {0, size[1] / 2}); break; case PaneAnchor::CenterRight: scale = Mat3F::scaling(interfaceScaleRatio, {size[0], size[1] / 2}); break; case PaneAnchor::Center: scale = Mat3F::scaling(interfaceScaleRatio, size / 2); break; default: scale = Mat3F::scaling(interfaceScaleRatio, Vec2F(windowSize()) / 2); } return Vec2I::round((scale * Vec3F(position, 0)).vec2()); } bool PaneManager::dismiss(PanePtr const& pane) { bool dismissed = false; for (auto& layerPair : m_displayedPanes) { if (auto panePair = layerPair.second.maybeTake(pane)) { dismissed = true; panePair->first->dismissed(); if (panePair->second) panePair->second(pane); } } return dismissed; } }