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 | } |