Cogs.Core
GuiContainer.cpp
1#include "GuiContainer.h"
2#include "GuiExtension.h"
3
4#include "Renderer/Tasks/RenderTask.h"
5#include "Renderer/RenderTexture.h"
6#include "Renderer/RenderResources.h"
7#include "Renderer/InspectorGui/InspectorGuiRenderer.h"
8
9#include "Resources/TextureManager.h"
10#include "Resources/ResourceStore.h"
11
12#include "Systems/Core/ScriptSystem.h"
13
14#include "Scripting/ScriptingManager.h"
15#include "Scripting/ScriptingEngine.h"
16
17#include "GuiSystem.h"
18
19#include "Foundation/HashSequence.h"
20#include "Foundation/Logging/Logger.h"
21#include "Foundation/ComponentModel/Entity.h"
22
23#include "el_text.h"
24#include "el_script.h"
25
26#define IMGUI_DEFINE_MATH_OPERATORS
27#include "imgui.h"
28#include "imgui_internal.h"
29
30namespace
31{
32 Cogs::Logging::Log logger = Cogs::Logging::getLogger("GuiContainer");
33}
34
35namespace Cogs
36{
37 namespace Core
38 {
39 std::shared_ptr<litehtml::element> findElement(const std::shared_ptr<litehtml::element> & e, const StringView & id)
40 {
41 auto a = e->get_attr("id");
42 if (a && id == a) return e;
43
44 auto numChildren = e->get_children_count();
45
46 for (size_t i = 0; i < numChildren; ++i) {
47 auto child = e->get_child(i);
48 auto found = findElement(child, id);
49
50 if (found) return found;
51 }
52
53 return nullptr;
54 };
55
56 std::string findFont(const StringView & family, int /*weight*/)
57 {
58#if defined(__linux__)
59 if (family.find(".ttf") != StringView::NoPosition) {
60 return family.to_string();
61 }
62 return "/usr/share/fonts/truetype/freefont/FreeSans.ttf";
63#else
64 if (family == "default") {
65 return "C:/Windows/Fonts/times.ttf";
66 }
67 else if (family == "sans-serif" || family == "arial") {
68 return "C:/Windows/Fonts/arial.ttf";
69 }
70 else if (family.find(".ttf") != StringView::NoPosition) {
71 return family.to_string();
72 }
73 return "C:/Windows/Fonts/times.ttf";
74#endif
75 }
76
77 ImVec4 toImVec4(const litehtml::web_color & color)
78 {
79 return { color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, color.alpha / 255.0f };
80 }
81
82 ImVec2 toImVec2(int x, int y)
83 {
84 return ImVec2(static_cast<float>(x), static_cast<float>(y));
85 }
86 }
87}
88
89litehtml::uint_ptr Cogs::Core::GuiContainer::create_font(const litehtml::tchar_t * faceName, int size, int weight, litehtml::font_style /*italic*/, unsigned int /*decoration*/, litehtml::font_metrics * fm)
90{
91 const size_t code = hashSequence(faceName, size, weight);
92
93 auto imRenderer = inspectorGuiRenderer->getImguiRenderer();
94
95 GuiFont * font;
96
97 auto it = imRenderer->fontRegistry.fonts.find(code);
98 if (it != imRenderer->fontRegistry.fonts.end()) {
99 font = &it->second;
100 }
101 else {
102 font = &imRenderer->fontRegistry.fonts[code];
103 font->load(renderContext->context, findFont(faceName, weight), static_cast<float>(size), nullptr);
104 }
105
106 fm->height = static_cast<int>(font->font[0]->FontSize);
107 fm->descent = static_cast<int>(font->font[0]->Descent);
108 fm->ascent = static_cast<int>(font->font[0]->Ascent);
109
110 return reinterpret_cast<litehtml::uint_ptr>(font);
111}
112
113void Cogs::Core::GuiContainer::delete_font(litehtml::uint_ptr /*hFont*/)
114{
115 return;
116}
117
118int Cogs::Core::GuiContainer::text_width(const litehtml::tchar_t * text, litehtml::uint_ptr hFont)
119{
120 auto font = (GuiFont *)hFont;
121
122 StringView view{ text };
123
124 auto size = font->font[0]->CalcTextSizeA(font->font[0]->FontSize, 100000, 0, view.begin(), view.end());
125
126 return static_cast<int>(size.x);
127}
128
129void Cogs::Core::GuiContainer::draw_text(litehtml::uint_ptr /*hdc*/, const litehtml::tchar_t * text, litehtml::uint_ptr hFont, litehtml::web_color color, const litehtml::position & pos)
130{
131 auto font = (GuiFont *)hFont;
132
133 auto col = toImVec4(color);
134
135 ImGui::PushFont(font->font[0]);
136
137 ImGui::SetCursorPos(toImVec2(pos.left(), pos.top() - (int)font->font[0]->FontSize / 2));
138
139 ImGui::PushStyleColor(ImGuiCol_Text, col);
140 ImGui::Text("%s", text);
141 ImGui::PopStyleColor();
142
143 ImGui::PopFont();
144}
145
146int Cogs::Core::GuiContainer::pt_to_px(int pt)
147{
148 return pt;
149}
150
151int Cogs::Core::GuiContainer::get_default_font_size() const
152{
153 return 16;
154}
155
156const litehtml::tchar_t * Cogs::Core::GuiContainer::get_default_font_name() const
157{
158 return "default";
159}
160
161void Cogs::Core::GuiContainer::draw_list_marker(litehtml::uint_ptr /*hdc*/, const litehtml::list_marker & /*marker*/)
162{
163}
164
165void Cogs::Core::GuiContainer::load_image(const litehtml::tchar_t * src, const litehtml::tchar_t * /*baseurl*/, bool /*redraw_on_ready*/)
166{
167 auto name = StringView(src);
168 auto code = name.hash();
169
170 auto it = guiTextures.find(code);
171
172 if (it != guiTextures.end()) return;
173
174 if (name[0] == '$') {
175 guiTextures[code] = renderContext->context->textureManager->getTexture(name);
176 } else {
177 guiTextures[code] = renderContext->context->textureManager->loadTexture(name, NoResourceId, TextureLoadFlags::None);
178 }
179}
180
181void Cogs::Core::GuiContainer::get_image_size(const litehtml::tchar_t * src, const litehtml::tchar_t * /*baseurl*/, litehtml::size & sz)
182{
183 auto name = StringView(src);
184
185 auto it = guiTextures.find(name.hash());
186
187 if (it == guiTextures.end()) return;
188
189 auto & texture = it->second;
190
191 if (!texture) return;
192
193 sz.height = texture->description.height;
194 sz.width = texture->description.width;
195}
196
197namespace Cogs
198{
199 namespace Core
200 {
201 bool hasRadius(const litehtml::border_radiuses & radius)
202 {
203 return radius.bottom_left_x != 0;
204 }
205
206 float getBorderWidth(const litehtml::borders & borders)
207 {
208 int width = borders.left.width;
209
210 if (borders.right.width != width ||
211 borders.bottom.width != width ||
212 borders.top.width != width) {
213 LOG_WARNING(logger, "Multiple border widths not supported.");
214 }
215
216 return static_cast<float>(width);
217 }
218
219 int calculateSegments(float radius)
220 {
221 return radius > 1.0f ? 16 : -1;
222 }
223
224 void createRoundedPath(const ImVec2 & a, const ImVec2 & b, const litehtml::border_radiuses & radius, float width = 1.0f)
225 {
226 auto imguiDrawList = ImGui::GetWindowDrawList();
227
228 const float halfPi = glm::half_pi<float>();
229 const float halfWidth = width * 0.5f;
230
231 const float r0 = glm::max(0.0f, radius.top_left_x - halfWidth);
232 const float r1 = glm::max(0.0f, radius.top_right_x - halfWidth);
233 const float r2 = glm::max(0.0f, radius.bottom_right_x - halfWidth);
234 const float r3 = glm::max(0.0f, radius.bottom_left_x - halfWidth);
235
236 imguiDrawList->PathArcTo(ImVec2(a.x + r0, a.y + r0), r0, 2 * halfPi, 3 * halfPi, calculateSegments(r0));
237 imguiDrawList->PathArcTo(ImVec2(b.x - r1, a.y + r1), r1, 3 * halfPi, 4 * halfPi, calculateSegments(r1));
238 imguiDrawList->PathArcTo(ImVec2(b.x - r2, b.y - r2), r2, 0, halfPi, calculateSegments(r2));
239 imguiDrawList->PathArcTo(ImVec2(a.x + r3, b.y - r3), r3, halfPi, 2 * halfPi, calculateSegments(r3));
240 }
241 }
242}
243
244void Cogs::Core::GuiContainer::draw_background(litehtml::uint_ptr /*hdc*/, const litehtml::background_paint & bg)
245{
246 ImVec2 p0 = toImVec2(bg.origin_box.left(), bg.origin_box.top());
247 ImVec2 p1 = toImVec2(bg.origin_box.right(), bg.origin_box.bottom());
248
249 ImVec4 color = toImVec4(bg.color);
250
251 auto imguiDrawList = ImGui::GetWindowDrawList();
252
253 const float borderWidth = static_cast<float>(bg.origin_box.x - bg.border_box.x);
254 const float radius = static_cast<float>(bg.border_radius.bottom_left_x) - borderWidth;
255
256 const auto u32Color = ImGui::ColorConvertFloat4ToU32(color);
257
258 if (bg.image.empty()) {
259 if (hasRadius(bg.border_radius)) {
260 createRoundedPath(p0, p1, bg.border_radius, borderWidth * 2);
261 imguiDrawList->PathFillConvex(u32Color);
262 } else {
263 imguiDrawList->AddRectFilled(p0, p1, u32Color);
264 }
265 } else {
266 auto name = StringView(bg.image);
267 auto & texture = guiTextures[name.hash()];
268 auto renderTexture = renderContext->resources->getRenderTexture(texture);
269
270 if (renderTexture) {
271 const auto texId = ImTextureID(renderTexture->textureHandle.handle);
272
273 if (radius != 0) {
274 imguiDrawList->PushTextureID(texId);
275 createRoundedPath(p0, p1, bg.border_radius, borderWidth * 2);
276 imguiDrawList->PathFillConvex(u32Color);
277 imguiDrawList->PopTextureID();
278 } else {
279 imguiDrawList->AddImage(texId, p0, p1, ImVec2(), ImVec2(1, 1), u32Color);
280 }
281 } else {
282 imguiDrawList->AddRectFilled(p0, p1, u32Color, radius);
283 }
284 }
285}
286
287void Cogs::Core::GuiContainer::draw_borders(litehtml::uint_ptr /*hdc*/, const litehtml::borders & borders, const litehtml::position & pos, bool /*root*/)
288{
289 //TODO: Fix me!
290
291 if (borders.left.style == litehtml::border_style_none) return;
292
293 if (borders.left.style != litehtml::border_style_solid) {
294 LOG_WARNING(logger, "Only solid border style supported.");
295 }
296
297 auto width = getBorderWidth(borders);
298
299 if (width == 0) return;
300
301 ImVec4 color = toImVec4(borders.left.color);
302
303 const float halfWidth = width / 2;
304 const float offset = 0.5f;
305
306 const auto a = ImVec2((float)pos.left() + halfWidth + offset, (float)pos.top() + halfWidth + offset);
307 const auto b = ImVec2((float)pos.right() - halfWidth - offset, (float)pos.bottom() - halfWidth - offset);
308
309 auto imguiDrawList = ImGui::GetWindowDrawList();
310
311 if (!hasRadius(borders.radius)) {
312 imguiDrawList->PathRect(a, b);
313 } else {
314 createRoundedPath(a, b, borders.radius, width);
315 }
316
317 imguiDrawList->PathStroke(ImGui::ColorConvertFloat4ToU32(color), true, width);
318}
319
320void Cogs::Core::GuiContainer::on_load(const std::shared_ptr<litehtml::document> & document)
321{
322 // FIXME: 2017-08-25 chrisdy. This fails if document is missing <head>, I'm guessing we sould search for <body>?
323 auto attribute = document->root()->get_child(2)->get_attr("onload");
324
325 if (attribute) {
326 auto scriptContext = context->scriptSystem->getScriptContext(entity->getComponent<ScriptComponent>(), ScriptFlags::JavaScript);
327 scriptContext->eval(attribute);
328 }
329}
330
331namespace Cogs
332{
333 namespace Core
334 {
335 ScriptObject * createElement(const ScriptContextHandle & scriptContext, litehtml::document * document, const litehtml::element::ptr & found)
336 {
337 if (found->get_userdata()) {
338 auto storedElement = (ScriptObject *)found->get_userdata();
339
340 // The object/element may have been finalized/recycled in between usages. Check if it still
341 // represents the same object.
342 if (storedElement->generation == found->get_userdata_generation()) {
343 return storedElement;
344 }
345 }
346
347 auto htmlElement = scriptContext->createObject(ScriptValueType::Object, (void *)"HTMLElement");
348
349 scriptContext->addProperty(htmlElement, "innerHTML", ScriptValueType::String,
350 [=](ScriptObject *, const ScriptArgs & /*args*/) -> ScriptObject *
351 {
352 std::string text;
353 found->get_text(text);
354 return scriptContext->createObject(ScriptValueType::String, (void *)text.c_str());
355 },
356 [=](ScriptObject *, const ScriptArgs & args) -> ScriptObject *
357 {
358 found->set_inner_html(args[0].stringValue.c_str());
359 found->get_document()->container()->invalidate_layout(found->get_document().get());
360 return nullptr;
361 });
362
363 scriptContext->addProperty(htmlElement, "className", ScriptValueType::String,
364 [=](ScriptObject *, const ScriptArgs & /*args*/) -> ScriptObject *
365 {
366 return scriptContext->createObject(ScriptValueType::String, (void *)found->get_attr("class"));
367 },
368 [=](ScriptObject *, const ScriptArgs & args) -> ScriptObject *
369 {
370 found->set_attr("class", args[0].stringValue.c_str());
371 found->apply_stylesheet(document->get_styles());
372 found->parse_styles();
373
374 return nullptr;
375 });
376
377 auto styles = scriptContext->createObjectProxy(
378 [=](ScriptObject * /*target*/, const StringView & key) -> ScriptObject *
379 {
380 auto style = found->get_style_property(key.data(), false);
381
382 return style ? scriptContext->createObject(ScriptValueType::String, (void *)style) : nullptr;
383 },
384 [=](ScriptObject * /*target*/, const StringView & key, const ScriptArg & value) -> bool
385 {
386 litehtml::style style;
387 style.add_property(key.data(), value.stringValue.data(), nullptr, true);
388
389 found->add_style(style);
390
391 found->parse_styles();
392
393 found->get_document()->container()->invalidate_layout(found->get_document().get());
394
395 return true;
396 });
397
398 scriptContext->setProperty(htmlElement, "styles", styles);
399
400 auto attributes = scriptContext->createObjectProxy(
401 [=](ScriptObject * /*target*/, const StringView & key) -> ScriptObject *
402 {
403 return scriptContext->createObject(ScriptValueType::String, (void *)found->get_attr(key.data()));
404 },
405 [=](ScriptObject * /*target*/, const StringView & key, const ScriptArg & value) -> bool
406 {
407 found->set_attr(key.data(), value.stringValue.data());
408 return true;
409 });
410
411 scriptContext->setProperty(htmlElement, "attributes", attributes);
412
413 scriptContext->setProperty(htmlElement, "id", found->get_attr("id"));
414
415 found->set_userdata(htmlElement, htmlElement->generation);
416
417 return htmlElement;
418 }
419
420 void createDocument(const ScriptContextHandle & scriptContext, litehtml::document * document, const litehtml::element::ptr & /*el*/)
421 {
422 auto doc = scriptContext->createObject(ScriptValueType::Object);
423 scriptContext->setProperty(nullptr, "document", doc);
424 scriptContext->addFunction(doc, "getElementById", ScriptValueType::Object, { ScriptValueType::String },
425 [&, document, scriptContext](ScriptObject *, const ScriptArgs & args) -> ScriptObject *
426 {
427 auto root = document->root();
428 auto found = findElement(root, args[0].stringValue);
429
430 if (!found) return nullptr;
431
432 return createElement(scriptContext, document, found);
433 });
434 }
435
436 void dispatchMouseEvent(ScriptContextHandle scriptContext, const litehtml::element::ptr & el, const StringView & eventName, const StringView & attributeName, int x, int y)
437 {
438 auto attribute = el->get_attr(attributeName.data());
439
440 if (attribute) {
441 scriptContext->eval(attribute);
442 }
443
444 // Only emit event for elements that have script representations.
445 if (!el->get_userdata()) return;
446
447 auto elementObject = createElement(scriptContext, el->get_document().get(), el);
448 auto eventObject = scriptContext->createObject(ScriptValueType::Object, (void *)"MouseEvent");
449
450 scriptContext->setProperty(nullptr, "_event", eventObject);
451
452 auto clientCoords = el->get_placement();
453
454 //TODO: Include button id.
455 scriptContext->setProperty(eventObject, "button", 0.0);
456 scriptContext->setProperty(eventObject, "target", elementObject);
457 scriptContext->setProperty(eventObject, "type", eventName);
458 scriptContext->setProperty(eventObject, "screenX", x);
459 scriptContext->setProperty(eventObject, "screenY", y);
460 scriptContext->setProperty(eventObject, "clientX", x - clientCoords.x);
461 scriptContext->setProperty(eventObject, "clientY", y - clientCoords.y);
462
463 scriptContext->callFunction(elementObject, "_dispatchEvent", eventObject);
464
465 scriptContext->setProperty(nullptr, "_event", nullptr);
466 }
467
468 ScriptContextHandle getScriptContext(Context * context, Entity * entity)
469 {
470 return context->scriptSystem->getScriptContext(entity->getComponent<ScriptComponent>(), ScriptFlags::JavaScript);
471 }
472 }
473}
474
475void Cogs::Core::GuiContainer::on_mouse_enter(const litehtml::element::ptr & el)
476{
477 dispatchMouseEvent(getScriptContext(context, entity), el, "mouseover", "onmouseover", 0, 0);
478}
479
480void Cogs::Core::GuiContainer::on_mouse_move(const litehtml::element::ptr & el, int x, int y)
481{
482 dispatchMouseEvent(getScriptContext(context, entity), el, "mousemove", "onmousemove", x, y);
483}
484
485void Cogs::Core::GuiContainer::on_mouse_leave(const litehtml::element::ptr & el)
486{
487 dispatchMouseEvent(getScriptContext(context, entity), el, "mouseout", "onmouseout", 0, 0);
488}
489
490void Cogs::Core::GuiContainer::on_click(const litehtml::element::ptr & el)
491{
492 dispatchMouseEvent(getScriptContext(context, entity), el, "click", "onclick", 0, 0);
493}
494
495void Cogs::Core::GuiContainer::import_css(litehtml::tstring & text, const litehtml::tstring & url, litehtml::tstring & /*baseurl*/)
496{
497 text = context->resourceStore->getResourceContentString(url, ResourceStoreFlags::NoCachedContent);
498}
499
500void Cogs::Core::GuiContainer::on_anchor_click(const litehtml::tchar_t * url, const litehtml::element::ptr & el)
501{
502 if (std::strstr(url, "cogs:") == url) {
503 Network::Message::Ptr message = Network::Message::allocate(GUIEXT_OnClickMessageID);
504
505 message->writeString(url + 5);
506 sendMessage(message);
507 }
508 else {
509 auto clicked = el->get_attr("onclick");
510
511 if (clicked) {
512 auto scriptContext = context->scriptSystem->getScriptContext(entity->getComponent<ScriptComponent>(), ScriptFlags::JavaScript);
513
514 scriptContext->eval(std::string(clicked) + "();");
515 }
516 }
517}
518
519void Cogs::Core::GuiContainer::get_client_rect(litehtml::position & client) const
520{
521 client.width = (int)size.x;
522 client.height = (int)size.y;
523 client.x = 0;
524 client.y = 0;
525}
526
527std::shared_ptr<litehtml::element> Cogs::Core::GuiContainer::create_element(const litehtml::tchar_t * /*tag_name*/, const litehtml::string_map & /*attributes*/, const std::shared_ptr<litehtml::document>& /*doc*/)
528{
529 return nullptr;
530}
531
532void Cogs::Core::GuiContainer::get_media_features(litehtml::media_features & media) const
533{
534 media.width = (int)size.x;
535 media.height = (int)size.y;
536 media.device_width = (int)size.x;
537 media.device_height = (int)size.y;
538 media.type = litehtml::media_type_screen;
539 media.resolution = 96;
540}
541
542void Cogs::Core::GuiContainer::execute_script(litehtml::document * document, const litehtml::element::ptr el)
543{
544 if (!entity) return;
545
546 auto scriptElement = (litehtml::el_script *)el.get();
547
548 if (scriptElement->get_text()) {
549 auto scriptContext = context->scriptSystem->getScriptContext(entity->getComponent<ScriptComponent>(), ScriptFlags::JavaScript);
550
551 createDocument(scriptContext, document, el);
552
553 auto source = scriptElement->get_attr("src", nullptr);
554 if (source) {
555 auto content = context->resourceStore->getResourceContentString(source);
556
557 scriptContext->addScript(content);
558 } else {
559 scriptContext->addScript(scriptElement->get_text());
560 }
561 }
562}
563
564void Cogs::Core::GuiContainer::invalidate_layout(litehtml::document * document)
565{
566 if (entity) {
567 invalidated = true;
568 } else {
569 auto entity = (Entity *)document->get_userdata();
570
571 if (entity) {
572 auto guiComponent = entity->getComponent<GuiComponent>();
573 auto * guiSystem = static_cast<GuiSystem*>(context->getExtensionSystem(GuiSystem::getTypeId()));
574 assert(guiSystem);
575
576 auto & data = guiSystem->getData(guiComponent);
577
578 data.invalidated = true;
579 }
580 }
581}
582
583void Cogs::Core::GuiContainer::initialize(RenderTaskContext * renderContext, InspectorGuiRenderer * guiRenderer)
584{
585 this->renderContext = renderContext;
586 context = renderContext->context;
587 this->inspectorGuiRenderer = guiRenderer;
588}
589
590void Cogs::Core::GuiContainer::pushState(ComponentModel::Entity * entity, glm::vec2 size)
591{
592 assert(renderContext && inspectorGuiRenderer && context && "Gui container not ready.");
593
594 invalidated = false;
595 this->entity = entity;
596 this->size = size;
597 this->scriptComponent = entity->getComponentHandle<ScriptComponent>();
598}
599
600void Cogs::Core::GuiContainer::popState()
601{
602 entity = nullptr;
603 scriptComponent = ComponentHandle::Empty();
604}
605
606void Cogs::Core::GuiContainer::clear()
607{
608 guiTextures.clear();
609}
Container for components, providing composition of dynamic entities.
Definition: Entity.h:18
T * getComponent() const
Get a pointer to the first component implementing the given type in the entity.
Definition: Entity.h:35
static Reflection::TypeId getTypeId()
Get the type id of the component type used by the system.
Log implementation class.
Definition: LogManager.h:139
static Ptr allocate(uint32_t id)
Retrieves an existing message from the pool, or creates a new one if the pool is empty.
Definition: Message.cpp:9
static constexpr size_t NoPosition
No position.
Definition: StringView.h:43
@ NoCachedContent
Never use cached data.
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
Contains all Cogs related functionality.
Definition: FieldSetter.h:23
constexpr size_t hashSequence(const T &t, const U &u)
Hash the last two items in a sequence of objects.
Definition: HashSequence.h:8
static ComponentHandle Empty()
Returns an empty, invalid handle. Will evaluate to false if tested against using operator bool().
Definition: Component.h:119