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