| 1 | //************************************ bs::framework - Copyright 2018 Marko Pintera **************************************// |
| 2 | //*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********// |
| 3 | #include "GUI/BsGUIWidget.h" |
| 4 | #include "GUI/BsGUIManager.h" |
| 5 | #include "GUI/BsGUISkin.h" |
| 6 | #include "GUI/BsGUILabel.h" |
| 7 | #include "GUI/BsGUIPanel.h" |
| 8 | #include "GUI/BsGUINavGroup.h" |
| 9 | #include "Math/BsVector2I.h" |
| 10 | #include "Components/BsCCamera.h" |
| 11 | #include "RenderAPI/BsViewport.h" |
| 12 | #include "Scene/BsSceneObject.h" |
| 13 | #include "Resources/BsBuiltinResources.h" |
| 14 | |
| 15 | namespace bs |
| 16 | { |
| 17 | GUIWidget::GUIWidget(const SPtr<Camera>& camera) |
| 18 | : mCamera(camera), mPanel(nullptr), mDepth(128), mIsActive(true), mPosition(BsZero), mRotation(BsIdentity) |
| 19 | , mScale(Vector3::ONE), mTransform(BsIdentity), mCachedRTId(0), mWidgetIsDirty(false) |
| 20 | { |
| 21 | construct(camera); |
| 22 | } |
| 23 | |
| 24 | GUIWidget::GUIWidget(const HCamera& camera) |
| 25 | : mCamera(camera->_getCamera()), mPanel(nullptr), mDepth(128), mIsActive(true), mPosition(BsZero) |
| 26 | , mRotation(BsIdentity), mScale(Vector3::ONE), mTransform(BsIdentity), mCachedRTId(0), mWidgetIsDirty(false) |
| 27 | { |
| 28 | construct(mCamera); |
| 29 | } |
| 30 | |
| 31 | void GUIWidget::construct(const SPtr<Camera>& camera) |
| 32 | { |
| 33 | if (mCamera != nullptr) |
| 34 | { |
| 35 | SPtr<RenderTarget> target = mCamera->getViewport()->getTarget(); |
| 36 | |
| 37 | if (target != nullptr) |
| 38 | mCachedRTId = target->getInternalID(); |
| 39 | } |
| 40 | |
| 41 | mDefaultNavGroup = GUINavGroup::create(); |
| 42 | |
| 43 | GUIManager::instance().registerWidget(this); |
| 44 | |
| 45 | mPanel = GUIPanel::create(); |
| 46 | mPanel->_changeParentWidget(this); |
| 47 | updateRootPanel(); |
| 48 | } |
| 49 | |
| 50 | GUIWidget::~GUIWidget() |
| 51 | { |
| 52 | _destroy(); |
| 53 | } |
| 54 | |
| 55 | SPtr<GUIWidget> GUIWidget::create(const SPtr<Camera>& camera) |
| 56 | { |
| 57 | return bs_shared_ptr(new (bs_alloc<GUIWidget>()) GUIWidget(camera)); |
| 58 | } |
| 59 | |
| 60 | SPtr<GUIWidget> GUIWidget::create(const HCamera& camera) |
| 61 | { |
| 62 | return bs_shared_ptr(new (bs_alloc<GUIWidget>()) GUIWidget(camera)); |
| 63 | } |
| 64 | |
| 65 | void GUIWidget::_destroy() |
| 66 | { |
| 67 | if (mPanel != nullptr) |
| 68 | { |
| 69 | GUILayout::destroy(mPanel); |
| 70 | mPanel = nullptr; |
| 71 | } |
| 72 | |
| 73 | if (mCamera != nullptr) |
| 74 | { |
| 75 | GUIManager::instance().unregisterWidget(this); |
| 76 | mCamera = nullptr; |
| 77 | } |
| 78 | |
| 79 | mElements.clear(); |
| 80 | mDirtyContents.clear(); |
| 81 | } |
| 82 | |
| 83 | void GUIWidget::setDepth(UINT8 depth) |
| 84 | { |
| 85 | mDepth = depth; |
| 86 | mWidgetIsDirty = true; |
| 87 | |
| 88 | updateRootPanel(); |
| 89 | } |
| 90 | |
| 91 | Viewport* GUIWidget::getTarget() const |
| 92 | { |
| 93 | if(mCamera != nullptr) |
| 94 | return mCamera->getViewport().get(); |
| 95 | |
| 96 | return nullptr; |
| 97 | } |
| 98 | |
| 99 | void GUIWidget::_updateTransform(const HSceneObject& parent) |
| 100 | { |
| 101 | // If the widgets parent scene object moved, we need to mark it as dirty |
| 102 | // as the GUIManager batching relies on object positions, so it needs to be updated. |
| 103 | const float diffEpsilon = 0.0001f; |
| 104 | |
| 105 | const Transform& tfrm = parent->getTransform(); |
| 106 | Vector3 position = tfrm.getPosition(); |
| 107 | Quaternion rotation = tfrm.getRotation(); |
| 108 | Vector3 scale = tfrm.getScale(); |
| 109 | |
| 110 | if(!mWidgetIsDirty) |
| 111 | { |
| 112 | if(!Math::approxEquals(mPosition, position, diffEpsilon)) |
| 113 | mWidgetIsDirty = true; |
| 114 | else |
| 115 | { |
| 116 | if(!Math::approxEquals(mRotation, rotation, diffEpsilon)) |
| 117 | mWidgetIsDirty = true; |
| 118 | else |
| 119 | { |
| 120 | if(Math::approxEquals(mScale, scale)) |
| 121 | mWidgetIsDirty = true; |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | mPosition = position; |
| 127 | mRotation = rotation; |
| 128 | mScale = scale; |
| 129 | mTransform = parent->getWorldMatrix(); |
| 130 | } |
| 131 | |
| 132 | void GUIWidget::_updateRT() |
| 133 | { |
| 134 | SPtr<RenderTarget> rt; |
| 135 | UINT64 newRTId = 0; |
| 136 | if(mCamera != nullptr) |
| 137 | { |
| 138 | rt = mCamera->getViewport()->getTarget(); |
| 139 | if (rt != nullptr) |
| 140 | newRTId = rt->getInternalID(); |
| 141 | } |
| 142 | |
| 143 | if(mCachedRTId != newRTId) |
| 144 | { |
| 145 | mCachedRTId = newRTId; |
| 146 | updateRootPanel(); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | void GUIWidget::_updateLayout() |
| 151 | { |
| 152 | // Check if render target size changed and update if needed |
| 153 | // Note: Purposely not relying to the RenderTarget::onResized callback, as it will trigger /before/ Input events. |
| 154 | // These events might trigger a resize, meaning the size would be delayed one frame, resulting in a visual artifact |
| 155 | // where the GUI doesn't match the target size. |
| 156 | Viewport* target = getTarget(); |
| 157 | if (target != nullptr) |
| 158 | { |
| 159 | Rect2I area = target->getPixelArea(); |
| 160 | UINT32 width = area.width; |
| 161 | UINT32 height = area.height; |
| 162 | |
| 163 | const Rect2I& panelArea = mPanel->_getLayoutData().area; |
| 164 | if(panelArea.width != width || panelArea.height != height) |
| 165 | { |
| 166 | updateRootPanel(); |
| 167 | onOwnerTargetResized(); |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | bs_frame_mark(); |
| 172 | |
| 173 | // Determine dirty contents and layouts |
| 174 | FrameStack<GUIElementBase*> todo; |
| 175 | todo.push(mPanel); |
| 176 | |
| 177 | while (!todo.empty()) |
| 178 | { |
| 179 | GUIElementBase* currentElem = todo.top(); |
| 180 | todo.pop(); |
| 181 | |
| 182 | if (currentElem->_isDirty()) |
| 183 | { |
| 184 | GUIElementBase* updateParent = currentElem->_getUpdateParent(); |
| 185 | assert(updateParent != nullptr || currentElem == mPanel); |
| 186 | |
| 187 | if (updateParent != nullptr) |
| 188 | _updateLayout(updateParent); |
| 189 | else // Must be root panel |
| 190 | _updateLayout(mPanel); |
| 191 | } |
| 192 | else |
| 193 | { |
| 194 | UINT32 numChildren = currentElem->_getNumChildren(); |
| 195 | for (UINT32 i = 0; i < numChildren; i++) |
| 196 | todo.push(currentElem->_getChild(i)); |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | bs_frame_clear(); |
| 201 | } |
| 202 | |
| 203 | void GUIWidget::_updateLayout(GUIElementBase* elem) |
| 204 | { |
| 205 | GUIElementBase* parent = elem->_getParent(); |
| 206 | bool isPanelOptimized = parent != nullptr && parent->_getType() == GUIElementBase::Type::Panel; |
| 207 | |
| 208 | GUIElementBase* updateParent = nullptr; |
| 209 | |
| 210 | if (isPanelOptimized) |
| 211 | updateParent = parent; |
| 212 | else |
| 213 | updateParent = elem; |
| 214 | |
| 215 | // For GUIPanel we can do a an optimization and update only the element in question instead |
| 216 | // of all the children |
| 217 | if (isPanelOptimized) |
| 218 | { |
| 219 | GUIPanel* panel = static_cast<GUIPanel*>(updateParent); |
| 220 | |
| 221 | GUIElementBase* dirtyElement = elem; |
| 222 | dirtyElement->_updateOptimalLayoutSizes(); |
| 223 | |
| 224 | LayoutSizeRange elementSizeRange = panel->_getElementSizeRange(dirtyElement); |
| 225 | Rect2I elementArea = panel->_getElementArea(panel->_getLayoutData().area, dirtyElement, elementSizeRange); |
| 226 | |
| 227 | GUILayoutData childLayoutData = panel->_getLayoutData(); |
| 228 | panel->_updateDepthRange(childLayoutData); |
| 229 | childLayoutData.area = elementArea; |
| 230 | |
| 231 | panel->_updateChildLayout(dirtyElement, childLayoutData); |
| 232 | } |
| 233 | else |
| 234 | { |
| 235 | GUILayoutData childLayoutData = updateParent->_getLayoutData(); |
| 236 | updateParent->_updateLayout(childLayoutData); |
| 237 | } |
| 238 | |
| 239 | // Mark dirty contents |
| 240 | bs_frame_mark(); |
| 241 | { |
| 242 | FrameStack<GUIElementBase*> todo; |
| 243 | todo.push(elem); |
| 244 | |
| 245 | while (!todo.empty()) |
| 246 | { |
| 247 | GUIElementBase* currentElem = todo.top(); |
| 248 | todo.pop(); |
| 249 | |
| 250 | if (currentElem->_getType() == GUIElementBase::Type::Element) |
| 251 | mDirtyContents.insert(static_cast<GUIElement*>(currentElem)); |
| 252 | |
| 253 | currentElem->_markAsClean(); |
| 254 | |
| 255 | UINT32 numChildren = currentElem->_getNumChildren(); |
| 256 | for (UINT32 i = 0; i < numChildren; i++) |
| 257 | todo.push(currentElem->_getChild(i)); |
| 258 | } |
| 259 | } |
| 260 | bs_frame_clear(); |
| 261 | } |
| 262 | |
| 263 | void GUIWidget::_registerElement(GUIElementBase* elem) |
| 264 | { |
| 265 | assert(elem != nullptr && !elem->_isDestroyed()); |
| 266 | |
| 267 | if (elem->_getType() == GUIElementBase::Type::Element) |
| 268 | { |
| 269 | mElements.push_back(static_cast<GUIElement*>(elem)); |
| 270 | mWidgetIsDirty = true; |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | void GUIWidget::_unregisterElement(GUIElementBase* elem) |
| 275 | { |
| 276 | assert(elem != nullptr); |
| 277 | |
| 278 | auto iterFind = std::find(begin(mElements), end(mElements), elem); |
| 279 | |
| 280 | if (iterFind != mElements.end()) |
| 281 | { |
| 282 | mElements.erase(iterFind); |
| 283 | mWidgetIsDirty = true; |
| 284 | } |
| 285 | |
| 286 | if (elem->_getType() == GUIElementBase::Type::Element) |
| 287 | mDirtyContents.erase(static_cast<GUIElement*>(elem)); |
| 288 | } |
| 289 | |
| 290 | void GUIWidget::_markMeshDirty(GUIElementBase* elem) |
| 291 | { |
| 292 | mWidgetIsDirty = true; |
| 293 | } |
| 294 | |
| 295 | void GUIWidget::_markContentDirty(GUIElementBase* elem) |
| 296 | { |
| 297 | if (elem->_getType() == GUIElementBase::Type::Element) |
| 298 | mDirtyContents.insert(static_cast<GUIElement*>(elem)); |
| 299 | } |
| 300 | |
| 301 | void GUIWidget::setSkin(const HGUISkin& skin) |
| 302 | { |
| 303 | mSkin = skin; |
| 304 | |
| 305 | for(auto& element : mElements) |
| 306 | element->_refreshStyle(); |
| 307 | } |
| 308 | |
| 309 | const GUISkin& GUIWidget::getSkin() const |
| 310 | { |
| 311 | if(mSkin.isLoaded()) |
| 312 | return *mSkin; |
| 313 | else |
| 314 | return *BuiltinResources::instance().getGUISkin(); |
| 315 | } |
| 316 | |
| 317 | void GUIWidget::setCamera(const SPtr<Camera>& camera) |
| 318 | { |
| 319 | SPtr<Camera> newCamera = camera; |
| 320 | if(newCamera != nullptr) |
| 321 | { |
| 322 | if (newCamera->getViewport()->getTarget() == nullptr) |
| 323 | newCamera = nullptr; |
| 324 | } |
| 325 | |
| 326 | if (mCamera == newCamera) |
| 327 | return; |
| 328 | |
| 329 | GUIManager::instance().unregisterWidget(this); |
| 330 | mCamera = newCamera; |
| 331 | GUIManager::instance().registerWidget(this); |
| 332 | |
| 333 | updateRootPanel(); |
| 334 | } |
| 335 | |
| 336 | void GUIWidget::setIsActive(bool active) |
| 337 | { |
| 338 | mIsActive = active; |
| 339 | } |
| 340 | |
| 341 | bool GUIWidget::isDirty(bool cleanIfDirty) |
| 342 | { |
| 343 | if (!mIsActive) |
| 344 | return false; |
| 345 | |
| 346 | const bool dirty = mWidgetIsDirty || !mDirtyContents.empty(); |
| 347 | |
| 348 | if(cleanIfDirty && dirty) |
| 349 | { |
| 350 | mWidgetIsDirty = false; |
| 351 | |
| 352 | // Update render contents recursively because updates can cause child GUI elements to become dirty |
| 353 | while(!mDirtyContents.empty()) |
| 354 | { |
| 355 | mDirtyContentsTemp.swap(mDirtyContents); |
| 356 | |
| 357 | for (auto& dirtyElement : mDirtyContentsTemp) |
| 358 | dirtyElement->_updateRenderElements(); |
| 359 | |
| 360 | mDirtyContentsTemp.clear(); |
| 361 | } |
| 362 | |
| 363 | updateBounds(); |
| 364 | } |
| 365 | |
| 366 | return dirty; |
| 367 | } |
| 368 | |
| 369 | bool GUIWidget::inBounds(const Vector2I& position) const |
| 370 | { |
| 371 | Viewport* target = getTarget(); |
| 372 | if (target == nullptr) |
| 373 | return false; |
| 374 | |
| 375 | // Technically GUI widget bounds can be larger than the viewport, so make sure we clip to viewport first |
| 376 | if(!target->getPixelArea().contains(position)) |
| 377 | return false; |
| 378 | |
| 379 | Vector3 vecPos((float)position.x, (float)position.y, 0.0f); |
| 380 | vecPos = mTransform.inverse().multiplyAffine(vecPos); |
| 381 | |
| 382 | Vector2I localPos(Math::roundToInt(vecPos.x), Math::roundToInt(vecPos.y)); |
| 383 | return mBounds.contains(localPos); |
| 384 | } |
| 385 | |
| 386 | void GUIWidget::updateBounds() const |
| 387 | { |
| 388 | if(mElements.size() > 0) |
| 389 | mBounds = mElements[0]->_getClippedBounds(); |
| 390 | |
| 391 | for(auto& elem : mElements) |
| 392 | { |
| 393 | Rect2I elemBounds = elem->_getClippedBounds(); |
| 394 | mBounds.encapsulate(elemBounds); |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | void GUIWidget::ownerWindowFocusChanged() |
| 399 | { |
| 400 | onOwnerWindowFocusChanged(); |
| 401 | } |
| 402 | |
| 403 | void GUIWidget::updateRootPanel() |
| 404 | { |
| 405 | Viewport* target = getTarget(); |
| 406 | if (target == nullptr) |
| 407 | return; |
| 408 | |
| 409 | Rect2I area = target->getPixelArea(); |
| 410 | UINT32 width = area.width; |
| 411 | UINT32 height = area.height; |
| 412 | |
| 413 | GUILayoutData layoutData; |
| 414 | layoutData.area.width = width; |
| 415 | layoutData.area.height = height; |
| 416 | layoutData.clipRect = Rect2I(0, 0, width, height); |
| 417 | layoutData.setWidgetDepth(mDepth); |
| 418 | |
| 419 | mPanel->setWidth(width); |
| 420 | mPanel->setHeight(height); |
| 421 | |
| 422 | mPanel->_setLayoutData(layoutData); |
| 423 | mPanel->_markLayoutAsDirty(); |
| 424 | } |
| 425 | } |