diff --git a/.gitmodules b/.gitmodules index 042e346..4bbbb08 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,6 @@ [submodule "nanosvg"] path = nanosvg url = https://github.com/memononen/nanosvg.git -[submodule "katana-parser"] - path = katana-parser - url = https://github.com/hackers-painters/katana-parser.git +[submodule "libcss"] + path = libcss + url = https://github.com/Unix4ever/libcss.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4daadff..2656e15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ option(BUILD_SAMPLES "build ImVue samples" OFF) option(BUILD_IMGUI_FROM_SUBMODULE "build Dear ImGui from submodule" ON) option(BUILD_LUA_BINDINGS "build Lua bindings" ON) option(BUILD_CSS_STYLING "build css styling" ON) -option(BUILD_KATANA_FROM_SUBMODULE "build libcss using submodule" ON) +option(BUILD_CSS_FROM_SUBMODULE "build libcss using submodule" ON) option(BUILD_TESTS "build unit test for the project" OFF) option(IMVUE_NO_EXCEPTIONS "disable raising exceptions. Use log errors instead" OFF) option(IMVUE_USE_LUAJIT "use luajit" OFF) @@ -40,14 +40,13 @@ if(BUILD_LUA_BINDINGS) set(ADDITIONAL_SOURCES src/lua/script.cpp ${ADDITIONAL_SOURCES}) endif(BUILD_LUA_BINDINGS) -# disable it for now. It's not used, and does not build on windows -#if(BUILD_KATANA_FROM_SUBMODULE) -# set(KATANA_INCLUDE_DIRS katana-parser/src/) -# set(KATANA_LIBRARY katana) -# include_directories(${KATANA_INCLUDE_DIRS}) -# file(GLOB_RECURSE sources katana-parser/src/*.c) -# add_library(${KATANA_LIBRARY} STATIC ${sources}) -#endif(BUILD_KATANA_FROM_SUBMODULE) +if(BUILD_CSS_FROM_SUBMODULE) + add_subdirectory(libcss) + include_directories(libcss/libcss/include) + include_directories(libcss/libwapcaplet/include) + include_directories(libcss/libparserutils/include) + set(ADDITIONAL_LIBS ${ADDITIONAL_LIBS} css wapcaplet parserutils) +endif(BUILD_CSS_FROM_SUBMODULE) if(BUILD_TESTS) find_package(GTest) @@ -73,7 +72,10 @@ add_library(${LIB_NAME} STATIC src/imvue_element.cpp src/imvue_script.cpp src/imvue_context.cpp + src/imvue_style.cpp + src/imvue_layout.cpp src/imstring.cpp + src/css/select.cpp ${ADDITIONAL_SOURCES}) set(IMGUI_SOURCES diff --git a/README.md b/README.md index 67b699b..70b2e41 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,15 @@ You can either define them using `lua` syntax or `imv` xml file. `imv` files search path is configured using `package.imvpath` variable. +CSS Styles Support +------------------ + +ImVue supports CSS styling and HTML syntax to some extent. + +[Styled document example](samples/simple/styled.xml) in action: + + + Vue Special Syntax ------------------ @@ -65,7 +74,6 @@ Vue Special Syntax - Getting `event.target` in event handlers. - Changing element properties using refs (RO access only). - `${}` eval syntax in attributes. -- CSS styles. - V8 JS integration. ### Lua Implementation Specifics @@ -76,33 +84,18 @@ there is no limitation on what you can do. Globals are also available. Besides, it will create reactive listeners for each field that was used in the evaluation. -Benchmarks ----------- - -Rendering 1000 windows with a single button. - -``` ------------------------------------------------------------------------------- -Benchmark Time CPU Iterations ------------------------------------------------------------------------------- -ImVueBenchmark/RenderImGuiLuaStatic 1968641 ns 1963054 ns 3493 # lua bindings -ImVueBenchmark/RenderImGuiStatic 1894949 ns 1894921 ns 3654 # vanilla ImGui -ImVueBenchmark/RenderImVueScripted 1909691 ns 1909661 ns 3615 # imgui with lua scripting enabled -ImVueBenchmark/RenderImVueStatic 1888849 ns 1888819 ns 3667 # static xml template -``` - Dependencies ------------ - ImGui without any modifications. - NanoSVG is used for image rendering. Using customized rasterizer. - RapidXML is used for XML parsing. +- customized version of [LibCSS](https://github.com/Unix4ever/libcss). Optional Core Dependecies ------------------------- - Lua 5.1+/LuaJIT 2.0.5+ - adds script interpreter. -- katana-parser allows reading CSS to change widgets style. Samples Dependencies -------------------- @@ -119,7 +112,3 @@ Build 1. Run: `make build` - -### Using conan - -TODO diff --git a/demo.gif b/demo.gif new file mode 100644 index 0000000..b6a287f Binary files /dev/null and b/demo.gif differ diff --git a/imgui b/imgui index 4e56de7..dc66f83 160000 --- a/imgui +++ b/imgui @@ -1 +1 @@ -Subproject commit 4e56de757c76c9d713d4f05a0f6adf82d1aac068 +Subproject commit dc66f83db8462e7837aec60fef86c025ac4c485b diff --git a/katana-parser b/katana-parser deleted file mode 160000 index 499118d..0000000 --- a/katana-parser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 499118d32c387a893fdc9dda2cb95eee524bdb9b diff --git a/libcss b/libcss new file mode 160000 index 0000000..34258bb --- /dev/null +++ b/libcss @@ -0,0 +1 @@ +Subproject commit 34258bb325c964500e0ae019471bb68ff664dbf6 diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 657a8b6..2d40a81 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -15,10 +15,10 @@ add_library(imgui_sdl2 set(LIBS imgui + ${ADDITIONAL_LIBS} ${OPENGL_LIBRARIES} ${GLEW_LIBRARIES} - #${SDL2_LIBRARIES} - ${LUA_LIBRARIES} + ${SDL2_LIBRARIES} SDL2::SDL2main SDL2::SDL2-static ) diff --git a/samples/fonts/MaterialIcons-Regular.ttf b/samples/fonts/MaterialIcons-Regular.ttf new file mode 100644 index 0000000..7015564 Binary files /dev/null and b/samples/fonts/MaterialIcons-Regular.ttf differ diff --git a/samples/fonts/times new roman.ttf b/samples/fonts/times new roman.ttf new file mode 100644 index 0000000..51261a0 Binary files /dev/null and b/samples/fonts/times new roman.ttf differ diff --git a/samples/simple/components/window.imv b/samples/simple/components/window.imv new file mode 100644 index 0000000..1aef28c --- /dev/null +++ b/samples/simple/components/window.imv @@ -0,0 +1,149 @@ + + + + + + + + {{ self.title or self.name }} + + + + + + + + + + + + diff --git a/samples/simple/main.cpp b/samples/simple/main.cpp index ea00b2f..1a5b1d3 100644 --- a/samples/simple/main.cpp +++ b/samples/simple/main.cpp @@ -104,7 +104,7 @@ int main(int argc, char** argv) ImGui::StyleColorsDark(); //ImGui::StyleColorsLight(); - float scale = 1.0f; + float scale = 1.5f; ImGuiStyle& style = ImGui::GetStyle(); style.ScaleAllSizes(scale); @@ -145,6 +145,8 @@ int main(int argc, char** argv) new OpenGL2TextureManager() ); + ctx->scale = ImVec2(scale, scale); + ImVue::Document document(ctx); const char* page = "simple.xml"; if(argc == 2) { diff --git a/samples/simple/simple.xml b/samples/simple/simple.xml index 748b513..3e9a07e 100644 --- a/samples/simple/simple.xml +++ b/samples/simple/simple.xml @@ -1,3 +1,14 @@ + + right click @@ -13,7 +24,6 @@ add more elements - clear list Dynamic list @@ -22,6 +32,15 @@ console run script submitted input: {{ if self.inputText then return self.inputText else return 'none' end}} + Tab bar + + + it works + + + + + diff --git a/samples/simple/styled.xml b/samples/simple/styled.xml new file mode 100644 index 0000000..62a6be7 --- /dev/null +++ b/samples/simple/styled.xml @@ -0,0 +1,311 @@ + + + + + + + Block element (div) + Inline element (span) + Another span that is using Times New Roman font + + First/last Elements + + + + + {{ key }} + + + + + Styled Buttons Demo + BUTTON + + + + + + {{if self.toggle then return self.heartActive else return self.heartInactive end}} + APPROVED + Forms + + Styled input example: + + Username: + + Password: + + Other kinds of inputs: + + + + Radio buttons: + + + + + + + Range input: + + + + Checkboxes sharing the same model: + + + + + Choices: + {{c}} + No choices yet + + + + + + + This input has custom style + + custom + looking + buttons + + + + + diff --git a/samples/simple/widgets.xml b/samples/simple/widgets.xml index 2c147c0..12d1037 100644 --- a/samples/simple/widgets.xml +++ b/samples/simple/widgets.xml @@ -1,3 +1,9 @@ + + increase counter diff --git a/src/css/select.cpp b/src/css/select.cpp new file mode 100644 index 0000000..5f7ca93 --- /dev/null +++ b/src/css/select.cpp @@ -0,0 +1,723 @@ +/* +Copyright (c) 2019 Artem Chernyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +extern "C" { +#ifndef restrict +#define restrict +#endif +#include +} + +#include "css/select.h" +#include "imvue_element.h" + +// This module implements libcss selector interface + +namespace ImVue { + + // special element iterator that takes in account PSEUDO_ELEMENT + class ElementIterator { + public: + + int index; + + ElementIterator(ContainerElement* target, int i = 0) + : index(i) + { + setActiveElement(target); + } + + Element* prev() + { + if(index > -1) { + index--; + onIndexChange(); + } + + return current(); + } + + Element* next() + { + if(index < (int)mChildren.size()) { + index++; + onIndexChange(); + } + + return current(); + } + + Element* current() + { + if(index >= 0 && index < (int)mChildren.size()) { + return mChildren[index]; + } + + return NULL; + } + + private: + + void onIndexChange() + { + Element* el = current(); + if(el) { + if(el->isPseudoElement()) { + setActiveElement(static_cast(el)); + index = 0; + } + } else if(mElement->isPseudoElement() && mElement->getParent() && index != 0 && mElement->index > 0) { + int direction = index / abs(index); + index = (int)mElement->index + direction; + setActiveElement(mElement->getParent()); + } + } + + void setActiveElement(ContainerElement* element) + { + mElement = element; + mChildren = element->getChildren(); + } + + Element* mElement; + Element::Elements mChildren; + }; + + + lwc_string* lwc_string_from_char(const char* s) { + lwc_string* res = NULL; + lwc_intern_string(s, strlen(s), &res); + return res; + } + + // resolve + + css_error resolve_url(void *pw, + const char *base, lwc_string *rel, lwc_string **abs) + { + (void)pw; + (void)base; + + /* About as useless as possible */ + *abs = rel; assert(abs != NULL); (*abs)->refcnt++; + + return CSS_OK; + } + + css_error resolve_font(void *pw, + lwc_string *name, css_system_font *system_font) + { + UNUSED(pw); + UNUSED(name); + UNUSED(system_font); + return CSS_INVALID; + } + + // select handlers + + css_error node_name(void *pw, void *n, css_qname *qname) + { + UNUSED(pw); + + const char* name = ((Element*)n)->getType(); + lwc_intern_string(name, strlen(name), &qname->name); + + return CSS_OK; + } + + css_error node_classes(void *pw, void *n, + lwc_string ***classes, uint32_t *n_classes) + { + UNUSED(pw); + Element* element = (Element*)n; + ImVector& classList = element->style()->getClasses(); + + *classes = &classList.Data[0]; + *n_classes = classList.size(); + for(int i = 0; i < classList.size(); i++) { + (*classes)[i] = lwc_string_ref(classList[i]); + } + + return CSS_OK; + } + + css_error node_id(void *pw, void *n, lwc_string **id) + { + UNUSED(pw); + UNUSED(n); + + const char* elementID = ((Element*)n)->id; + if(elementID == NULL) { + *id = NULL; + return CSS_OK; + } + lwc_intern_string(elementID, strlen(elementID), id); + return CSS_OK; + } + + + css_error named_ancestor_node(void *pw, void *n, + const css_qname *qname, + void **ancestor) + { + UNUSED(pw); + + Element* element = ((Element*)n)->getParent(); + *ancestor = NULL; + while(element) { + if(ImStricmp(element->getType(), lwc_string_data(qname->name)) == 0) { + *ancestor = element; + break; + } + + element = element->getParent(); + } + return CSS_OK; + } + + css_error named_parent_node(void *pw, void *n, + const css_qname *qname, + void **parent) + { + UNUSED(pw); + Element* parentElement = ((Element*)n)->getParent(); + if(parentElement && ImStricmp(parentElement->getType(), lwc_string_data(qname->name)) == 0) { + *parent = parentElement; + } else { + *parent = NULL; + } + + return CSS_OK; + } + + css_error named_generic_sibling_node(void *pw, void *n, + const css_qname *qname, + void **sibling) + { + UNUSED(pw); + *sibling = NULL; + Element* element = (Element*)n; + ContainerElement* parent = element->getParent(); + if(!parent || !element->enabled) { + return CSS_OK; + } + + ElementIterator iter(parent, element->index); + for(iter.prev(); iter.current(); iter.prev()) { + Element* previous = iter.current(); + if(!previous->visible()) { + continue; + } + + bool match = false; + lwc_error status = lwc_string_caseless_isequal( + qname->name, lwc_string_from_char(previous->getType()), &match); + UNUSED(status); + IM_ASSERT(status == lwc_error_ok); + if (match == true) { + *sibling = (void*)previous; + break; + } + } + return CSS_OK; + } + + css_error named_sibling_node(void *pw, void *n, + const css_qname *qname, + void **sibling) + { + UNUSED(pw); + Element* element = (Element*)n; + + *sibling = NULL; + ContainerElement* parent = element->getParent(); + if(!parent || !element->enabled) { + return CSS_OK; + } + + ElementIterator iter(parent, element->index); + Element* previous = iter.prev(); + if (previous) { + if(!previous->visible()) { + return CSS_OK; + } + bool match = false; + + lwc_error status = lwc_string_caseless_isequal( + qname->name, lwc_string_from_char(previous->getType()), &match); + UNUSED(status); + IM_ASSERT(status == lwc_error_ok); + if (match == true) + *sibling = (void*)previous; + } + + return CSS_OK; + } + + css_error parent_node(void *pw, void *n, void **parent) + { + UNUSED(pw); + Element* p = ((Element*)n)->getParent(); + if(p && p->isPseudoElement()) { + p = p->getParent(); + } + + *parent = p; + return CSS_OK; + } + + css_error sibling_node(void *pw, void *n, void **sibling) + { + UNUSED(pw); + Element* element = (Element*)n; + *sibling = NULL; + if(element->index == 0) { + return CSS_OK; + } + + ContainerElement* parent = element->getParent(); + if(!parent || !element->enabled) { + return CSS_OK; + } + + *sibling = ElementIterator(parent, element->index).prev(); + return CSS_OK; + } + + css_error node_has_name(void *pw, void *n, + const css_qname *qname, + bool *match) + { + lwc_string *node = lwc_string_from_char(((Element*)n)->getType()); + + UNUSED(pw); + lwc_error status = lwc_string_caseless_isequal(node, qname->name, match); + UNUSED(status); + IM_ASSERT(status == + lwc_error_ok); + return CSS_OK; + } + + css_error node_has_class(void *pw, void *n, + lwc_string *name, + bool *match) + { + UNUSED(pw); + *match = ((Element*)n)->hasClass(lwc_string_data(name)); + return CSS_OK; + } + + css_error node_has_id(void *pw, void *n, + lwc_string *name, + bool *match) + { + UNUSED(pw); + *match = ImStricmp(((Element*)n)->id, lwc_string_data(name)) == 0; + return CSS_OK; + } + + css_error node_has_attribute(void *pw, void *n, + const css_qname *qname, + bool *match) + { + UNUSED(pw); + *match = ((Element*)n)->hasAttribute(lwc_string_data(qname->name)); + return CSS_OK; + } + + css_error node_has_attribute_equal(void *pw, void *n, + const css_qname *qname, + lwc_string *expected, + bool *match) + { + UNUSED(pw); + char* actual = NULL; + if(((Element*)n)->evalAttribute(lwc_string_data(qname->name), &actual) && actual) { + *match = ImStricmp(lwc_string_data(expected), actual) == 0; + } else { + *match = false; + } + ImGui::MemFree(actual); + return CSS_OK; + } + + css_error node_has_attribute_dashmatch(void *pw, void *n, + const css_qname *qname, + lwc_string *value, + bool *match) + { + UNUSED(pw); + UNUSED(n); + UNUSED(qname); + UNUSED(value); + *match = false; + return CSS_OK; + } + + css_error node_has_attribute_includes(void *pw, void *n, + const css_qname *qname, + lwc_string *value, + bool *match) + { + UNUSED(pw); + UNUSED(n); + UNUSED(qname); + UNUSED(value); + *match = false; + return CSS_OK; + } + + css_error node_has_attribute_prefix(void *pw, void *n, + const css_qname *qname, + lwc_string *value, + bool *match) + { + UNUSED(pw); + UNUSED(n); + UNUSED(qname); + UNUSED(value); + *match = false; + return CSS_OK; + } + + css_error node_has_attribute_suffix(void *pw, void *n, + const css_qname *qname, + lwc_string *value, + bool *match) + { + UNUSED(pw); + UNUSED(n); + UNUSED(qname); + UNUSED(value); + *match = false; + return CSS_OK; + } + + css_error node_has_attribute_substring(void *pw, void *n, + const css_qname *qname, + lwc_string *value, + bool *match) + { + UNUSED(pw); + UNUSED(n); + UNUSED(qname); + UNUSED(value); + *match = false; + return CSS_OK; + } + + css_error node_is_first_child(void *pw, void *n, bool *match) + { + UNUSED(pw); + Element* element = (Element*)n; + ContainerElement* parent = element->getParent(); + *match = element->enabled && (!parent || ElementIterator(parent, element->index).prev() == NULL); + return CSS_OK; + } + + css_error node_is_root(void *pw, void *n, bool *match) + { + UNUSED(pw); + Element* parent = ((Element*)n)->getParent(); + *match = parent == NULL || ImStricmp(parent->getType(), "template") == 0; + return CSS_OK; + } + + css_error node_count_siblings(void *pw, void *n, + bool same_name, bool after, int32_t *count) + { + UNUSED(pw); + int cnt = 0; + *count = 0; + Element* element = (Element*)n; + ContainerElement* parent = element->getParent(); + if(!parent || !element->enabled) { + return CSS_OK; + } + + ElementIterator iter(parent, element->index); + const char* name = element->getType(); + + while(true) { + Element* el = NULL; + if(after) { + el = iter.next(); + } else { + el = iter.prev(); + } + + if(!el) { + break; + } + + if(!el->visible()) { + continue; + } + + if (!same_name || ImStricmp(name, el->getType()) == 0) + cnt++; + } + + *count = cnt; + return CSS_OK; + } + + css_error node_is_empty(void *pw, void *n, bool *match) + { + UNUSED(pw); + Element* element = (Element*)n; + if(!element->isContainer()) { + *match = false; + } else { + *match = ElementIterator(static_cast(element), 0).current() == 0; + } + return CSS_OK; + } + + css_error node_is_link(void *pw, void *n, bool *match) + { + UNUSED(pw); + *match = ((Element*)n)->hasState(Element::LINK); + return CSS_OK; + } + + css_error node_is_visited(void *pw, void *n, bool *match) + { + UNUSED(pw); + *match = ((Element*)n)->hasState(Element::VISITED); + return CSS_OK; + } + + css_error node_is_hover(void *pw, void *n, bool *match) + { + UNUSED(pw); + *match = ((Element*)n)->hasState(Element::HOVERED); + return CSS_OK; + } + + css_error node_is_active(void *pw, void *n, bool *match) + { + UNUSED(pw); + *match = ((Element*)n)->hasState(Element::ACTIVE); + return CSS_OK; + } + + css_error node_is_focus(void *pw, void *n, bool *match) + { + UNUSED(pw); + *match = ((Element*)n)->hasState(Element::FOCUSED); + return CSS_OK; + } + + css_error node_is_enabled(void *pw, void *n, bool *match) + { + UNUSED(pw); + UNUSED(n); + Element* element = (Element*)n; + *match = !element->hasState(Element::DISABLED) && element->enabled; + return CSS_OK; + } + + css_error node_is_disabled(void *pw, void *n, bool *match) + { + UNUSED(pw); + UNUSED(n); + *match = ((Element*)n)->hasState(Element::DISABLED); + return CSS_OK; + } + + css_error node_is_checked(void *pw, void *n, bool *match) + { + UNUSED(pw); + UNUSED(n); + *match = ((Element*)n)->hasState(Element::CHECKED); + return CSS_OK; + } + + css_error node_is_target(void *pw, void *n, bool *match) + { + // this makes no sense in our usecase + UNUSED(pw); + UNUSED(n); + *match = false; + return CSS_OK; + } + + css_error node_is_lang(void *pw, void *n, + lwc_string *lang, + bool *match) + { + UNUSED(pw); + char* value = NULL; + if(((Element*)n)->evalAttribute("lang", &value) && value) { + *match = strcmp(value, lwc_string_data(lang)) == 0; + } else { + *match = false; + } + return CSS_OK; + } + + css_error node_presentational_hint(void *pw, void *node, + uint32_t *nhints, css_hint **hints) + { + UNUSED(pw); + UNUSED(node); + *nhints = 0; + *hints = NULL; + return CSS_OK; + } + + css_error ua_default_for_property(void *pw, uint32_t property, css_hint *hint) + { + UNUSED(pw); + + if (property == CSS_PROP_COLOR) { + hint->data.color = 0x00000000; + hint->status = CSS_COLOR_INHERIT; + } else if (property == CSS_PROP_FONT_FAMILY) { + hint->data.strings = NULL; + hint->status = CSS_FONT_FAMILY_INHERIT; + } else if (property == CSS_PROP_QUOTES) { + // Not exactly useful + hint->data.strings = NULL; + hint->status = CSS_QUOTES_NONE; + } else if (property == CSS_PROP_VOICE_FAMILY) { + hint->data.strings = NULL; + hint->status = 0; + } else { + return CSS_INVALID; + } + + return CSS_OK; + } + + css_error compute_font_size(void *pw, const css_hint *parent, css_hint *size) + { + static css_hint_length sizes[] = { + { FLTTOFIX(6.75), CSS_UNIT_PX }, + { FLTTOFIX(7.50), CSS_UNIT_PX }, + { FLTTOFIX(9.75), CSS_UNIT_PX }, + { FLTTOFIX(12.0), CSS_UNIT_PX }, + { FLTTOFIX(15.0), CSS_UNIT_PX }, + { FLTTOFIX(18.0), CSS_UNIT_PX }, + { FLTTOFIX(24.0), CSS_UNIT_PX } + }; + + if(size->status == CSS_FONT_SIZE_DIMENSION) + { + return CSS_OK; + } + + ImFont* font = ImGui::GetFont(); + if(!font) { + font = ImGui::GetDefaultFont(); + } + + const css_hint_length* parent_size; + if(!parent) { + size->status = CSS_FONT_SIZE_DIMENSION; + size->data.length = { FLTTOFIX(font->FontSize), CSS_UNIT_PX }; + return CSS_OK; + } + + css_hint_length font_size = { FLTTOFIX(font ? font->FontSize : 0), CSS_UNIT_PX }; + + UNUSED(pw); + + // Grab parent size, defaulting to medium if none + if (parent == NULL) { + parent_size = &sizes[CSS_FONT_SIZE_MEDIUM + 1]; + } else { + assert(parent->data.length.unit != CSS_UNIT_EM); + assert(parent->data.length.unit != CSS_UNIT_EX); + + if(parent->status == CSS_FONT_SIZE_INHERIT) { + parent_size = &font_size; + } + + parent_size = &parent->data.length; + } + + assert(size->status != CSS_FONT_SIZE_INHERIT); + + if (size->status < CSS_FONT_SIZE_LARGER) { + // Keyword -- simple + size->data.length = sizes[size->status - 1]; + } else if (size->status == CSS_FONT_SIZE_LARGER) { + // \todo Step within table, if appropriate + size->data.length.value = + FMUL(parent_size->value, FLTTOFIX(1.2)); + size->data.length.unit = parent_size->unit; + } else if (size->status == CSS_FONT_SIZE_SMALLER) { + // \todo Step within table, if appropriate + size->data.length.value = + FMUL(parent_size->value, FLTTOFIX(1.2)); + size->data.length.unit = parent_size->unit; + } else if (size->data.length.unit == CSS_UNIT_EM || + size->data.length.unit == CSS_UNIT_EX) { + size->data.length.value = + FMUL(size->data.length.value, parent_size->value); + + if (size->data.length.unit == CSS_UNIT_EX) { + size->data.length.value = FMUL(size->data.length.value, + FLTTOFIX(0.6)); + } + + size->data.length.unit = parent_size->unit; + } else if (size->data.length.unit == CSS_UNIT_PCT) { + size->data.length.value = FDIV(FMUL(size->data.length.value, + parent_size->value), FLTTOFIX(100)); + size->data.length.unit = parent_size->unit; + } + + size->status = CSS_FONT_SIZE_DIMENSION; + + return CSS_OK; + } + + css_error set_libcss_node_data(void *pw, void *n, + void *libcss_node_data) + { + UNUSED(pw); + + Element* element = (Element*)n; + if (element->style()->libcssData) { + css_libcss_node_data_handler((css_select_handler*)pw, CSS_NODE_DELETED, + NULL, element, NULL, element->style()->libcssData); + element->style()->libcssData = 0; + } + + element->style()->libcssData = libcss_node_data; + + return CSS_OK; + } + + css_error get_libcss_node_data(void *pw, void *n, + void **libcss_node_data) + { + UNUSED(pw); + + *libcss_node_data = ((Element*)n)->style()->libcssData; + + return CSS_OK; + } +} diff --git a/src/css/select.h b/src/css/select.h new file mode 100644 index 0000000..757a0cb --- /dev/null +++ b/src/css/select.h @@ -0,0 +1,147 @@ +/* +Copyright (c) 2019 Artem Chernyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef __CSS_SELECT__ +#define __CSS_SELECT__ + +#include +#define UNUSED(value) (void)value + +#undef lwc_string_data +#define lwc_string_data(str) (const char*)((str) + 1) + +#undef lwc_string_ref +inline lwc_string* lwc_string_ref(lwc_string* str) { + lwc_string *__lwc_s = str; + assert(__lwc_s != NULL); + __lwc_s->refcnt++; + return __lwc_s; +} + +#undef lwc_string_caseless_isequal +inline lwc_error lwc_string_caseless_isequal(lwc_string* _str1, lwc_string* _str2, bool* _ret) { + lwc_error __lwc_err = lwc_error_ok; + lwc_string *__lwc_str1 = (_str1); + lwc_string *__lwc_str2 = (_str2); + bool *__lwc_ret = (_ret); + if (__lwc_str1->insensitive == NULL) { + __lwc_err = lwc__intern_caseless_string(__lwc_str1); + } + if (__lwc_err == lwc_error_ok && __lwc_str2->insensitive == NULL) { + __lwc_err = lwc__intern_caseless_string(__lwc_str2); + } + if (__lwc_err == lwc_error_ok) + *__lwc_ret = (__lwc_str1->insensitive == __lwc_str2->insensitive); + return __lwc_err; +} + +namespace ImVue { + css_error node_name(void *pw, void *node, + css_qname *qname); + css_error node_classes(void *pw, void *node, + lwc_string ***classes, uint32_t *n_classes); + css_error node_id(void *pw, void *node, + lwc_string **id); + css_error named_ancestor_node(void *pw, void *node, + const css_qname *qname, + void **ancestor); + css_error named_parent_node(void *pw, void *node, + const css_qname *qname, + void **parent); + css_error named_sibling_node(void *pw, void *node, + const css_qname *qname, + void **sibling); + css_error named_generic_sibling_node(void *pw, void *node, + const css_qname *qname, + void **sibling); + css_error parent_node(void *pw, void *node, void **parent); + css_error sibling_node(void *pw, void *node, void **sibling); + css_error node_has_name(void *pw, void *node, + const css_qname *qname, + bool *match); + css_error node_has_class(void *pw, void *node, + lwc_string *name, + bool *match); + css_error node_has_id(void *pw, void *node, + lwc_string *name, + bool *match); + css_error node_has_attribute(void *pw, void *node, + const css_qname *qname, + bool *match); + css_error node_has_attribute_equal(void *pw, void *node, + const css_qname *qname, + lwc_string *value, + bool *match); + css_error node_has_attribute_dashmatch(void *pw, void *node, + const css_qname *qname, + lwc_string *value, + bool *match); + css_error node_has_attribute_includes(void *pw, void *node, + const css_qname *qname, + lwc_string *value, + bool *match); + css_error node_has_attribute_prefix(void *pw, void *node, + const css_qname *qname, + lwc_string *value, + bool *match); + css_error node_has_attribute_suffix(void *pw, void *node, + const css_qname *qname, + lwc_string *value, + bool *match); + css_error node_has_attribute_substring(void *pw, void *node, + const css_qname *qname, + lwc_string *value, + bool *match); + css_error node_is_root(void *pw, void *node, bool *match); + css_error node_count_siblings(void *pw, void *node, + bool same_name, bool after, int32_t *count); + css_error node_is_empty(void *pw, void *node, bool *match); + css_error node_is_link(void *pw, void *node, bool *match); + css_error node_is_visited(void *pw, void *node, bool *match); + css_error node_is_hover(void *pw, void *node, bool *match); + css_error node_is_active(void *pw, void *node, bool *match); + css_error node_is_focus(void *pw, void *node, bool *match); + css_error node_is_enabled(void *pw, void *node, bool *match); + css_error node_is_disabled(void *pw, void *node, bool *match); + css_error node_is_checked(void *pw, void *node, bool *match); + css_error node_is_target(void *pw, void *node, bool *match); + css_error node_is_lang(void *pw, void *node, + lwc_string *lang, bool *match); + css_error node_presentational_hint(void *pw, void *node, + uint32_t *nhints, css_hint **hints); + css_error ua_default_for_property(void *pw, uint32_t property, + css_hint *hint); + css_error compute_font_size(void *pw, const css_hint *parent, + css_hint *size); + css_error set_libcss_node_data(void *pw, void *n, + void *libcss_node_data); + css_error get_libcss_node_data(void *pw, void *n, + void **libcss_node_data); + + css_error resolve_url(void *pw, + const char *base, lwc_string *rel, lwc_string **abs); + + css_error resolve_font(void *pw, + lwc_string *name, css_system_font *system_font); +} + +#endif diff --git a/src/imvue_extensions.h b/src/extras/svg.h similarity index 99% rename from src/imvue_extensions.h rename to src/extras/svg.h index d31d35d..73f03f6 100644 --- a/src/imvue_extensions.h +++ b/src/extras/svg.h @@ -1,5 +1,5 @@ -#ifndef __IMVUE_EXTENSIONS__ -#define __IMVUE_EXTENSIONS__ +#ifndef __IMVUE_SVG__ +#define __IMVUE_SVG__ #include "imvue_element.h" #include "nanosvg.h" diff --git a/src/extras/xhtml.h b/src/extras/xhtml.h new file mode 100644 index 0000000..57ea0f8 --- /dev/null +++ b/src/extras/xhtml.h @@ -0,0 +1,627 @@ +/* +Copyright (c) 2019-2020 Artem Chernyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef __IMVUE_HTML_H__ +#define __IMVUE_HTML_H__ + +#include "imgui.h" +#include "imvue_element.h" + + +namespace ImVue { + + /** + * Top level html container + */ + class Html : public ContainerElement { + + public: + + void renderBody() + { + if(!GetCurrentWindowNoDefault()) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(ImGui::GetIO().DisplaySize)); + if(ImGui::Begin("###htmlWindow", NULL, ImGuiWindowFlags_NoDecoration)) { + ImGui::PopStyleVar(); + ContainerElement::renderBody(); + } else { + ImGui::PopStyleVar(); + } + + ImGui::End(); + } else { + ContainerElement::renderBody(); + } + } + }; + + /** + * Base class for any html node + */ + class HtmlContainer : public ContainerElement { + + public: + + HtmlContainer() + : mID(0) + , mWidth(0) + { + } + + ~HtmlContainer() + { + } + + bool build() + { + bool built = ContainerElement::build(); + if(built) { + Element* parent = mParent; + + while(parent) { + ImU32 id = ImHashStr(parent->getType()); + if(!mID) { + mID = id; + } else { + mID = mID ^ id; + } + parent = parent->getParent(); + } + + } + + return built; + } + + void renderBody() + { + bool useChild = size.y > 0; + bool drawContent = true; + ImVec2 pos = ImGui::GetCursorScreenPos(); + + ImRect pad(ImVec2(0, 0), ImVec2(0, 0)); + if(display != CSS_DISPLAY_INLINE) { + pad.Min = pad.Max = ImGui::GetStyle().FramePadding; + if(padding[0] >= 0) { + pad.Min.x = padding[0]; + } + + if(padding[1] >= 0) { + pad.Min.y = padding[1]; + } + + if(padding[2] >= 0) { + pad.Max.x = padding[2]; + } + + if(padding[3] >= 0) { + pad.Max.y = padding[3]; + } + } + + if(useChild) { + ImVec2 childSize(size.x > 0 ? size.x : mWidth, size.y); + /* + childSize -= ImVec2( + ImMax(mStyle.decoration.thickness[0], mStyle.decoration.thickness[2]), + ImMax(mStyle.decoration.thickness[1], mStyle.decoration.thickness[3]) + );*/ + ImGui::PushStyleColor(ImGuiCol_ChildBg, 0); + drawContent = ImGui::BeginChild(mID + index, childSize, false, ImGuiWindowFlags_NavFlattened | ImGuiWindowFlags_NoDecoration); + ImGui::PopStyleColor(); + ImGui::SetCursorPos(pad.Min); + } else { + ImGui::BeginGroup(); + ImGui::SetCursorScreenPos(pos + pad.Min); + } + + if(drawContent) { + ContainerElement::renderBody(); + } + + if(useChild) { + ImGui::SetCursorScreenPos(ImGui::GetItemRectMax() + pad.Max); + mWidth = ImGui::GetItemRectSize().x + pad.Min.x + pad.Max.x; + ImGui::EndChild(); + } else { + ImGui::SetCursorScreenPos(ImGui::GetItemRectMax() + pad.Max); + ImGui::EndGroup(); + } + } + + private: + + ImU32 mID; + float mWidth; + + }; + + static int InputTextCallback(ImGuiInputTextCallbackData* data) + { + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + // Resize string callback + ImVector* str = (ImVector*)data->UserData; + IM_ASSERT(data->Buf == str->Data); + str->resize(data->BufTextLen); + data->Buf = str->Data; + } + + return 0; + } + + /** + * Text input + */ + class Input : public Element { + + public: + + enum InputType { + BUTTON, + CHECKBOX, + COLOR, + DATE, + DATETIME_LOCAL, + EMAIL, + TFILE, + HIDDEN, + IMAGE, + MONTH, + NUMBER, + PASSWORD, + RADIO, + RANGE, + RESET, + SEARCH, + SUBMIT, + TEL, + TEXT, + TIME, + URL, + WEEK + }; + + Input() + : placeholder(0) + , format(0) + , min(0.0f) + , max(100.0f) + , step(1.0f) + , name(0) + , mValue(0) + , mModel(0) + , mValueUpdated(false) + , mActivate(false) + , mType(TEXT) + { + mBuffer.resize(48); + memset(mBuffer.Data, 0, sizeof(char) * 48); + } + + ~Input() + { + if(placeholder) { + ImGui::MemFree(placeholder); + } + + if(mValue) { + ImGui::MemFree(mValue); + } + + if(mModel) { + ImGui::MemFree(mModel); + } + + if(format) { + ImGui::MemFree(format); + } + + if(name) { + ImGui::MemFree(name); + } + } + + bool build() + { + bool built = Element::build(); + if(built) { + Element* parent = mParent; + + while(parent) { + ImU32 id = ImHashStr(parent->getType()); + if(!mID) { + mID = id; + } else { + mID = mID ^ id; + } + parent = parent->getParent(); + } + + } + + return built; + } + + void renderBody() + { + if(mInvalidFlags & Element::MODEL) { + mInvalidFlags ^= Element::MODEL; + syncModel(); + } + + ImGui::PushID(mID + index); + ImGui::BeginGroup(); + ImVec2 pos = ImGui::GetCursorScreenPos(); + + ImRect pad(ImVec2(0, 0), ImVec2(0, 0)); + if(display != CSS_DISPLAY_INLINE) { + pad.Min = pad.Max = ImGui::GetStyle().FramePadding; + if(padding[0] >= 0) { + pad.Min.x = padding[0]; + } + + if(padding[1] >= 0) { + pad.Min.y = padding[1]; + } + + if(padding[2] >= 0) { + pad.Max.x = padding[2]; + } + + if(padding[3] >= 0) { + pad.Max.y = padding[3]; + } + } + + ImGui::SetCursorScreenPos(pos + pad.Min); + int popColor = 0; + if(mStyle.decoration.bgCol != 0) { + ImGui::PushStyleColor(ImGuiCol_FrameBg, 0); + ++popColor; + } + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0); + ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize; + if(size.x != 0) { + ImGui::PushItemWidth(size.x - pad.Min.x - pad.Max.x); + } + int col[4] = {0}; + bool changed = false; + bool checked = hasState(CHECKED); + + switch(mType) { + case PASSWORD: + flags |= ImGuiInputTextFlags_Password; + case TEXT: + if(mValueUpdated) { + strcpy(&mBuffer.Data[0], mValue); + changed = true; + } + + if(placeholder) { + changed = ImGui::InputTextWithHint("##label", placeholder, mBuffer.Data, mBuffer.size() + 1, flags, InputTextCallback, &mBuffer); + } else { + changed = ImGui::InputText("##label", mBuffer.Data, mBuffer.size() + 1, flags, InputTextCallback, &mBuffer); + } + + break; + case BUTTON: + if(mValue) + ImGui::TextUnformatted(mValue); + break; + case COLOR: + if(mValueUpdated) { + char* p = mValue; + while (*p == '#' || ImCharIsBlankA(*p)) + p++; + + if (strlen(p) == 8) { + sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&col[0], (unsigned int*)&col[1], (unsigned int*)&col[2], (unsigned int*)&col[3]); + } else { + sscanf(p, "%02X%02X%02X", (unsigned int*)&col[0], (unsigned int*)&col[1], (unsigned int*)&col[2]); + col[3] = 0xFF; + } + + mColor.x = (float)col[0]/255.0f; + mColor.y = (float)col[1]/255.0f; + mColor.z = (float)col[2]/255.0f; + mColor.w = (float)col[3]/255.0f; + changed = true; + } + + ImGui::ColorButton(placeholder ? placeholder : "color", mColor); + if (ImGui::BeginPopup("color-picker")) { + changed = ImGui::ColorPicker4("##picker", (float*)&mColor, ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoSmallPreview); + ImGui::EndPopup(); + } + + break; + case CHECKBOX: + if(ImGui::Checkbox(placeholder ? placeholder : "##checkbox", &checked)) { + if(checked) { + setState(CHECKED); + } else { + resetState(CHECKED); + } + changed = true; + } + break; + case RADIO: + if(ImGui::RadioButton(placeholder ? placeholder : "##radio", hasState(CHECKED))) { + setState(CHECKED); + changed = true; + } + break; + case RANGE: + changed = ImGui::SliderFloat(placeholder ? placeholder : "##slider", &mSliderValue, min, max, format ? format : "%.3f", step); + default: + // nothing + break; + } + + if(size.x != 0) { + ImGui::PopItemWidth(); + } + ImGui::PopStyleColor(popColor); + ImGui::PopStyleVar(2); + ImGui::SetCursorScreenPos(ImGui::GetItemRectMax() + pad.Max); + ImGui::EndGroup(); + + if(ImGui::IsItemClicked(0)) { + if((mFlags & Element::BUTTON) == 0) { + mActivate = true; + } + + if(mType == COLOR) { + ImGui::OpenPopup("color-picker"); + } + } + ImGui::PopID(); + + if(!ImGui::IsMouseDown(0) && mActivate) { + mActivate = false; + ImGui::SetKeyboardFocusHere(-1); + } + mValueUpdated = false; + + if(changed) { + syncModel(true); + } + } + + void setType(char* type) { + if(ImStricmp(type, "text") == 0) { + mType = TEXT; + } else if(ImStricmp(type, "password") == 0) { + mType = PASSWORD; + } else if(ImStricmp(type, "button") == 0) { + mType = BUTTON; + } else if(ImStricmp(type, "color") == 0) { + mType = COLOR; + } else if(ImStricmp(type, "checkbox") == 0) { + mType = CHECKBOX; + } else if(ImStricmp(type, "radio") == 0) { + mType = RADIO; + } else if(ImStricmp(type, "range") == 0) { + mType = RANGE; + } + + if(mType == TEXT || mType == PASSWORD) { + mFlags ^= Element::BUTTON; + } + } + + void setValue(char* value) + { + if(mValue == value || (mValue && strcmp(value, mValue) == 0)) { + return; + } + + if(mValue) { + ImGui::MemFree(mValue); + mValue = 0; + } + + if(!value) { + return; + } + + mValue = ImStrdup(value); + mValueUpdated = true; + } + + void setModel(char* key) + { + if(mModel) { + ImGui::MemFree(mModel); + ImU32 id = mScriptState->hash(mModel); + bool removed = mScriptState->removeListener(id, this); + (void)removed; + IM_ASSERT(removed && "failed to remove listener"); + mModel = 0; + } + + if(key[0] != '\0') { + bindListener(mScriptState->hash(key), NULL, Element::MODEL); + invalidateFlags(Element::MODEL); + mModel = ImStrdup(key); + } + } + + void setChecked(bool value) + { + value ? setState(CHECKED) : resetState(CHECKED); + } + + char* placeholder; + + // range only + char* format; + float min; + float max; + float step; + + // radio only + char* name; + + private: + + void syncModel(bool write = false) + { + if(!mModel || (mFlags & BUTTON)) + return; + + Object object = (*mScriptState)[mModel]; + + if(write) { + switch(mType) { + case PASSWORD: + case TEXT: + (*mScriptState)[mModel] = mBuffer.Data; + break; + case COLOR: + (*mScriptState)[mModel] = ImGui::ColorConvertFloat4ToU32(mColor); + break; + case CHECKBOX: + if(object.type() == ObjectType::USERDATA) { + updateArray(object); + } else { + (*mScriptState)[mModel] = hasState(CHECKED); + } + break; + case RADIO: + (*mScriptState)[mModel] = mValue ? mValue : placeholder; + break; + case RANGE: + (*mScriptState)[mModel] = mSliderValue; + default: + break; + } + } else { + switch(mType) { + case PASSWORD: + case TEXT: + setValue(object.as().get()); + break; + case COLOR: + mColor = ImGui::ColorConvertU32ToFloat4(object.as()); + break; + case CHECKBOX: + if(object.type() == ObjectType::USERDATA) { + resetState(CHECKED); + for(Object::iterator iter = object.begin(); iter != object.end(); ++iter) { + if(strcmp(iter.value.as().get(), mValue ? mValue : placeholder) == 0) { + setState(CHECKED); + break; + } + } + } else { + if(object.as()) + setState(CHECKED); + else + resetState(CHECKED); + } + + break; + case RADIO: + if(strcmp(object.as().get(), (mValue ? mValue : placeholder)) == 0) + setState(CHECKED); + else + resetState(CHECKED); + + break; + case RANGE: + mSliderValue = object.as(); + default: + break; + } + } + } + + void updateArray(Object& object) + { + bool found = false; + int index = 0; + const char* value = mValue ? mValue : placeholder; + + for(Object::iterator iter = object.begin(); iter != object.end(); ++iter) { + index++; + if(strcmp(iter.value.as().get(), value) == 0) { + if(!hasState(CHECKED)) { + object.erase(iter.key); + } + found = true; + break; + } + } + + if(hasState(CHECKED) && !found) { + object[index + 1] = value; + } + } + + char* mValue; + char* mModel; + bool mValueUpdated; + bool mActivate; + float mSliderValue; + ImVec4 mColor; + ImU32 mID; + InputType mType; + ImVector mBuffer; + + }; + + inline void registerHtmlExtras(ElementFactory& factory) + { + factory.element("html"); + factory.element("body"); + factory.element("div"); + factory.element("span"); + factory.element("label"); + factory.element("p"); + factory.element("h1"); + factory.element("h2"); + factory.element("h3"); + factory.element("h4"); + factory.element("h5"); + factory.element("h6"); + factory.element("input") + .setter("type", &Input::setType) + .setter("v-model", &Input::setModel) + .setter("value", &Input::setValue) + .setter("checked", &Input::setChecked) + .attribute("name", &Input::name) + .attribute("min", &Input::min) + .attribute("max", &Input::max) + .attribute("step", &Input::step) + .attribute("format", &Input::format) + .attribute("placeholder", &Input::placeholder); + } + +} + +#endif diff --git a/src/imvue.cpp b/src/imvue.cpp index 2e4ae2e..76edfa7 100644 --- a/src/imvue.cpp +++ b/src/imvue.cpp @@ -1,8 +1,30 @@ +/* +Copyright (c) 2019 Artem Chernyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + #include "imvue_element.h" #include "imvue_generated.h" #include "imgui.h" #include "imvue.h" -#include "imvue_extensions.h" +#include "imvue_style.h" #include "imgui_internal.h" #include "rapidxml.hpp" #include @@ -84,10 +106,6 @@ namespace ImVue { } } - // ComponentContainer - // should create a new context - // define itself as a root object - // ComponentContainer::ComponentContainer() : mDocument(0) , mRawData(NULL) @@ -169,7 +187,12 @@ namespace ImVue { { Element* res = NULL; Context* ctx = mCtx; - const ElementBuilder* builder = mFactory->get(node->name()); + const char* nodeName = node->name(); + if(nodeName[0] == '\0') { + nodeName = TEXT_NODE; + } + + const ElementBuilder* builder = mFactory->get(nodeName); if(builder) { res = builder->create(node, ctx, sctx, parent); } else { @@ -268,20 +291,35 @@ namespace ImVue { rapidxml::xml_node<>* scriptNode = root->first_node("script"); if(scriptNode && mScriptState) { - rapidxml::xml_attribute<>* src = scriptNode->first_attribute("src"); - if(src) { - char* rawData = mCtx->fs->load(src->value()); - if(!rawData) { - IMVUE_EXCEPTION(ElementError, "failed to load document %s", src->value()); - return; + char* data = getNodeData(mCtx, scriptNode); + if(data) { + try { + mScriptState->initialize(data); + } catch(...) { + ImGui::MemFree(data); + throw; } - mScriptState->initialize(rawData); - ImGui::MemFree(rawData); } else { mScriptState->initialize(scriptNode->value()); } } + for (rapidxml::xml_node<>* node = root->first_node("style"); node; node = node->next_sibling("style")) { + char* data = getNodeData(mCtx, node); + bool scoped = node->first_attribute("scoped") != NULL; + + if(data) { + try { + mCtx->style->load(data, scoped); + } catch(...) { + ImGui::MemFree(data); + throw; + } + } else { + mCtx->style->load(node->value(), scoped); + } + } + fireCallback(ScriptState::BEFORE_CREATE); mNode = root->first_node("template"); @@ -360,7 +398,24 @@ namespace ImVue { if(ref && mScriptState) { mScriptState->addReference(ref, this); } - createChildren(mDocument); + rapidxml::xml_node<>* tmpl = mDocument->first_node("template"); + createChildren(tmpl ? tmpl : mDocument); + + for (rapidxml::xml_node<>* node = mDocument->first_node("style"); node; node = node->next_sibling("style")) { + char* data = getNodeData(mCtx, node); + bool scoped = node->first_attribute("scoped") != NULL; + + if(data) { + try { + mCtx->style->load(data, scoped); + } catch(...) { + ImGui::MemFree(data); + throw; + } + } else { + mCtx->style->load(node->value(), scoped); + } + } // clear change listeners to avoid triggering reactive change for each prop initial setup if(mCtx->script) diff --git a/src/imvue.h b/src/imvue.h index cf7c081..d176426 100644 --- a/src/imvue.h +++ b/src/imvue.h @@ -1,3 +1,25 @@ +/* +Copyright (c) 2019 Artem Chernyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + #ifndef __IMVUE_H__ #define __IMVUE_H__ @@ -59,6 +81,22 @@ namespace ImVue { typedef std::unordered_map ComponentProperties; + inline char* getNodeData(Context* ctx, rapidxml::xml_node<>* node) + { + rapidxml::xml_attribute<>* src = node->first_attribute("src"); + if(src) { + char* data = ctx->fs->load(src->value()); + if(!data) { + IMVUE_EXCEPTION(ElementError, "failed to load file %s", src->value()); + return NULL; + } + + return data; + } + + return NULL; + } + /** * Loads component construction info * Can create new components of a type diff --git a/src/imvue_context.cpp b/src/imvue_context.cpp index 67c65a0..ecaf8b8 100644 --- a/src/imvue_context.cpp +++ b/src/imvue_context.cpp @@ -20,30 +20,43 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#include "imgui.h" #include "imvue_context.h" #include "imvue_generated.h" #include "imvue_script.h" +#include "imvue_style.h" +#include "extras/xhtml.h" +#include "extras/svg.h" #include #include namespace ImVue { - char* SimpleFileSystem::load(const char* path) + char* SimpleFileSystem::load(const char* path, int* size, Mode mode) { - std::ifstream stream(path, std::ifstream::binary); + std::ifstream stream(path, mode == TEXT ? std::ifstream::in : std::ifstream::binary); char* data = NULL; + if(size) { + *size = 0; + } try { stream.seekg(0, std::ios::end); - size_t length = stream.tellg(); + int length = stream.tellg(); stream.seekg(0, std::ios::beg); + if(length < 0) { + return data; + } data = (char*)ImGui::MemAlloc(length + 1); - data[length] = '\0'; + if(mode == Mode::TEXT) { + data[length] = '\0'; + } stream.read(data, length); + if(size) { + *size = length; + } } catch(std::exception& e) { @@ -63,6 +76,57 @@ namespace ImVue { ImGui::MemFree(data); } + FontManager::FontManager() + { + + } + + ImFont* FontManager::loadFontFromFile(const char* name, const char* path, ImVector glyphRanges) + { + ImU32 id = ImHashStr(name); + if(mFonts.find(id) != mFonts.end()) { + return mFonts[id].font; + } + + int size = 0; + char* data = fs->load(path, &size, FileSystem::BINARY); + if(!data || size == 0) { + return NULL; + } + + ImGuiIO& io = ImGui::GetIO(); + ImFontConfig fontCfg; + FontHandle handle; + memset(&handle, 0, sizeof(FontHandle)); + strcpy(&fontCfg.Name[0], name); + handle.glyphRanges = glyphRanges; + handle.size = 25.0f; + mFonts[id] = handle; + if(glyphRanges.size() != 0) { + fontCfg.GlyphRanges = mFonts[id].glyphRanges.Data; + } + + ImFont* font = io.Fonts->AddFontFromMemoryTTF(data, size, handle.size, &fontCfg); + if(font) { + mFonts[id].font = font; + } else { + mFonts.erase(id); + } + + return font; + } + + bool FontManager::pushFont(const char* name) + { + ImU32 id = ImHashStr(name); + if(mFonts.find(id) != mFonts.end()) { + ImGui::PushFont(mFonts[id].font); + return true; + } + + return false; + } + Context::~Context() { if(!parent) { @@ -77,14 +141,22 @@ namespace ImVue { if(fs) { delete fs; } + + if(fontManager) { + delete fontManager; + } } if(script) { delete script; } + + if(style) { + delete style; + } } - Context* createContext(ElementFactory* factory, ScriptState* script, TextureManager* texture, FileSystem* fs, void* userdata) + Context* createContext(ElementFactory* factory, ScriptState* script, TextureManager* texture, FileSystem* fs, FontManager* fontManager, Style* s, void* userdata) { Context* ctx = new Context(); memset(ctx, 0, sizeof(Context)); @@ -92,6 +164,7 @@ namespace ImVue { ctx->factory = createElementFactory(); } ctx->factory = factory; + registerHtmlExtras(*ctx->factory); ctx->texture = texture; if(!fs) { fs = new SimpleFileSystem(); @@ -99,16 +172,28 @@ namespace ImVue { ctx->fs = fs; ctx->script = script; ctx->userdata = userdata; + ctx->style = s ? s : new Style(); + ctx->fontManager = fontManager; + ctx->scale = ImVec2(1.0f, 1.0f); + fontManager->fs = fs; return ctx; } + Context* createContext(ElementFactory* factory, ScriptState* script, TextureManager* texture, FileSystem* fs, void* userdata) + { + FontManager* fontManager = new FontManager(); + Style* style = new Style(); + return createContext(factory, script, texture, fs, fontManager, style, userdata); + } + Context* newChildContext(Context* ctx) { ScriptState* script = 0; if(ctx->script) { script = ctx->script->clone(); } - Context* child = createContext(ctx->factory, script, ctx->texture, ctx->fs, ctx->userdata); + Style* style = new Style(ctx->style); + Context* child = createContext(ctx->factory, script, ctx->texture, ctx->fs, ctx->fontManager, style, ctx->userdata); child->parent = ctx; return child; } diff --git a/src/imvue_context.h b/src/imvue_context.h index 2cf6588..613451f 100644 --- a/src/imvue_context.h +++ b/src/imvue_context.h @@ -24,17 +24,32 @@ SOFTWARE. #ifndef __IMVUE_CONTEXT_H__ #define __IMVUE_CONTEXT_H__ +#include +#include "imgui.h" +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui_internal.h" + namespace ImVue { + class Context; class ComponentContainer; class ElementFactory; + class Element; class ScriptState; + class Style; + class FontManager; + struct Layout; /** * Customization point for file system access */ class FileSystem { public: + enum Mode { + TEXT, + BINARY + }; + typedef void LoadCallback(char* data); virtual ~FileSystem() {} @@ -43,9 +58,11 @@ namespace ImVue { * Loads file * * @param path file path + * @param size output size to the variable + * @param mode file read mode * @return loaded data */ - virtual char* load(const char* path) = 0; + virtual char* load(const char* path, int* size = NULL, Mode mode = Mode::TEXT) = 0; /** * Load file async @@ -65,9 +82,11 @@ namespace ImVue { * Loads file * * @param path file path + * @param size output size to the variable + * @param mode file read mode * @return loaded data */ - char* load(const char* path); + char* load(const char* path, int* size = NULL, Mode mode = Mode::TEXT); /** * Load file async @@ -100,20 +119,72 @@ namespace ImVue { virtual void deleteTexture(ImTextureID id) = 0; }; + /** + * Font manager + */ + class FontManager { + public: + struct FontHandle { + ImFont* font; + ImFontConfig cfg; + ImVector glyphRanges; + float size; + }; + + FontManager(); + + /** + * Load font from disk + * + * @param name Font name + * @param path Font path + * + * @returns ImFont if succeed + */ + ImFont* loadFontFromFile(const char* name, const char* path, ImVector glyphRanges); + + /** + * Activate font using name + * + * @param name Font name + * @param size Font size + * + * @returns true if pushed + */ + bool pushFont(const char* name); + + inline FontHandle& getFont(const char* name) { + return mFonts.at(ImHashStr(name)); + } + + FileSystem* fs; + + private: + typedef std::map Fonts; + Fonts mFonts; + }; + /** * Object that keeps imvue configuration */ - struct Context { - ~Context(); - - ScriptState* script; - TextureManager* texture; - ElementFactory* factory; - FileSystem* fs; - ComponentContainer* root; - Context* parent; - // additional userdata that will be available from all the components - void* userdata; + class Context { + public: + ~Context(); + + ScriptState* script; + TextureManager* texture; + ElementFactory* factory; + FileSystem* fs; + ComponentContainer* root; + Context* parent; + Style* style; + FontManager* fontManager; + Layout* layout; + // additional userdata that will be available from all the components + void* userdata; + + // adjust styles scale using this variable + ImVec2 scale; }; /** diff --git a/src/imvue_element.cpp b/src/imvue_element.cpp index f116cdf..d31c240 100644 --- a/src/imvue_element.cpp +++ b/src/imvue_element.cpp @@ -29,9 +29,10 @@ SOFTWARE. namespace ImVue { - EventHandler::EventHandler(const char* handlerName, const char* script) + EventHandler::EventHandler(Element* element, const char* handlerName, const char* script) : mHandlerName(handlerName) , mScript(script) + , mElement(element) { } @@ -55,8 +56,8 @@ namespace ImVue { } } - InputHandler::InputHandler(const char* handlerName, const char* script) - : EventHandler(handlerName, script) + InputHandler::InputHandler(Element* element, const char* handlerName, const char* script) + : EventHandler(element, handlerName, script) , mModifiers(0) , mExact(false) { @@ -82,8 +83,8 @@ namespace ImVue { } } - MouseEventHandler::MouseEventHandler(const char* handlerName, const char* script) - : InputHandler(handlerName, script) + MouseEventHandler::MouseEventHandler(Element* element, const char* handlerName, const char* script) + : InputHandler(element, handlerName, script) , mType(Click) , mMouseButton(0) , mHovered(false) @@ -96,6 +97,8 @@ namespace ImVue { if(checkProp("click")) { mType = Click; + } else if(checkProp("doubleclick")) { + mType = DoubleClick; } else if(checkProp("mousedown")) { mType = MouseDown; } else if(checkProp("mouseup")) { @@ -109,7 +112,7 @@ namespace ImVue { bool MouseEventHandler::check() { - bool hovered = ImGui::IsItemHovered(); + bool hovered = mElement->isHovered(); bool changed = mHovered != hovered; mHovered = hovered; @@ -120,7 +123,10 @@ namespace ImVue { bool trigger = false; switch(mType) { case Click: - trigger = ImGui::IsItemClicked(mMouseButton); + trigger = ImGui::IsMouseClicked(mMouseButton); + break; + case DoubleClick: + trigger = ImGui::IsMouseDoubleClicked(mMouseButton); break; case MouseDown: trigger = ImGui::IsMouseDown(mMouseButton); @@ -156,8 +162,8 @@ namespace ImVue { const KeyboardEventHandler::KeyMap KeyboardEventHandler::keyMap = KeyboardEventHandler::init(); - KeyboardEventHandler::KeyboardEventHandler(const char* handlerName, const char* script) - : InputHandler(handlerName, script) + KeyboardEventHandler::KeyboardEventHandler(Element* element, const char* handlerName, const char* script) + : InputHandler(element, handlerName, script) , mKeyIndex(-1) , mType(Down) { @@ -214,6 +220,9 @@ namespace ImVue { , ref(NULL) , enabledAttr(NULL) , key(NULL) + , display(CSS_DISPLAY_BLOCK) + , index(-1) + , bgColor(0) , mNode(0) , mParent(0) , mFactory(0) @@ -223,11 +232,22 @@ namespace ImVue { , mScriptState(NULL) , mCtx(0) , mScriptContext(0) + , mStyle(this) , mInvalidFlags(0) - , mFlags(0) + , mFlags(BUTTON) + , mState(0) , mRequiredAttrsCount(0) , mConfigured(false) { + padding[0] = -1.0f; + padding[1] = -1.0f; + padding[2] = -1.0f; + padding[3] = -1.0f; + + margins[0] = FLT_MIN; + margins[1] = FLT_MIN; + margins[2] = FLT_MIN; + margins[3] = FLT_MIN; } Element::~Element() @@ -237,7 +257,9 @@ namespace ImVue { if(mScriptState) { for(Element::ReactiveFields::const_iterator iter = mReactiveFields.begin(); iter != mReactiveFields.end(); ++iter) { - IM_ASSERT(mScriptState->removeListener(iter->first, this) && "failed to remove listener"); + bool success = mScriptState->removeListener(iter->first, this); + (void)success; + IM_ASSERT(success && "failed to remove listener"); } } @@ -268,6 +290,12 @@ namespace ImVue { if(mScriptContext && mScriptContext->owner == this) { delete mScriptContext; } + + for(Classes::iterator iter = classes.begin(); iter != classes.end(); iter++) { + ImGui::MemFree(iter->first); + } + + classes.clear(); } void Element::configure(rapidxml::xml_node<>* node, Context* ctx, ScriptState::Context* sctx, Element* parent) @@ -281,6 +309,9 @@ namespace ImVue { mNode = node; mFactory = ctx->factory; mTextureManager = ctx->texture; + if(ImStricmp(node->name(), "window") == 0) { + mFlags |= WINDOW; + } mConfigured = build(); } @@ -292,6 +323,120 @@ namespace ImVue { return mBuilder->get(attrID); } + bool Element::evalAttribute(const char* id, char** dest) + { + *dest = 0; + if(!mNode) { + return false; + } + + int flags = 0; + + rapidxml::xml_attribute<>* attr = mNode->first_attribute(id); + if(!attr) { + char* scripted = (char*)ImGui::MemAlloc(strlen(id) + 2); + scripted[0] = ':'; + strcpy(&scripted[1], id); + attr = mNode->first_attribute(scripted); + ImGui::MemFree(scripted); + + if(!attr) { + return false; + } + flags |= Attribute::SCRIPT; + } + + return evaluateString(attr->value(), this, mScriptState, flags, 0, dest); + } + + void Element::setSize(const ImVec2& s) + { + size = s; + if(mFlags & WINDOW) { + ImGui::SetNextWindowSize(size); + } // widgets have no common method to set size unfortunately + } + + void Element::setInlineStyle(char* style) + { + mStyle.setInlineStyle(style); + ImGui::MemFree(style); + invalidateFlags(Element::STYLE); + } + + void Element::setClasses(const char* cls, int flags, ScriptState::Fields* fields) { + for(Classes::iterator iter = classes.begin(); iter != classes.end(); iter++) { + ImGui::MemFree(iter->first); + } + + classes.clear(); + if(flags & Attribute::SCRIPT) { + Object classesList = mScriptState->getObject(cls, fields, mScriptContext); + for(Object::iterator iter = classesList.begin(); iter != classesList.end(); ++iter) { + classes[ImStrdup(iter.value.as().c_str())] = true; + } + } else { + ImVector values; + if(detail::parse_array(cls, values, ' ')) { + for(int i = 0; i < values.size(); i++) { + classes[values[i]] = true; + } + } + } + mStyle.updateClassCache(); + invalidateFlags(Element::STYLE); + } + + bool Element::isHovered(ImGuiHoveredFlags flags) const + { + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiWindow* window = g.CurrentWindow; + if (g.NavDisableMouseHover && !g.NavDisableHighlight) + return ImGui::IsItemFocused(); + + // Test for bounding box overlap, as updated as ItemAdd() + if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) && !ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMin() + getSize()).Contains(ImGui::GetIO().MousePos) ) + return false; + IM_ASSERT((flags & (ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows)) == 0); // Flags not supported by this function + + // Test if we are hovering the right window (our window could be behind another window) + // [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable to use IsItemHovered() after EndChild() itself. + // Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was the test that has been running for a long while. + //if (g.HoveredWindow != window) + // return false; + if (g.HoveredRootWindow != window->RootWindow && !(flags & ImGuiHoveredFlags_AllowWhenOverlapped)) + return false; + + // Test if another item is active (e.g. being dragged) + if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + if (g.ActiveId != 0 && g.ActiveId != window->DC.LastItemId && !g.ActiveIdAllowOverlap && g.ActiveId != window->MoveId) + return false; + + // Test if interactions on this window are blocked by an active popup or modal. + // The ImGuiHoveredFlags_AllowWhenBlockedByPopup flag will be tested here. + if (g.NavWindow) + if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindow) + if (focused_root_window->WasActive && focused_root_window != window->RootWindow) + { + // For the purpose of those flags we differentiate "standard popup" from "modal popup" + // NB: The order of those two tests is important because Modal windows are also Popups. + if (focused_root_window->Flags & ImGuiWindowFlags_Modal) + return false; + if ((focused_root_window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + return false; + } + + // Test if the item is disabled + if ((window->DC.ItemFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + + // Special handling for the dummy item after Begin() which represent the title bar or tab. + // When the window is collapsed (SkipItems==true) that last item will never be overwritten so we need to detect the case. + if (window->DC.LastItemId == window->MoveId && window->WriteAccessed) + return false; + return true; + } + void Element::invalidateFlags(unsigned int flags) { mInvalidFlags |= flags; } @@ -356,6 +501,10 @@ namespace ImVue { return true; } + if(ImStricmp(attrID, "class") == 0) { + setClasses(value, flags, fields); + } + return false; } @@ -401,6 +550,9 @@ namespace ImVue { IMVUE_EXCEPTION(ElementError, "failed to build element %s, missing required properties %s", getType(), missed.str().c_str()); return false; } + + mStyle.compute(this); + invalidateFlags(Element::STYLE); return enabled; } @@ -408,18 +560,87 @@ namespace ImVue { { if(mInvalidFlags & Element::BUILD) { mConfigured = build(); - mInvalidFlags = 0; + mInvalidFlags ^= Element::BUILD; + } + + if(!mConfigured) { + return; + } + + if(mInvalidFlags & Element::STYLE) { + if(mStyle.compute(this)) { + for(size_t i = 0; i < mChildren.size(); ++i) { + mChildren[i]->invalidateFlags(Element::STYLE); + } + } + mInvalidFlags ^= Element::STYLE; } computeProperties(); if(!enabled) { + setState(HIDDEN); + return; + } + + if(display == CSS_DISPLAY_NONE) { + setState(HIDDEN); return; } + + resetState(HIDDEN); + if(key) { ImGui::PushID(ImHashStr(key)); } - renderBody(); + mStyle.begin(this); + + Layout* layout = isPseudoElement() ? 0 : mCtx->layout; + if(layout) + layout->beginElement(this); + + pos = ImGui::GetCursorPos(); + ImGuiWindow* window = GetCurrentWindowNoDefault(); + if(window) { + // render decoration + mStyle.decoration.render( + ImRect( + window->Pos - window->Scroll + pos, + window->Pos - window->Scroll + pos + getSize() + ) + ); + } + + try { + renderBody(); + } catch (...) { + mStyle.end(); + throw; + } + + computedSize = ImGui::GetItemRectMax() - ImGui::GetItemRectMin(); + if(layout) + layout->endElement(this); + + mStyle.end(); + bool hovered = isHovered(); + + if(ImGui::IsItemActive() || (hovered && ImGui::IsMouseDown(0) && (mFlags & BUTTON))) { + resetState(HOVERED); + setState(ACTIVE); + } else if(hovered) { + resetState(ACTIVE); + setState(HOVERED); + } else { + resetState(HOVERED); + resetState(ACTIVE); + } + + if(ImGui::IsItemFocused()) { + setState(FOCUSED); + } else { + resetState(FOCUSED); + } if(key) { ImGui::PopID(); @@ -434,6 +655,11 @@ namespace ImVue { } } + ContainerElement* Element::getParent() + { + return mParent ? static_cast(mParent) : NULL; + } + void Element::computeProperties() { if(mDirtyProperties.size() == 0) { @@ -506,7 +732,7 @@ namespace ImVue { return; } - EventHandler* handler = builder->createHandler(handlerName, fullName, value); + EventHandler* handler = builder->createHandler(this, handlerName, fullName, value); if(!handler) { IMVUE_EXCEPTION(ElementError, "failed to create handler of type %s", handlerName); return; @@ -549,8 +775,23 @@ namespace ImVue { } void ContainerElement::renderChildren() { + bool pseudoElement = isPseudoElement(); + Layout* backup = mCtx->layout; + if(!pseudoElement) { + mCtx->layout = &mLayout; + mLayout.begin(this); + } + + int i = 0; for(Elements::iterator el = mChildren.begin(); el != mChildren.end(); el++) { - (*el)->render(); + Element* element = *el; + element->index = i++; + element->render(); + } + + if(!pseudoElement) { + mLayout.end(); + mCtx->layout = backup; } } @@ -570,7 +811,6 @@ namespace ImVue { rapidxml::xml_node<>* root = doc ? doc : mNode; for (rapidxml::xml_node<>* node = root->first_node(); node; node = node->next_sibling()) { - Element* e = NULL; // v-for case is special: we don't need to create a single element for it const rapidxml::xml_attribute<>* vfor = node->first_attribute("v-for"); @@ -586,9 +826,9 @@ namespace ImVue { throw; } } else { - e = createElement(node); + e = createElement(node, NULL, this); if(!e) { - IMVUE_EXCEPTION(ElementError, "failed to create element %s", node->name()); + IMVUE_EXCEPTION(ElementError, "failed to create element '%s'", node->name()); continue; } @@ -669,6 +909,8 @@ namespace ImVue { size_t index = 0; + mStyle.compute(this); + for(Object::iterator iter = object.begin(); iter != object.end(); ++iter, ++index) { ScriptState::FieldHash hash = mScriptState->hash(iter.key.as().get()); @@ -697,7 +939,7 @@ namespace ImVue { c->add(keyVar, iter.key, ScriptState::Variable::KEY); } - Element* e = createElement(mNode, c); + Element* e = createElement(mNode, c, this); if(!e) { IMVUE_EXCEPTION(ElementError, "failed to create element %s", mNode->name()); return false; @@ -712,6 +954,8 @@ namespace ImVue { mElementsByKey[hash] = e; visited[hash] = true; } + + mChildren[index]->invalidateFlags(Element::STYLE); } for(size_t i = mChildren.size(); i > index; --i) { @@ -740,9 +984,15 @@ namespace ImVue { void ConditionChain::pushChild(Element* element, rapidxml::xml_node<>* node) { + if(!mScriptState) { + IMVUE_EXCEPTION(ElementError, "conditionals are supported only with script enabled"); + return; + } + IM_ASSERT(element->enabledAttr != NULL && "element does not have enabledAttr defined, why is it in the ConditionChain?"); if(mDefault) { IMVUE_EXCEPTION(ElementError, "malformed condition chain: v-else/v-else-if/v-if after the last v-else"); + return; } if(ImStricmp(element->enabledAttr, "v-else") == 0) { @@ -758,6 +1008,7 @@ namespace ImVue { mChildren.push_back(element); element->setParent(this); + element->invalidateFlags(Element::STYLE); invalidateFlags(Element::BUILD); } @@ -840,19 +1091,20 @@ namespace ImVue { void Slot::configure(rapidxml::xml_node<>* node, Context* ctx, ScriptState::Context* sctx, Element* parent) { - (void)parent; + (void)node; if(!ctx->root || (ctx->root->getFlags() & Element::COMPONENT) == 0) { IMVUE_EXCEPTION(ElementError, "slot must be a child of a component"); return; } rapidxml::xml_node<>* n = ctx->root->node(); - if(!n->first_node() && !n->value()) { + + if(!n->first_node() && *n->value() == 0) { return; } mBuilder = ctx->factory->get("slot"); - Element::configure(node, ctx->parent ? ctx->parent : ctx, sctx, ctx->root); + PseudoElement::configure(n, ctx, sctx, parent); } } diff --git a/src/imvue_element.h b/src/imvue_element.h index 512fbfc..9613737 100644 --- a/src/imvue_element.h +++ b/src/imvue_element.h @@ -30,6 +30,8 @@ SOFTWARE. #include "imvue_script.h" #include "imvue_errors.h" #include "imvue_context.h" +#include "imvue_style.h" +#include "imvue_layout.h" #include #include #include @@ -60,6 +62,11 @@ namespace ImVue { */ static const char* TEXT_ID = "@text"; + /** + * TEXT node ID + */ + static const char* TEXT_NODE = "__textnode__"; + // attribute converting utilities // used for parsing statically defined fields as ints/floats/arrays etc namespace detail { @@ -146,7 +153,7 @@ namespace ImVue { } template - bool parse_array(const char* value, ImVector& values); + bool parse_array(const char* value, ImVector& values, char separator = ','); inline bool read(const char* value, ImGuiID* dest) { if(!str_to_number(value, dest)) { @@ -236,7 +243,7 @@ namespace ImVue { typename std::enable_if::value, bool>::type read(const char* value, C* dest) { - IM_ASSERT("Detected unsupported field"); + IM_ASSERT(false && "Detected unsupported field"); (void)value; (void)dest; // default unsupported behaviour @@ -244,7 +251,7 @@ namespace ImVue { } template - bool parse_array(const char* value, ImVector& values) + bool parse_array(const char* value, ImVector& values, char separator) { char endsym = 0; const char* start = value; @@ -272,7 +279,7 @@ namespace ImVue { end = (endsym == 0 ? end : ImStrchrRange(value, end, endsym)) - 1; while(start < end + 1) { - const char* next = ImStrchrRange(start, end, ','); + const char* next = ImStrchrRange(start, end, separator); if(next == 0) { next = end + 1; } @@ -291,12 +298,11 @@ namespace ImVue { } memcpy(&part[0], start, len); - part[len + 1] = '\0'; + part[len] = '\0'; - C element; - if(read(&part[0], &element)) { - values.push_back(element); - } else { + values.resize(values.size() + 1); + values[values.size() - 1] = 0; + if(!read(&part[0], &values[values.size() - 1])) { return false; } start = next + 1; @@ -410,7 +416,7 @@ namespace ImVue { typename std::enable_if::value, bool>::type read(Object& value, C* dest) { - IM_ASSERT("Detected unsupported field"); + IM_ASSERT(false && "Detected unsupported field"); (void)value; (void)dest; // default unsupported behaviour @@ -433,15 +439,32 @@ namespace ImVue { class Element { public: + typedef std::vector Elements; enum InvalidationFlag { - BUILD = 1 << 0 + BUILD = 1 << 0, + STYLE = 1 << 1, + MODEL = 1 << 2 + }; + + // mutually excluding element states + enum ElementState { + HIDDEN = 1 << 0, + ACTIVE = 1 << 1, + HOVERED = 1 << 2, + DISABLED = 1 << 3, + CHECKED = 1 << 4, + LINK = 1 << 5, + VISITED = 1 << 6, + FOCUSED = 1 << 7 }; enum Flag { CONTAINER = 1 << 0, PSEUDO_ELEMENT = 1 << 1, - COMPONENT = 1 << 2 + COMPONENT = 1 << 2, + WINDOW = 1 << 3, + BUTTON = 1 << 4 }; typedef std::unordered_map ReactiveFields; @@ -469,17 +492,34 @@ namespace ImVue { */ inline void setParent(Element* element) { mParent = element; } + /** + * Get parent + */ + ContainerElement* getParent(); + /** * Enable element */ inline void enable() { if(!mConfigured) invalidateFlags(Element::BUILD); + + invalidateFlags(Element::STYLE); enabled = true; } /** - * Get context + * Get element ctx + */ + inline Context* context() + { + return mCtx; + } + + /** + * Get element script context + * + * @param recursive traverse to parent context */ inline ScriptState::Context* getContext(bool recursive = false) { if(mScriptContext) { @@ -525,6 +565,13 @@ namespace ImVue { return (mFlags & Element::CONTAINER) != 0; } + /** + * Check if Element is pseudo element + */ + inline bool isPseudoElement() const { + return (mFlags & Element::PSEUDO_ELEMENT) != 0; + } + /** * Get attribute interface by field id * @@ -532,6 +579,25 @@ namespace ImVue { */ const Attribute* getAttribute(const char* id) const; + /** + * Check if underlying node has attribute + * + * @param id attribute name + */ + inline bool hasAttribute(const char* name) const { + return mNode ? mNode->first_attribute(name) != 0 : false; + } + + /** + * Evaluates attribute + * + * @param id attribute name + * @param dest Destination to write to + * + * @returns true if succeed + */ + bool evalAttribute(const char* id, char** dest); + /** * Exposed mainly for tests */ @@ -541,6 +607,57 @@ namespace ImVue { inline rapidxml::xml_node<>* node() { return mNode; } + void setSize(const ImVec2& size); + + inline ImVec2 getSize() const { + ImVec2 res = size; + if(res.x <= 0) { + res.x = computedSize.x; + } + + if(res.y <= 0) { + res.y = computedSize.y; + } + + return res; + } + + void setInlineStyle(char* style); + + void setClasses(const char* cls, int flags, ScriptState::Fields* fields); + + bool isHovered(ImGuiHoveredFlags flags = 0) const; + + inline bool hasClass(const char* cls) const + { + return classes.find(const_cast(cls)) != classes.end(); + } + + inline ComputedStyle* style() { + return &mStyle; + } + + inline bool visible() const { + return enabled && display != CSS_DISPLAY_NONE; + } + + inline bool hasState(ElementState s) { + return (mState & s) != 0; + } + + inline ImRect getMarginsRect() const { + return ImRect( + ImVec2( + margins[1] == FLT_MIN ? 0 : margins[1], + margins[0] == FLT_MIN ? 0 : margins[0] + ), + ImVec2( + margins[2] == FLT_MIN ? 0 : margins[2], + margins[3] == FLT_MIN ? 0 : margins[3] + ) + ); + } + // public properties /** @@ -568,8 +685,39 @@ namespace ImVue { */ char* key; + // style related variables + + ImVec2 pos; + /** + * Common size definition + */ + ImVec2 size; + + ImVec2 computedSize; + + /** + * Element display type + */ + uint16_t display; + + /** + * Element index + */ + int index; + int state; + + float padding[4]; + float margins[4]; + + ImU32 bgColor; + + typedef std::map Classes; + Classes classes; + protected: + friend class ContainerElement; + void bindListeners(ScriptState::Fields& fields, const char* attribute = 0, unsigned int flags = 0); void bindListener(ScriptState::FieldHash field, const char* attribute = 0, unsigned int flags = 0); @@ -598,9 +746,31 @@ namespace ImVue { void addHandler(const char* name, const char* value, const ElementBuilder* builder); + inline void setState(ElementState s) + { + if((mState & s) == 0) { + mState |= s; + invalidateFlags(Element::STYLE); + + for(size_t i = 0; i < mChildren.size(); ++i) { + mChildren[i]->invalidateFlags(Element::STYLE); + } + } + } + + inline void resetState(ElementState s) { + if((mState & s) != 0) { + mState ^= s; + invalidateFlags(Element::STYLE); + + for(size_t i = 0; i < mChildren.size(); ++i) { + mChildren[i]->invalidateFlags(Element::STYLE); + } + } + } + rapidxml::xml_node<>* mNode; - typedef std::vector Elements; typedef std::map Handlers; Handlers mHandlers; @@ -615,9 +785,11 @@ namespace ImVue { DirtyProperties mDirtyProperties; Context* mCtx; ScriptState::Context* mScriptContext; + ComputedStyle mStyle; unsigned int mInvalidFlags; unsigned int mFlags; + unsigned int mState; int mRequiredAttrsCount; bool mConfigured; @@ -664,7 +836,7 @@ namespace ImVue { * @param flags evaluation flags */ virtual void read( - const char* attribute, + const char* attr, const char* str, Element* element, ScriptState* scriptState, @@ -685,6 +857,62 @@ namespace ImVue { bool required; }; + template + bool evaluateString(const char* str, Element* element, ScriptState* scriptState, int flags, ScriptState::Fields* fields, Type* value) + { + bool success = false; + if(scriptState) { + if(flags & Attribute::SCRIPT) { + Object object = scriptState->getObject(str, fields, element->getContext()); + if(!object.valid()) { + IMVUE_EXCEPTION(ScriptError, "failed to evaluate data %s", str); + return false; + } + success = detail::read(object, value); + } else if(flags & Attribute::TEMPLATED_STRING) { + std::string retval; + std::stringstream result; + std::stringstream ss; + bool evaluation = false; + + for(int i = 0;;i++) { + if(str[i] == '\0') { + break; + } + + if(evaluation && std::strncmp(&str[i], "}}", 2) == 0) { + Object object = scriptState->getObject(&ss.str()[0], fields, element->getContext()); + ss = std::stringstream(); + evaluation = false; + result << object.as().c_str(); + i++; + continue; + } + + if(!evaluation && std::strncmp(&str[i], "{{", 2) == 0) { + evaluation = true; + i++; + continue; + } + + if(evaluation) { + ss << str[i]; + } else { + result << str[i]; + } + } + + success = detail::read(&result.str()[0], value); + } + } + + if(!success) { + success = detail::read(str, value); + } + + return success; + } + /** * MemPtr implementation of attribute reader */ @@ -702,56 +930,7 @@ namespace ImVue { void read(const char* attribute, const char* str, Element* element, ScriptState* scriptState, int flags = 0, ScriptState::Fields* fields = 0) const { D* dest = &(static_cast(element)->*mMemPtr); - bool success = false; - if(scriptState) { - if(flags & SCRIPT) { - Object object = scriptState->getObject(str, fields, element->getContext()); - if(!object.valid()) { - IMVUE_EXCEPTION(ScriptError, "failed to evaluate data %s", str); - return; - } - success = detail::read(object, dest); - } else if(flags & TEMPLATED_STRING) { - std::string retval; - std::stringstream result; - std::stringstream ss; - bool evaluation = false; - - for(int i = 0;;i++) { - if(str[i] == '\0') { - break; - } - - if(evaluation && std::strncmp(&str[i], "}}", 2) == 0) { - Object object = scriptState->getObject(&ss.str()[0], fields, element->getContext()); - ss = std::stringstream(); - evaluation = false; - result << object.as().c_str(); - i++; - continue; - } - - if(!evaluation && std::strncmp(&str[i], "{{", 2) == 0) { - evaluation = true; - i++; - continue; - } - - if(evaluation) { - ss << str[i]; - } else { - result << str[i]; - } - } - - success = detail::read(&result.str()[0], dest); - } - } - - if(!success) { - success = detail::read(str, dest); - } - + bool success = evaluateString(str, element, scriptState, flags, fields, dest); if(!success && required) { IMVUE_EXCEPTION(ElementError, "failed to read required attribute %s", attribute); } @@ -767,6 +946,19 @@ namespace ImVue { D C::* mMemPtr; }; + template + typename std::enable_if::value, Type>::type initVariable() + { + return 0; + } + + template + typename std::enable_if::value, Type>::type initVariable() + { + static_assert(std::is_trivially_constructible::value, "setter can only be used with trivially constructible types"); + return Type(); + } + /** * Setter implementation of attribute reader */ @@ -785,56 +977,8 @@ namespace ImVue { void read(const char* attribute, const char* str, Element* element, ScriptState* scriptState, int flags = 0, ScriptState::Fields* fields = 0) const { - Type value; - bool success = false; - if(scriptState) { - if(flags & SCRIPT) { - Object object = scriptState->getObject(str, fields, element->getContext()); - if(!object.valid()) { - IMVUE_EXCEPTION(ScriptError, "failed to evaluate data %s", str); - return; - } - success = detail::read(object, &value); - } else if(flags & TEMPLATED_STRING) { - std::string retval; - std::stringstream result; - std::stringstream ss; - bool evaluation = false; - - for(int i = 0;;i++) { - if(str[i] == '\0') { - break; - } - - if(evaluation && std::strncmp(&str[i], "}}", 2) == 0) { - Object object = scriptState->getObject(&ss.str()[0], fields, element->getContext()); - ss = std::stringstream(); - evaluation = false; - result << object.as().c_str(); - i++; - continue; - } - - if(!evaluation && std::strncmp(&str[i], "{{", 2) == 0) { - evaluation = true; - i++; - continue; - } - - if(evaluation) { - ss << str[i]; - } else { - result << str[i]; - } - } - - success = detail::read(&result.str()[0], &value); - } - } - - if(!success) { - success = detail::read(str, &value); - } + Type value = initVariable(); + bool success = evaluateString(str, element, scriptState, flags, fields, &value); if(success) { (static_cast(element)->*mSetter)(value); @@ -860,7 +1004,7 @@ namespace ImVue { class HandlerFactory : public ElementEntity { public: virtual ~HandlerFactory() {} - virtual EventHandler* create(const char* handlerName, const char* script) const = 0; + virtual EventHandler* create(Element* element, const char* handlerName, const char* script) const = 0; }; /** @@ -870,8 +1014,8 @@ namespace ImVue { class HandlerFactoryImpl : public HandlerFactory { public: virtual ~HandlerFactoryImpl() {} - EventHandler* create(const char* handlerName, const char* script) const { - return new C(handlerName, script); + EventHandler* create(Element* element, const char* handlerName, const char* script) const { + return new C(element, handlerName, script); } }; @@ -908,9 +1052,9 @@ namespace ImVue { return nullptr; } - inline EventHandler* createHandler(const char* name, const char* fullName, const char* script) const { + inline EventHandler* createHandler(Element* element, const char* name, const char* fullName, const char* script) const { if(mHandlers.find(name) != mHandlers.end()) { - return mHandlers.at(name)->create(fullName, script); + return mHandlers.at(name)->create(element, fullName, script); } return nullptr; @@ -994,11 +1138,7 @@ namespace ImVue { template ElementBuilderImpl& attribute(const char* name, Type C::* memPtr, bool required = false) { - mAttributes[name] = new AttributeMemPtr(memPtr, required); - mAttributes[name]->owner = this; - if(required) { - mRequiredAttrs.push_back(name); - } + registerAttribute(name, new AttributeMemPtr(memPtr, required)); return *this; } @@ -1012,11 +1152,7 @@ namespace ImVue { template ElementBuilderImpl& setter(const char* name, void(C::*func)(Type), bool required = false) { - mAttributes[name] = new AttributeSetter(func, required); - mAttributes[name]->owner = this; - if(required) { - mRequiredAttrs.push_back(name); - } + registerAttribute(name, new AttributeSetter(func, required)); return *this; } @@ -1030,11 +1166,20 @@ namespace ImVue { template ElementBuilderImpl& text(D C::* memPtr, bool required = false) { - mAttributes[TEXT_ID] = new AttributeMemPtr(memPtr, required); - mAttributes[TEXT_ID]->owner = this; - if(required) { - mRequiredAttrs.push_back(TEXT_ID); - } + registerAttribute(TEXT_ID, new AttributeMemPtr(memPtr, required)); + return *this; + } + + /** + * Register text setter + * + * @param func setter function to use (&Element::setValue) + * @param required property is required + */ + template + ElementBuilderImpl& text(void(C::*func)(Type), bool required = false) + { + registerAttribute(TEXT_ID, new AttributeSetter(func, required)); return *this; } @@ -1065,6 +1210,19 @@ namespace ImVue { mInheritance.push_back(ImString(name)); return *this; } + + void registerAttribute(const char* id, Attribute* attr) + { + if(mAttributes.find(id) != mAttributes.end()) { + delete mAttributes[id]; + } + + mAttributes[id] = attr; + attr->owner = this; + if(attr->required) { + mRequiredAttrs.push_back(id); + } + } }; /** @@ -1154,7 +1312,7 @@ namespace ImVue { class EventHandler { public: - EventHandler(const char* handlerName, const char* script); + EventHandler(Element* element, const char* handlerName, const char* script); virtual ~EventHandler() {} inline const char* getScript() const { return mScript; } @@ -1173,6 +1331,7 @@ namespace ImVue { typedef std::unordered_map Properties; Properties mProperties; + Element* mElement; }; inline int modState(); @@ -1186,7 +1345,7 @@ namespace ImVue { Super = 1 << 3 }; - InputHandler(const char* handlerName, const char* script); + InputHandler(Element* element, const char* handlerName, const char* script); inline bool checkModifiers() { int current = modState(); return mExact ? (current == mModifiers) : ((current | mModifiers) || !mModifiers); @@ -1226,13 +1385,14 @@ namespace ImVue { enum Type { Click, + DoubleClick, MouseDown, MouseUp, MouseOver, MouseOut }; - MouseEventHandler(const char* handlerName, const char* script); + MouseEventHandler(Element* element, const char* handlerName, const char* script); protected: bool check(); private: @@ -1246,7 +1406,7 @@ namespace ImVue { */ class ChangeEventHandler : public EventHandler { public: - ChangeEventHandler(const char* handlerName, const char* script) : EventHandler(handlerName, script) {} + ChangeEventHandler(Element* element, const char* handlerName, const char* script) : EventHandler(element, handlerName, script) {} protected: bool check() { return ImGui::IsItemEdited(); @@ -1264,7 +1424,7 @@ namespace ImVue { Press }; - KeyboardEventHandler(const char* handlerName, const char* script); + KeyboardEventHandler(Element* element, const char* handlerName, const char* script); typedef std::unordered_map KeyMap; static KeyMap init(); @@ -1287,6 +1447,10 @@ namespace ImVue { ContainerElement(); virtual ~ContainerElement(); + inline const Elements& getChildren() const { + return mChildren; + } + /** * Selector that behaves the same way as jQuery $ */ @@ -1297,9 +1461,12 @@ namespace ImVue { int kind = 0; if(query[0] == '#') { // get by id kind = 1; + } else if(query[0] == '.') { // get by class + kind = 2; + } else if(query[0] == '*') { // get all + kind = 3; } - for(Elements::iterator iter = mChildren.begin(); iter != mChildren.end(); ++iter) { bool push = false; Element* e = *iter; @@ -1327,6 +1494,12 @@ namespace ImVue { } push = std::strcmp(&query[1], e->id) == 0; break; + case 2: + push = e->hasClass(&query[1]); + break; + case 3: + push = true; + break; } if(push) { @@ -1353,9 +1526,7 @@ namespace ImVue { void renderChildren(); - typedef std::vector Elements; - - Elements mChildren; + Layout mLayout; }; class PseudoElement : public ContainerElement { @@ -1405,6 +1576,87 @@ namespace ImVue { public: void configure(rapidxml::xml_node<>* node, Context* ctx, ScriptState::Context* sctx = 0, Element* parent = 0); }; + + /** + * Default element for text node + */ + class Text : public Element { + + public: + + Text() : text(0), mEstimatedSize(false) { + } + + virtual ~Text() + { + if(text) + ImGui::MemFree(text); + } + + bool build() + { + if(!mBuilder) { + mBuilder = mFactory->get(TEXT_NODE); + } + + bool res = Element::build(); + // always display as inline block + display = CSS_DISPLAY_INLINE; + return res; + } + + void renderBody() + { + if(!mParent || !text) { + IMVUE_EXCEPTION(ElementError, "text node must always be attached to some parent"); + return; + } + + if(!mEstimatedSize) { + ImGui::TextUnformatted(text); + mEstimatedSize = true; + } else { + bool popWrap = false; + if(mParent->size.x > 0) { + ImGui::PushTextWrapPos(mParent->pos.x + mParent->size.x); + popWrap = true; + } + + ImGui::TextWrapped("%s", text); + + if(popWrap) + ImGui::PopTextWrapPos(); + } + } + + void setText(char* value) + { + if(text) { + ImGui::MemFree(text); + text = 0; + } + + if(!value) { + return; + } + + text = ImStrdup(value); + } + + char* text; + + private: + + bool mEstimatedSize; + }; + + inline bool displayedInline(Element* element) { + if(!element) { + return false; + } + + return element->display == CSS_DISPLAY_INLINE_BLOCK || element->display == CSS_DISPLAY_INLINE; + } } #endif diff --git a/src/imvue_errors.h b/src/imvue_errors.h index 74eafda..ff008c2 100644 --- a/src/imvue_errors.h +++ b/src/imvue_errors.h @@ -24,6 +24,8 @@ SOFTWARE. #define __IMVUE_ERRORS_H__ #include +#include +#include "imgui.h" namespace ImVue { @@ -58,6 +60,17 @@ namespace ImVue { #else #define IMVUE_EXCEPTION(cls, ...) throw cls(format(__VA_ARGS__)) #endif + + /** + * Style related errors + */ + class StyleError : public std::runtime_error { + public: + StyleError(const std::string& detailedMessage) + : runtime_error("Style error: " + detailedMessage) + { + } + }; /** * Raised when script execution fails diff --git a/src/imvue_generated.h b/src/imvue_generated.h index f86ee0c..f33b57a 100644 --- a/src/imvue_generated.h +++ b/src/imvue_generated.h @@ -300,7 +300,6 @@ namespace ImVue { ImGui::InvisibleButton(str_id, size); } char* str_id; - ImVec2 size; }; class Combo : public ContainerElement { @@ -504,7 +503,7 @@ namespace ImVue { } } char* str_id; - int mouse_button; + ImGuiMouseButton mouse_button; bool also_over_items; }; @@ -552,7 +551,6 @@ namespace ImVue { } } ImGuiID id; - ImVec2 size; ImGuiWindowFlags flags; }; @@ -599,7 +597,7 @@ namespace ImVue { public: InputTextMultiline() - : label(NULL),buf(NULL),buf_size(512),size(ImVec2(0,0)),flags(0),callback(NULL),user_data(NULL) + : label(NULL),buf(NULL),buf_size(512),flags(0),callback(NULL),user_data(NULL) { buf = (char*)ImGui::MemAlloc(buf_size); buf[0] = '\0'; @@ -615,7 +613,6 @@ namespace ImVue { char* label; char* buf; size_t buf_size; - ImVec2 size; ImGuiInputTextFlags flags; ImGuiInputTextCallback callback; void* user_data; @@ -625,7 +622,7 @@ namespace ImVue { public: Selectable() - : label(NULL),selected(false),flags(0),size(ImVec2(0,0)) + : label(NULL),selected(false),flags(0) { } virtual ~Selectable() { @@ -638,7 +635,6 @@ namespace ImVue { char* label; bool selected; ImGuiSelectableFlags flags; - ImVec2 size; }; class DragDropTarget : public ContainerElement { @@ -727,7 +723,7 @@ namespace ImVue { public: Child() - : str_id(NULL),size(ImVec2(0,0)),border(false),flags(0) + : str_id(NULL),border(false),flags(0) { } virtual ~Child() { @@ -740,7 +736,6 @@ namespace ImVue { ImGui::EndChild(); } char* str_id; - ImVec2 size; bool border; ImGuiWindowFlags flags; }; @@ -780,7 +775,7 @@ namespace ImVue { } } char* str_id; - int mouse_button; + ImGuiMouseButton mouse_button; }; class InputInt : public Element { @@ -857,7 +852,6 @@ namespace ImVue { void renderBody() { ImGui::Dummy(size); } - ImVec2 size; }; class Unindent : public Element { @@ -904,7 +898,7 @@ namespace ImVue { public: Button() - : label(NULL),size(ImVec2(0,0)) + : label(NULL) { } virtual ~Button() { @@ -915,7 +909,6 @@ namespace ImVue { ImGui::Button(label, size); } char* label; - ImVec2 size; }; class InputFloat2 : public Element { @@ -1049,7 +1042,6 @@ namespace ImVue { ImGui::VSliderFloat(label, size, v, v_min, v_max, format, power); } char* label; - ImVec2 size; float* v; float v_min; float v_max; @@ -1122,7 +1114,7 @@ namespace ImVue { } } char* str_id; - int mouse_button; + ImGuiMouseButton mouse_button; }; class Columns : public Element { @@ -1148,7 +1140,7 @@ namespace ImVue { public: ColorButton() - : desc_id(NULL),flags(0),size(ImVec2(0,0)) + : desc_id(NULL),flags(0) { } virtual ~ColorButton() { @@ -1161,7 +1153,6 @@ namespace ImVue { char* desc_id; ImVec4 col; ImGuiColorEditFlags flags; - ImVec2 size; }; class SliderInt4 : public Element { @@ -1307,7 +1298,6 @@ namespace ImVue { ImGui::ImageButton(user_texture_id, size, uv0, uv1, frame_padding, bg_col, tint_col); } ImTextureID user_texture_id; - ImVec2 size; ImVec2 uv0; ImVec2 uv1; int frame_padding; @@ -1351,7 +1341,6 @@ namespace ImVue { ImGui::Image(user_texture_id, size, uv0, uv1, tint_col, border_col); } ImTextureID user_texture_id; - ImVec2 size; ImVec2 uv0; ImVec2 uv1; ImVec4 tint_col; @@ -1432,7 +1421,7 @@ namespace ImVue { public: ProgressBar() - : fraction(0.0f),size_arg(ImVec2(-1,0)),overlay(NULL) + : fraction(0.0f),overlay(NULL) { } virtual ~ProgressBar() { @@ -1440,10 +1429,9 @@ namespace ImVue { } void renderBody() { - ImGui::ProgressBar(fraction, size_arg, overlay); + ImGui::ProgressBar(fraction, size, overlay); } float fraction; - ImVec2 size_arg; char* overlay; }; @@ -1487,7 +1475,6 @@ namespace ImVue { ImGui::VSliderInt(label, size, &v, v_min, v_max, format); } char* label; - ImVec2 size; int v; int v_min; int v_max; @@ -1765,8 +1752,7 @@ namespace ImVue { .attribute("flags", &InputFloat3::flags); factory.element("invisible-button") - .attribute("str-id", &InvisibleButton::str_id, true) - .attribute("size", &InvisibleButton::size, true); + .attribute("str-id", &InvisibleButton::str_id, true); factory.element("combo") .attribute("label", &Combo::label, true) @@ -1836,7 +1822,6 @@ namespace ImVue { factory.element("child-frame") .attribute("id", &ChildFrame::id, true) - .attribute("size", &ChildFrame::size) .attribute("flags", &ChildFrame::flags); factory.element("plot-lines") @@ -1856,7 +1841,6 @@ namespace ImVue { .text(&InputTextMultiline::label) .attribute("buf", &InputTextMultiline::buf) .attribute("buf-size", &InputTextMultiline::buf_size) - .attribute("size", &InputTextMultiline::size) .attribute("flags", &InputTextMultiline::flags) .attribute("callback", &InputTextMultiline::callback) .attribute("user-data", &InputTextMultiline::user_data); @@ -1864,8 +1848,7 @@ namespace ImVue { factory.element("selectable") .text(&Selectable::label) .attribute("selected", &Selectable::selected) - .attribute("flags", &Selectable::flags) - .attribute("size", &Selectable::size); + .attribute("flags", &Selectable::flags); factory.element("drag-drop-target"); @@ -1886,7 +1869,6 @@ namespace ImVue { factory.element("child") .attribute("str-id", &Child::str_id, true) - .attribute("size", &Child::size) .attribute("border", &Child::border) .attribute("flags", &Child::flags); @@ -1913,8 +1895,7 @@ namespace ImVue { .attribute("str-id", &TabBar::str_id, true) .attribute("flags", &TabBar::flags); - factory.element("dummy") - .attribute("size", &Dummy::size); + factory.element("dummy"); factory.element("unindent") .attribute("indent-w", &Unindent::indent_w); @@ -1925,8 +1906,7 @@ namespace ImVue { factory.element("bullet"); factory.element("button") - .text(&Button::label, true) - .attribute("size", &Button::size); + .text(&Button::label, true); factory.element("input-float2") .text(&InputFloat2::label) @@ -1960,7 +1940,6 @@ namespace ImVue { factory.element("v-slider-float") .text(&VSliderFloat::label, true) - .attribute("size", &VSliderFloat::size) .attribute("v", &VSliderFloat::v, true) .attribute("v-min", &VSliderFloat::v_min) .attribute("v-max", &VSliderFloat::v_max) @@ -1993,8 +1972,7 @@ namespace ImVue { factory.element("color-button") .attribute("desc-id", &ColorButton::desc_id, true) .attribute("col", &ColorButton::col, true) - .attribute("flags", &ColorButton::flags) - .attribute("size", &ColorButton::size); + .attribute("flags", &ColorButton::flags); factory.element("slider-int4") .text(&SliderInt4::label) @@ -2039,7 +2017,6 @@ namespace ImVue { factory.element("image-button") .attribute("user-texture-id", &ImageButton::user_texture_id) - .attribute("size", &ImageButton::size) .attribute("uv0", &ImageButton::uv0) .attribute("uv1", &ImageButton::uv1) .attribute("frame-padding", &ImageButton::frame_padding) @@ -2057,7 +2034,6 @@ namespace ImVue { factory.element("image") .attribute("user-texture-id", &Image::user_texture_id) - .attribute("size", &Image::size) .attribute("uv0", &Image::uv0) .attribute("uv1", &Image::uv1) .attribute("tint-col", &Image::tint_col) @@ -2088,7 +2064,6 @@ namespace ImVue { factory.element("progress-bar") .attribute("fraction", &ProgressBar::fraction) - .attribute("size-arg", &ProgressBar::size_arg) .attribute("overlay", &ProgressBar::overlay); factory.element("drag-float2") @@ -2102,7 +2077,6 @@ namespace ImVue { factory.element("v-slider-int") .text(&VSliderInt::label) - .attribute("size", &VSliderInt::size) .attribute("v", &VSliderInt::v) .attribute("v-min", &VSliderInt::v_min) .attribute("v-max", &VSliderInt::v_max) @@ -2162,6 +2136,9 @@ namespace ImVue { .attribute("format-max", &DragIntRange2::format_max); + factory.element(TEXT_NODE) + .text(&Text::setText, true); + factory.element("collapsing-header") .attribute("label", &CollapsingHeader::label) .attribute("flags", &CollapsingHeader::flags); @@ -2170,6 +2147,7 @@ namespace ImVue { factory.element("template"); factory.element("__element__") .handler("click") + .handler("doubleclick") .handler("mousedown") .handler("mouseup") .handler("mouseover") @@ -2178,13 +2156,15 @@ namespace ImVue { .handler("keydown") .handler("keyup") .handler("keypress") + .attribute("size", &Element::size) .attribute("id", &Element::id) .attribute("key", &Element::key) - .attribute("ref", &Element::ref); + .attribute("ref", &Element::ref) + .setter("style", &Element::setInlineStyle); factory.element("slot"); return res; } } -#endif +#endif \ No newline at end of file diff --git a/src/imvue_layout.cpp b/src/imvue_layout.cpp new file mode 100644 index 0000000..a11eea5 --- /dev/null +++ b/src/imvue_layout.cpp @@ -0,0 +1,148 @@ + /* + Copyright (c) 2019-2020 Artem Chernyshev + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +#include "imvue_element.h" +#include "imvue_layout.h" + +namespace ImVue { + + bool skipLayout(Element* element) + { + return element->getFlags() & Element::WINDOW || !GetCurrentWindowNoDefault() || + element->style()->position == CSS_POSITION_FIXED || + element->style()->position == CSS_POSITION_ABSOLUTE; + } + + void Layout::beginElement(Element* element) + { + if(skipLayout(element)) { + return; + } + + if(!currentElement) { + lineEnd = ImGui::GetCursorPos(); + cursorStart = lineEnd; + } + + // move to the new line if current element is block + // or if it's inline but should be wrapped to the next line + if(element->display == CSS_DISPLAY_BLOCK || + (contentRegion.x > 0 && lineEnd.x + element->getSize().x > contentRegion.x)) { + newLine(); + } + + currentElement = element; + // initial position setup + ImVec2 pos = lineEnd; + // apply element margins + + ImGuiStyle& style = ImGui::GetStyle(); + // top + if(element->margins[0] == FLT_MIN) { + topMargin = style.ItemSpacing.y; + } else { + // always greater than 0 + topMargin = ImMax(element->margins[0], topMargin); + } + + // left + if(element->margins[1] == FLT_MIN) { + pos.x += style.ItemSpacing.y; + } else { + pos.x += element->margins[1]; + } + + pos.y += topMargin; + ImGui::SetCursorPos(pos); + lineEnd.x = pos.x; + } + + void Layout::endElement(Element* element) + { + if(skipLayout(element)) { + if(GetCurrentWindowNoDefault()) + ImGui::SetCursorPos(lineEnd); + return; + } + + IM_ASSERT(currentElement == element); + + // right + if(element->margins[2] != FLT_MIN) { + lineEnd.x += element->margins[2]; + } + + // bottom + if(element->margins[3] != FLT_MIN) { + bottomMargin = ImMax(element->margins[3], bottomMargin); + } + + ImVec2 size = element->getSize(); + lineEnd.x += size.x; + height = ImMax(size.y, height); + + if(element->display == CSS_DISPLAY_BLOCK) { + newLine(); + } else { + ImGui::SetCursorPos(ImVec2(lineEnd.x, cursorStart.y)); + } + + ++index; + } + + void Layout::newLine() + { + index = 0; + if(!currentElement) { + return; + } + + lineEnd.x = cursorStart.x; + lineEnd.y += height + topMargin + bottomMargin; + height = 0.0f; + topMargin = 0.0f; + bottomMargin = 0.0f; + } + + void Layout::begin(ContainerElement* element) + { + container = element; + contentRegion = element->size; + height = 0.0f; + topMargin = 0.0f; + bottomMargin = 0.0f; + currentElement = 0; + } + + void Layout::end() + { + ImVec2 max; + max.x = lineEnd.x; + newLine(); + max.y = lineEnd.y; + + ImGuiWindow* window = GetCurrentWindowNoDefault(); + if(window) { + window->DC.CursorMaxPos = ImMax(window->Pos - window->Scroll + max, window->DC.CursorMaxPos); + } + } +} diff --git a/src/imvue_layout.h b/src/imvue_layout.h new file mode 100644 index 0000000..019dafc --- /dev/null +++ b/src/imvue_layout.h @@ -0,0 +1,74 @@ +/* +Copyright (c) 2019-2020 Artem Chernyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef __IMVUE_LAYOUT__ +#define __IMVUE_LAYOUT__ + +namespace ImVue { + class Element; + class ContainerElement; + + struct Layout { + + float topMargin; // top margin max value + float bottomMargin; // bottom margin max value + float height; // total line height + + ImVec2 cursorStart; + ImVec2 lineEnd; + ImVec2 contentRegion; + + Element* currentElement; + ContainerElement* container; + + int index; + + /** + * Prepares layout for element rendering + * * sets cursor position + * * wraps line if needed + * * saves current draw list position + * + * @param element Next element + */ + void beginElement(Element* element); + + /** + * Finalizes element draw + * May do transform for the current draw list + * If item should be aligned or wrapped + * + * @param element Element that was just drawn + */ + void endElement(Element* element); + + void newLine(); + + void endLine(); + + void begin(ContainerElement* container); + + void end(); + }; +} + +#endif diff --git a/src/imvue_script.cpp b/src/imvue_script.cpp index ad30e8d..37cb077 100644 --- a/src/imvue_script.cpp +++ b/src/imvue_script.cpp @@ -58,6 +58,16 @@ namespace ImVue { return mObject->get(key); } + Object Object::operator[](int index) + { + return mObject->get(index); + } + + void Object::erase(const Object& key) + { + mObject->erase(key); + } + bool Object::valid() const { return mObject != 0; } @@ -85,7 +95,7 @@ namespace ImVue { return mObject->keys(dest); } - bool Object::set(void* value, ObjectType type) + bool Object::setValue(void* value, ObjectType type) { switch(type) { case ObjectType::OBJECT: @@ -107,6 +117,11 @@ namespace ImVue { case ObjectType::BOOLEAN: mObject->setBool(*reinterpret_cast(value)); break; + case ObjectType::VEC2: + mObject->initObject(); + (*this)["x"] = (*reinterpret_cast(value)).x; + (*this)["y"] = (*reinterpret_cast(value)).y; + break; default: return false; } diff --git a/src/imvue_script.h b/src/imvue_script.h index 8aded39..b74e508 100644 --- a/src/imvue_script.h +++ b/src/imvue_script.h @@ -52,7 +52,10 @@ namespace ImVue { OBJECT = 5, USERDATA = 6, FUNCTION = 7, - ARRAY = 8 + ARRAY = 8, + + // special cases + VEC2 = 9 }; #define DEFINE_TYPE_ID(cls, id) \ @@ -61,7 +64,7 @@ namespace ImVue { static ObjectType value() { return id; } \ } - template::value> + template::value && !std::is_same::value> struct typeID {}; DEFINE_TYPE_ID(bool, ObjectType::BOOLEAN); @@ -70,6 +73,7 @@ namespace ImVue { DEFINE_TYPE_ID(Object, ObjectType::OBJECT); DEFINE_TYPE_ID(double, ObjectType::NUMBER); DEFINE_TYPE_ID(float, ObjectType::NUMBER); + DEFINE_TYPE_ID(ImVec2, ObjectType::VEC2); template struct typeID { @@ -107,6 +111,16 @@ namespace ImVue { */ virtual Object get(const char* key) = 0; + /** + * Get object by index + */ + virtual Object get(int index) = 0; + + /** + * Delete object key + */ + virtual void erase(const Object& key) = 0; + /** * Call function */ @@ -144,6 +158,8 @@ namespace ImVue { virtual void setString(const char* value) = 0; virtual void setBool(bool value) = 0; + virtual void initObject() = 0; + }; /** * Abstract object representation @@ -166,6 +182,10 @@ namespace ImVue { Object operator[](const char* key); + Object operator[](int index); + + void erase(const Object& key); + bool valid() const; operator bool() const; @@ -180,7 +200,25 @@ namespace ImVue { inline const ObjectImpl* getImpl() const { return mObject.get(); } - bool set(void* value, ObjectType type); + template + typename std::enable_if::value, bool>::type set(C* value, ObjectType type) + { + long val = (long)(*value); + return setValue(&val, type); + } + + template + typename std::enable_if::value && !std::is_floating_point::value, bool>::type set(C* value, ObjectType type) + { + return setValue(value, type); + } + + template + typename std::enable_if::value, bool>::type set(C* value, ObjectType type) + { + double val = (double)(*value); + return setValue(&val, type); + } template C as() const { @@ -210,6 +248,8 @@ namespace ImVue { return *this; } private: + bool setValue(void* value, ObjectType type); + std::shared_ptr mObject; }; diff --git a/src/imvue_style.cpp b/src/imvue_style.cpp index e69de29..a29aee3 100644 --- a/src/imvue_style.cpp +++ b/src/imvue_style.cpp @@ -0,0 +1,1425 @@ +/* +MIT License + +Copyright (c) 2019-2020 Artem Chernyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "imvue_style.h" +#include "imvue_element.h" +#include "imvue.h" +#include "imvue_errors.h" +#include +#include "css/select.h" + +namespace ImVue { + +#define IM_NORMALIZE2F_OVER_ZERO(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.0f) { float inv_len = 1.0f / ImSqrt(d2); VX *= inv_len; VY *= inv_len; } } +#define IM_FIXNORMAL2F(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 < 0.5f) d2 = 0.5f; float inv_lensq = 1.0f / d2; VX *= inv_lensq; VY *= inv_lensq; } +#define AA_SIZE 1.0f +#define IM_SEGMENTS(rounding) (IM_PI * rounding) + + inline ImVec2 quadBezierCalc(ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) + { + return ImVec2( + (1 - t) * (1 - t) * p1.x + 2 * (1 - t) * t * p2.x + t * t * p3.x, + (1 - t) * (1 - t) * p1.y + 2 * (1 - t) * t * p2.y + t * t * p3.y + ); + } + + inline ImVec2 getNormal(ImVec2 p1, ImVec2 p2) + { + float dx = p2.x - p1.x; + float dy = p2.y - p1.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + return ImVec2(dy, -dx); + } + + void ComputedStyle::Decoration::drawJoint( + const ImVec2& p1, + const ImVec2& p2, + const ImVec2& p3, + const ImVec2& p1i, + const ImVec2& p2i, + const ImVec2& p3i, + ImU32 startColor, + ImU32 endColor, + int numSegments + ) + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImVec2 uv = drawList->_Data->TexUvWhitePixel; + const int pointsCount = numSegments + 1; + const int idxCount = numSegments * 18; + const int vtxCount = pointsCount * 4; + drawList->PrimReserve(idxCount, vtxCount); + ImVec2 normal; + ImVec2 outer = quadBezierCalc(p1, p2, p3, 0); + ImVec2 inner = quadBezierCalc(p1i, p2i, p3i, 0); + unsigned int idx1 = drawList->_VtxCurrentIdx; + + for(int i = 0; i < pointsCount; ++i) { + float t = (float)i/(float)numSegments; + + ImVec2 nextOuter = quadBezierCalc(p1, p2, p3, t); + ImVec2 nextInner = quadBezierCalc(p1i, p2i, p3i, t); + ImVec2 nextNormal = getNormal(outer, nextOuter); + float dmx = (nextNormal.x + normal.x) * 0.5f; + float dmy = (nextNormal.y + normal.y) * 0.5f; + IM_FIXNORMAL2F(dmx, dmy) + + dmx *= AA_SIZE; + dmy *= AA_SIZE; + + ImU32 col; + if(t < 0.5) { + col = startColor > 0 ? startColor : endColor; + } else { + col = endColor > 0 ? endColor : startColor; + } + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + + unsigned int idx2 = drawList->_VtxCurrentIdx; + + // Add vertices + drawList->_VtxWritePtr[0].pos = nextOuter + ImVec2(dmx, dmy) / 2; drawList->_VtxWritePtr[0].uv = uv; drawList->_VtxWritePtr[0].col = col_trans; + drawList->_VtxWritePtr[1].pos = nextOuter - ImVec2(dmx, dmy) / 2; drawList->_VtxWritePtr[1].uv = uv; drawList->_VtxWritePtr[1].col = col; + drawList->_VtxWritePtr[2].pos = nextInner + ImVec2(dmx, dmy) / 2; drawList->_VtxWritePtr[2].uv = uv; drawList->_VtxWritePtr[2].col = col; + drawList->_VtxWritePtr[3].pos = nextInner - ImVec2(dmx, dmy) / 2; drawList->_VtxWritePtr[3].uv = uv; drawList->_VtxWritePtr[3].col = col_trans; + drawList->_VtxWritePtr += 4; + drawList->_VtxCurrentIdx += 4; + + if(i == 0) { + continue; + } + + // Add indices + drawList->_IdxWritePtr[0] = (ImDrawIdx)(idx2+1); drawList->_IdxWritePtr[1] = (ImDrawIdx)(idx1+1); drawList->_IdxWritePtr[2] = (ImDrawIdx)(idx1+2); + drawList->_IdxWritePtr[3] = (ImDrawIdx)(idx1+2); drawList->_IdxWritePtr[4] = (ImDrawIdx)(idx2+2); drawList->_IdxWritePtr[5] = (ImDrawIdx)(idx2+1); + drawList->_IdxWritePtr[6] = (ImDrawIdx)(idx2+1); drawList->_IdxWritePtr[7] = (ImDrawIdx)(idx1+1); drawList->_IdxWritePtr[8] = (ImDrawIdx)(idx1+0); + drawList->_IdxWritePtr[9] = (ImDrawIdx)(idx1+0); drawList->_IdxWritePtr[10] = (ImDrawIdx)(idx2+0); drawList->_IdxWritePtr[11] = (ImDrawIdx)(idx2+1); + drawList->_IdxWritePtr[12] = (ImDrawIdx)(idx2+2); drawList->_IdxWritePtr[13] = (ImDrawIdx)(idx1+2); drawList->_IdxWritePtr[14] = (ImDrawIdx)(idx1+3); + drawList->_IdxWritePtr[15] = (ImDrawIdx)(idx1+3); drawList->_IdxWritePtr[16] = (ImDrawIdx)(idx2+3); drawList->_IdxWritePtr[17] = (ImDrawIdx)(idx2+2); + drawList->_IdxWritePtr += 18; + normal = nextNormal; + outer = nextOuter; + inner = nextInner; + + idx1 = idx2; + } + } + + void ComputedStyle::Decoration::drawJoint( + const ImVec2& p1, + const ImVec2& p2, + const ImVec2& p3, + const ImVec2& center, + ImU32 startColor, + ImU32 endColor, + const ImVec2& startPoint, + const ImVec2& endPoint, + int numSegments + ) + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + bool half = false; + drawList->PathLineTo(startPoint); + for(int i = 0; i < numSegments; ++i) { + float t1 = (float)i/(float)numSegments; + float t2 = (float)(i+1)/(float)numSegments; + + ImVec2 tr1 = quadBezierCalc(p1, p2, p3, t1); + ImVec2 tr2 = quadBezierCalc(p1, p2, p3, t2); + drawList->PathLineTo(tr1); + drawList->PathLineTo(tr2); + if(t1 > 0.5 && !half) { + drawList->PathLineTo(center); + drawList->PathFillConvex(startColor == 0 ? endColor : startColor); + drawList->PathLineTo(quadBezierCalc(p1, p2, p3, 0.5)); + half = true; + } + } + drawList->PathLineTo(endPoint); + drawList->PathLineTo(center); + drawList->PathFillConvex(endColor == 0 ? startColor : endColor); + } + + void ComputedStyle::Decoration::renderBackground(ImRect rect) + { + if(bgCol == 0) { + return; + } + + rect.Min -= ImVec2(thickness[0], thickness[1]) * 0.1f; + rect.Max += ImVec2(thickness[2], thickness[3]) * 0.1f; + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + float tlrx, tlry, trrx, trry, tbrx, tbry, tblx, tbly; + tlrx = ImMax(0.0f, rounding[0] - thickness[0]); + tlry = ImMax(0.0f, rounding[0] - thickness[1]); + + trrx = ImMax(0.0f, rounding[1] - thickness[2]); + trry = ImMax(0.0f, rounding[1] - thickness[1]); + + tbrx = ImMax(0.0f, rounding[2] - thickness[2]); + tbry = ImMax(0.0f, rounding[2] - thickness[3]); + + tblx = ImMax(0.0f, rounding[3] - thickness[0]); + tbly = ImMax(0.0f, rounding[3] - thickness[3]); + + drawList->PathLineTo(rect.Min + ImVec2(0, tlry)); + if(tlrx > 0 || tlry > 0) { + drawList->PathBezierCurveTo(rect.Min + ImVec2(0, tlry), + rect.Min, + rect.Min + ImVec2(tlrx, 0), + IM_SEGMENTS(rounding[0])); + } + drawList->PathLineTo(ImVec2(rect.Max.x - trrx, rect.Min.y)); + if(trrx > 0 || trry > 0) { + drawList->PathBezierCurveTo(ImVec2(rect.Max.x - trrx, rect.Min.y), + ImVec2(rect.Max.x, rect.Min.y), + ImVec2(rect.Max.x, rect.Min.y + trry), + IM_SEGMENTS(rounding[1])); + } + drawList->PathLineTo(ImVec2(rect.Max.x, rect.Max.y - tbry)); + + if(tbrx > 0 || tbry > 0) { + drawList->PathBezierCurveTo(ImVec2(rect.Max.x, rect.Max.y - tbry), + rect.Max, + ImVec2(rect.Max.x - tbrx, rect.Max.y), + IM_SEGMENTS(rounding[2])); + } + drawList->PathLineTo(ImVec2(rect.Min.x + tblx, rect.Max.y)); + + if(tblx > 0 || tbly > 0) { + drawList->PathBezierCurveTo(ImVec2(rect.Min.x + tblx, rect.Max.y), + ImVec2(rect.Min.x, rect.Max.y), + ImVec2(rect.Min.x, rect.Max.y - tbly), + IM_SEGMENTS(rounding[3])); + } + drawList->PathFillConvex(bgCol); + } + + void ComputedStyle::Decoration::render(ImRect rect) + { + ImVec2 s = rect.GetSize(); + if(s.x <= 0 && s.y <= 0) { + return; + } + // TODO: this function definitely has room for improvement + // 1. try to implement continuous flow for line when drawing using same color + // 2. loops ? + renderBackground(rect); + rect.Min.x -= thickness[0]; + rect.Min.y -= thickness[1]; + rect.Max.x += thickness[2]; + rect.Max.y += thickness[3]; + + ImVec2 corners[4] = { + rect.Min, + ImVec2(rect.Max.x, rect.Min.y), + rect.Max, + ImVec2(rect.Min.x, rect.Max.y) + }; + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + ImVec2 p1, p2, p3, p1i, p2i, p3i; + int numSegments; + + if(rounding[0] > 0 && (thickness[0] > 0 || thickness[1] > 0)) { + p1 = corners[0] + ImVec2(0, rounding[0]); + p2 = corners[0]; + p3 = corners[0] + ImVec2(rounding[0], 0); + + numSegments = IM_SEGMENTS(rounding[0]); + + if(rounding[0] > thickness[0] && rounding[0] > thickness[1]) { + p1i = corners[0] + ImVec2(thickness[0], rounding[0]); + p2i = corners[0] + ImVec2(thickness[0], thickness[1]); + p3i = corners[0] + ImVec2(rounding[0], thickness[1]); + drawJoint(p1, p2, p3, p1i, p2i, p3i, col[0], col[1], numSegments); + } else { + ImVec2 center = corners[0] + ImVec2(ImMax(rounding[0], thickness[0]), ImMax(rounding[0], thickness[1])); + ImVec2 startPoint = ImVec2(corners[0].x, center.y); + ImVec2 endPoint = ImVec2(center.x, corners[0].y); + + drawJoint(p1, p2, p3, center, col[0], col[1], startPoint, endPoint, numSegments); + } + } + + if(rounding[1] > 0 && (thickness[1] > 0 || thickness[2] > 0)) { + p1 = corners[1] - ImVec2(rounding[1], 0); + p2 = corners[1]; + p3 = corners[1] + ImVec2(0, rounding[1]); + + numSegments = IM_SEGMENTS(rounding[1]); + + if(rounding[1] > thickness[1] && rounding[1] > thickness[2]) { + p1i = corners[1] + ImVec2(-rounding[1], thickness[1]); + p2i = corners[1] + ImVec2(-thickness[2], thickness[1]); + p3i = corners[1] + ImVec2(-thickness[2], rounding[1]); + drawJoint(p1, p2, p3, p1i, p2i, p3i, col[1], col[2], numSegments); + } else { + ImVec2 center = corners[1] + ImVec2(-ImMax(rounding[1], thickness[2]), ImMax(rounding[1], thickness[1])); + ImVec2 startPoint = ImVec2(center.x, corners[1].y); + ImVec2 endPoint = ImVec2(corners[1].x, center.y); + + drawJoint(p1, p2, p3, center, col[1], col[2], startPoint, endPoint, numSegments); + } + } + + if(rounding[2] > 0 && (thickness[2] > 0 || thickness[3] > 0)) { + p1 = corners[2] - ImVec2(0, rounding[2]); + p2 = corners[2]; + p3 = corners[2] - ImVec2(rounding[2], 0); + + numSegments = IM_SEGMENTS(rounding[2]); + + if(rounding[2] > thickness[2] && rounding[2] > thickness[3]) { + p1i = corners[2] - ImVec2(thickness[2], rounding[2]); + p2i = corners[2] - ImVec2(thickness[2], thickness[3]); + p3i = corners[2] - ImVec2(rounding[2], thickness[3]); + drawJoint(p1, p2, p3, p1i, p2i, p3i, col[2], col[3], numSegments); + } else { + ImVec2 center = corners[2] - ImVec2(ImMax(rounding[2], thickness[2]), ImMax(rounding[2], thickness[3])); + ImVec2 startPoint = ImVec2(corners[2].x, center.y); + ImVec2 endPoint = ImVec2(center.x, corners[2].y); + + drawJoint(p1, p2, p3, center, col[2], col[3], startPoint, endPoint, numSegments); + } + } + + if(rounding[3] > 0 && (thickness[3] > 0 || thickness[0] > 0)) { + p1 = corners[3] + ImVec2(rounding[3], 0); + p2 = corners[3]; + p3 = corners[3] - ImVec2(0, rounding[3]); + + numSegments = IM_SEGMENTS(rounding[3]); + + if(rounding[3] > thickness[3] && rounding[3] > thickness[0]) { + p1i = corners[3] + ImVec2(rounding[3], -thickness[3]); + p2i = corners[3] + ImVec2(thickness[0], -thickness[3]); + p3i = corners[3] + ImVec2(thickness[0], -rounding[3]); + drawJoint(p1, p2, p3, p1i, p2i, p3i, col[3], col[0], numSegments); + } else { + ImVec2 center = corners[3] + ImVec2(ImMax(rounding[3], thickness[0]), -ImMax(rounding[3], thickness[3])); + ImVec2 startPoint = ImVec2(center.x, corners[3].y); + ImVec2 endPoint = ImVec2(corners[3].x, center.y); + + drawJoint(p1, p2, p3, center, col[3], col[0], startPoint, endPoint, numSegments); + } + } + + if(thickness[0] > 0) { + // left + drawList->PathLineTo(corners[3] + ImVec2(0, -rounding[3] + 0.5f)); + drawList->PathLineTo(corners[0] + ImVec2(0, rounding[0] - 0.5f)); + drawList->PathLineTo(corners[0] + ImVec2(thickness[0], ImMax(rounding[0] - 0.5f, thickness[1] - 0.5f))); + drawList->PathLineTo(corners[3] + ImVec2(thickness[0], -ImMax(rounding[3] - 0.5f, thickness[3] - 0.5f))); + drawList->PathFillConvex(col[0]); + } + + if(thickness[1] > 0) { + // top + drawList->PathLineTo(corners[0] + ImVec2(rounding[0] - 0.5f, 0)); + drawList->PathLineTo(corners[1] + ImVec2(-rounding[1] + 0.5f, 0)); + drawList->PathLineTo(corners[1] + ImVec2(-ImMax(rounding[1] - .5f, thickness[2] - 0.5f), thickness[1])); + drawList->PathLineTo(corners[0] + ImVec2(ImMax(rounding[0] -.5f, thickness[0] - 0.5f), thickness[1])); + drawList->PathFillConvex(col[1]); + } + + if(thickness[2] > 0) { + // right + drawList->PathLineTo(corners[1] + ImVec2(0, rounding[1] - .5f)); + drawList->PathLineTo(corners[2] + ImVec2(0, -rounding[2] + .5f)); + drawList->PathLineTo(corners[2] + ImVec2(-thickness[2], -ImMax(rounding[2] - .5f, thickness[3] - 0.5f))); + drawList->PathLineTo(corners[1] + ImVec2(-thickness[2], ImMax(rounding[1] - .5f, thickness[1]))); + drawList->PathFillConvex(col[2]); + } + + if(thickness[3] > 0) { + // bottom + drawList->PathLineTo(corners[2] + ImVec2(-rounding[2] + .5f, 0)); + drawList->PathLineTo(corners[3] + ImVec2(rounding[3] - .5f, 0)); + drawList->PathLineTo(corners[3] + ImVec2(ImMax(rounding[3] - .5f, thickness[0]), -thickness[3])); + drawList->PathLineTo(corners[2] + ImVec2(-ImMax(rounding[2] - .5f, thickness[2]), -thickness[3])); + drawList->PathFillConvex(col[3]); + } + } + + ImGuiWindow* GetCurrentWindowNoDefault() + { + if(!ImGui::GetCurrentContext()) { + return 0; + } + + ImGuiWindow* window = ImGui::GetCurrentWindowRead(); + if(window && strcmp(window->Name, "Debug##Default") == 0) { + return 0; + } + + return window; + } + + /* Table of function pointers for the LibCSS Select API. */ + static css_select_handler selectHandler = { + CSS_SELECT_HANDLER_VERSION_1, + + node_name, + node_classes, + node_id, + named_ancestor_node, + named_parent_node, + named_sibling_node, + named_generic_sibling_node, + parent_node, + sibling_node, + node_has_name, + node_has_class, + node_has_id, + node_has_attribute, + node_has_attribute_equal, + node_has_attribute_dashmatch, + node_has_attribute_includes, + node_has_attribute_prefix, + node_has_attribute_suffix, + node_has_attribute_substring, + node_is_root, + node_count_siblings, + node_is_empty, + node_is_link, + node_is_visited, + node_is_hover, + node_is_active, + node_is_focus, + node_is_enabled, + node_is_disabled, + node_is_checked, + node_is_target, + node_is_lang, + node_presentational_hint, + ua_default_for_property, + compute_font_size, + set_libcss_node_data, + get_libcss_node_data + }; + + float parseUnits(css_fixed value, css_unit unit, ComputedStyle& style, ParseUnitsAxis axis) + { + return parseUnits(value, unit, style, style.contentRegion, axis); + } + + //typedef float (*parseUnitsImpl)(css_fixed value, css_unit unit, ComputedStyle& style, const ImVec2& parentSize, ParseUnitsAxis axis); + //static parseUnitsImpl unitParsers[CSS_UNIT_COUNT + + float parseUnits(css_fixed value, css_unit unit, ComputedStyle& style, const ImVec2& parentSize, ParseUnitsAxis axis) { + + float res = FIXTOFLT(value); + + ImVec2& scales = style.context->scale; + + float scale = axis == ParseUnitsAxis::Y ? scales.y : scales.x; + float parentLength = axis == ParseUnitsAxis::Y ? parentSize.y : parentSize.x; + Element* root = style.context->root; + while(root->getParent() != 0 && root) { + root = root->getParent()->context()->root; + } + + switch(unit) { + case CSS_UNIT_PCT: + if(parentLength == 0) { + return res; + } + + res = parentLength * res / 100; + break; + + case CSS_UNIT_EM: + res = style.fontSize * res; + break; + + case CSS_UNIT_REM: + res = root->style()->fontSize * res; + break; + + case CSS_UNIT_IN: + res = DEFAULT_DPI * res * scale; + break; + + case CSS_UNIT_CM: + res = DEFAULT_DPI / 2.54 * res * scale; + break; + + default: + res *= scale; + break; + + } + + return res; + } + + bool setDimensions(ComputedStyle& cs) + { + bool changed = false; + int width = 0; + int height = 0; + + ImVec2 res(cs.element->size.x, cs.element->size.y); + css_unit unit = CSS_UNIT_PX; + + uint8_t type = css_computed_width( + cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], + &width, &unit + ); + + if(type == CSS_WIDTH_SET) { + res.x = parseUnits(width, unit, cs, ParseUnitsAxis::X); + changed = true; + } + + type = css_computed_height( + cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], + &height, &unit + ); + + if(type == CSS_HEIGHT_SET) { + res.y = parseUnits(height, unit, cs, ParseUnitsAxis::Y); + changed = true; + } + + cs.element->setSize(res); + return changed; + } + + bool setPosition(ComputedStyle& cs) + { + bool changed = false; + ImVec2 pos; + ImVec2 offset(cs.element->padding[0], cs.element->padding[1]); + ImVec2 parentSize = cs.contentRegion; + uint16_t position = css_computed_position(cs.style->styles[CSS_PSEUDO_ELEMENT_NONE]); + cs.position = position; + ImGuiWindow* window = 0; + Element* parent = cs.element->getParent(); + + switch(position) { + case CSS_POSITION_ABSOLUTE: + window = GetCurrentWindowNoDefault(); + if(window) { + if(parent && parent->style()->position == CSS_POSITION_RELATIVE) { + pos = window->Pos + parent->pos; + } else { + pos = window->Pos; + } + break; + } + case CSS_POSITION_FIXED: + parentSize = ImGui::GetIO().DisplaySize; + break; + case CSS_POSITION_RELATIVE: + pos = cs.elementScreenPosition; + break; + } + + ImVec2 elementSize = cs.element->getSize(); + css_fixed x = 0; + css_fixed y = 0; + css_unit unit = CSS_UNIT_PX; + uint16_t leftType = css_computed_left( + cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], + &x, &unit); + + if(leftType == CSS_LEFT_SET) { + offset.x = parseUnits(x, unit, cs, parentSize, ParseUnitsAxis::X); + changed = true; + } + + uint16_t topType = css_computed_top( + cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], + &y, &unit); + + if(topType == CSS_TOP_SET) { + offset.y = parseUnits(y, unit, cs, parentSize, ParseUnitsAxis::Y); + changed = true; + } + + css_fixed bottom = 0; + css_fixed right = 0; + ImVec2 size(cs.element->size.x, cs.element->size.y); + if(css_computed_right( + cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], + &right, &unit) == CSS_RIGHT_SET) { + if(position == CSS_POSITION_RELATIVE) { + offset.x -= parseUnits(x, unit, cs, parentSize, ParseUnitsAxis::X); + } else { + if(leftType == CSS_LEFT_SET) { + size.x = parentSize.x - offset.x - parseUnits(right, unit, cs, parentSize, ParseUnitsAxis::X); + } else { + offset.x = parentSize.x - elementSize.x - parseUnits(right, unit, cs, parentSize, ParseUnitsAxis::X); + } + } + changed = true; + } + + if(css_computed_bottom( + cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], + &bottom, &unit) == CSS_BOTTOM_SET) { + + if(position == CSS_POSITION_RELATIVE) { + offset.y = -parseUnits(y, unit, cs, parentSize, ParseUnitsAxis::Y); + } else { + if(topType == CSS_TOP_SET) { + size.y = parentSize.y - offset.y - parseUnits(bottom, unit, cs, parentSize, ParseUnitsAxis::Y); + } else { + offset.y = parentSize.y - elementSize.y - parseUnits(bottom, unit, cs, parentSize, ParseUnitsAxis::Y); + } + } + + changed = true; + } + + if(cs.element->getFlags() & Element::WINDOW) { + ImGui::SetNextWindowPos(pos + offset); + } else { + ImGui::SetCursorScreenPos(pos + offset); + } + cs.element->setSize(size); + return changed; + } + + bool setColor(ComputedStyle& cs) + { + css_color color; + if(css_computed_color( + cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], + &color) != CSS_COLOR_COLOR) { + return false; + } + + if(color == 0) { + return false; + } + ImGui::PushStyleColor(ImGuiCol_Text, parseColor(color)); + cs.nCol++; + return true; + } + + bool setBackgroundColor(ComputedStyle& cs) + { + css_color color; + uint16_t type = css_computed_background_color( + cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], + &color); + + if(color == 0 || type != CSS_BACKGROUND_COLOR_COLOR) { + return false; + } + + ImGuiCol col; + if(cs.element->getFlags() & Element::WINDOW) { + col = ImGuiCol_WindowBg; + } else { + col = ImGuiCol_ChildBg; + } + cs.element->bgColor = cs.decoration.bgCol = parseColor(color); + ImGui::PushStyleColor(col, parseColor(color)); + cs.nCol++; + return true; + } + + struct SpacingFunc { + uint8_t (*func)(const css_computed_style*, css_fixed*, css_unit*); + ParseUnitsAxis axis; + }; + + bool setPadding(ComputedStyle& cs) + { + css_fixed value = 0; + css_unit unit = CSS_UNIT_PX; + + SpacingFunc funcs[4] = { + SpacingFunc{css_computed_padding_left, ParseUnitsAxis::X}, + SpacingFunc{css_computed_padding_top, ParseUnitsAxis::Y}, + SpacingFunc{css_computed_padding_right, ParseUnitsAxis::X}, + SpacingFunc{css_computed_padding_bottom, ParseUnitsAxis::Y} + }; + + bool defined = false; + + for(size_t i = 0; i < 4; i++) { + if(funcs[i].func(cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], &value, &unit) == CSS_PADDING_SET) + { + float res = parseUnits(value, unit, cs, funcs[i].axis); + cs.element->padding[i] = res; + defined = true; + } + } + + if(defined) { + ImGuiStyle& style = ImGui::GetStyle(); + ImVec2 windowPadding = style.WindowPadding; + ImVec2 framePadding = style.FramePadding; + + if(cs.element->padding[0] >= 0.0f) { + windowPadding.x = cs.element->padding[0]; + framePadding.x = cs.element->padding[0]; + } + + if(cs.element->padding[1] >= 0.0f) { + windowPadding.y = cs.element->padding[1]; + framePadding.y = cs.element->padding[1]; + } + + if(cs.element->getFlags() & Element::WINDOW) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, windowPadding); + } else { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, framePadding); + } + ++cs.nStyle; + } + return defined; + } + + bool setMargins(ComputedStyle& cs) + { + css_fixed value = 0; + css_unit unit = CSS_UNIT_PX; + + SpacingFunc funcs[4] = { + SpacingFunc{css_computed_margin_top, ParseUnitsAxis::Y}, + SpacingFunc{css_computed_margin_left, ParseUnitsAxis::X}, + SpacingFunc{css_computed_margin_right, ParseUnitsAxis::X}, + SpacingFunc{css_computed_margin_bottom, ParseUnitsAxis::Y} + }; + + bool defined = false; + + for(size_t i = 0; i < 4; i++) { + if(funcs[i].func(cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], &value, &unit) == CSS_MARGIN_SET) + { + float res = parseUnits(value, unit, cs, funcs[i].axis); + cs.element->margins[i] = res; + defined = true; + } else { + cs.element->margins[i] = FLT_MIN; + } + } + + return defined; + } + + bool setFont(ComputedStyle& cs) + { + const char* fontName = cs.fontName; + + cs.fontScale = ImGui::GetIO().FontGlobalScale; + int fonts = 0; + + css_fixed fs; + css_unit unit; + uint8_t code = css_computed_font_size(cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], &fs, &unit); + + float size, defaultFontSize; + size = defaultFontSize = ImGui::GetFont()->FontSize; + + if(fontName) { + defaultFontSize = cs.element->context()->fontManager->getFont(fontName).size; + } + + Element* parent = cs.element->getParent(); + if(parent) { + cs.fontSize = parent->style()->fontSize; + } else { + cs.fontSize = defaultFontSize; + } + + if(code == CSS_FONT_SIZE_INHERIT) { + float inherited = parent ? parent->style()->fontSize : 0; + if(inherited > 0) { + size = inherited; + } + } else { + size = parseUnits(fs, unit, cs, ParseUnitsAxis::Y); + } + + if(size == 0) { + return false; + } + + float expectedScale = size / defaultFontSize; + bool changed = expectedScale != ImGui::GetIO().FontGlobalScale; + if(changed) { + ImGui::GetIO().FontGlobalScale = expectedScale; + } + + if(fontName) { + if(cs.element->context()->fontManager->pushFont(fontName)) { + fonts = 1; + } + } + + if(fonts == 0 && changed) { + ImGui::PushFont(ImGui::GetFont()); + fonts = 1; + } + + cs.fontSize = size; + cs.nFonts += fonts; + return true; + } + + typedef uint8_t(*BorderRadiusFunc)(const css_computed_style*, css_fixed*, css_unit*); + + bool setBorderRadius(ComputedStyle& cs) + { + css_fixed value; + css_unit unit; + bool changed = false; + BorderRadiusFunc funcs[4] = { + css_computed_border_radius_top_left, + css_computed_border_radius_top_right, + css_computed_border_radius_bottom_right, + css_computed_border_radius_bottom_left + }; + + for(size_t i = 0; i < 4; i++) { + if(funcs[i](cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], &value, &unit) == CSS_BORDER_RADIUS_SET) + { + cs.decoration.rounding[i] = parseUnits(value, unit, cs, ParseUnitsAxis::X); + changed = true; + } else { + cs.decoration.rounding[i] = 0.0f; + } + } + + cs.borderRadius.x = cs.decoration.rounding[0]; + cs.borderRadius.y = cs.decoration.rounding[1]; + cs.borderRadius.z = cs.decoration.rounding[2]; + cs.borderRadius.w = cs.decoration.rounding[3]; + return changed; + } + + typedef uint8_t(*BorderStyleFunc)(const css_computed_style*); + typedef uint8_t(*BorderWidthFunc)(const css_computed_style*, css_fixed*, css_unit*); + typedef uint8_t(*BorderColorFunc)(const css_computed_style*, css_color*); + + bool setBorder(ComputedStyle& cs) + { + bool changed = false; + BorderStyleFunc styles[4] = { + css_computed_border_left_style, + css_computed_border_top_style, + css_computed_border_right_style, + css_computed_border_bottom_style + }; + + BorderWidthFunc widths[4] = { + css_computed_border_left_width, + css_computed_border_top_width, + css_computed_border_right_width, + css_computed_border_bottom_width + }; + + BorderColorFunc colors[4] = { + css_computed_border_left_color, + css_computed_border_top_color, + css_computed_border_right_color, + css_computed_border_bottom_color + }; + + for(size_t i = 0; i < 4; i++) { + uint8_t type = styles[i](cs.style->styles[CSS_PSEUDO_ELEMENT_NONE]); + if(type == CSS_BORDER_STYLE_NONE || type == CSS_BORDER_STYLE_INHERIT) { + continue; + } + + css_fixed value; + css_unit unit; + css_color col; + widths[i](cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], &value, &unit); + colors[i](cs.style->styles[CSS_PSEUDO_ELEMENT_NONE], &col); + + cs.decoration.thickness[i] = ImMax(1.0f, parseUnits(value, unit, cs, ParseUnitsAxis::X)); + cs.decoration.col[i] = parseColor(col); + changed = true; + int marginIndex = i < 2 ? (i + 1) % 2 : i; + float m = cs.element->margins[marginIndex]; + cs.element->margins[marginIndex] = m != FLT_MIN ? m + cs.decoration.thickness[i] : cs.decoration.thickness[i]; + } + + return changed; + } + + ComputedStyle::ComputedStyle(Element* target) + : libcssData(0) + , fontName(0) + , fontSize(0) + , element(target) + , style(0) + , context(0) + , nCol(0) + , nStyle(0) + , nFonts(0) + , fontScale(0) + , mSelectCtx(0) + , mWidthMode(CSS_WIDTH_AUTO) + , mHeightMode(CSS_HEIGHT_AUTO) + , mInlineStyle(0) + , mAutoSize(false) + { + memset(&decoration, 0, sizeof(Decoration)); + } + + ComputedStyle::~ComputedStyle() + { + if(fontName) { + ImGui::MemFree(fontName); + } + if(mInlineStyle) { + css_stylesheet_destroy(mInlineStyle); + } + cleanupClassCache(); + destroy(); + } + + bool ComputedStyle::compute(Element* e) + { + element = e; + context = e->context(); + css_error code; + + css_media media; + memset(&media, 0, sizeof(css_media)); + media.type = CSS_MEDIA_SCREEN; + if(mSelectCtx) { + css_select_ctx_destroy(mSelectCtx); + } + + mSelectCtx = element->context()->style->select(); + if(!mSelectCtx) { + return false; + } + + css_select_results* newStyle = 0; + code = css_select_style(mSelectCtx, element, + &media, mInlineStyle, + &selectHandler, + &selectHandler, + &newStyle + ); + + if(style && memcmp(newStyle, style, sizeof(css_select_results)) == 0) { + if(css_select_results_destroy(newStyle) != CSS_OK) { + IMVUE_EXCEPTION(StyleError, "failed to destroy tmp style %s", css_error_to_string(code)); + } + return false; + } + + if(style) { + if(css_select_results_destroy(style) != CSS_OK) { + IMVUE_EXCEPTION(StyleError, "failed to destroy style %s", css_error_to_string(code)); + } + } + + style = newStyle; + + if (code != CSS_OK) { + IMVUE_EXCEPTION(StyleError, "failed to select style %s", css_error_to_string(code)); + return false; + } + + mStyleCallbacks.clear(); + + position = CSS_POSITION_INHERIT; + uint16_t display = css_computed_display(style->styles[CSS_PSEUDO_ELEMENT_NONE], false); + + // fonts should go first as line height might affect units parser + initFonts(&media); + + for(size_t i = 0; i < 4; i++) { + element->padding[i] = -1.0f; + element->margins[i] = FLT_MIN; + } + + // dimensions + { + css_fixed width = 0; + css_fixed height = 0; + css_unit unit = CSS_UNIT_PX; + mWidthMode = css_computed_width( + style->styles[CSS_PSEUDO_ELEMENT_NONE], + &width, &unit + ); + + mHeightMode = css_computed_height( + style->styles[CSS_PSEUDO_ELEMENT_NONE], + &height, &unit + ); + + bool shouldSetDimensions = mWidthMode == CSS_WIDTH_SET || mHeightMode == CSS_HEIGHT_SET; + + if(shouldSetDimensions && display != CSS_DISPLAY_INLINE) + mStyleCallbacks.push_back(setDimensions); + + if(shouldSetDimensions) { + element->size = ImVec2(0.f, 0.f); + element->invalidate("size"); + } + } + + // positioning + { + position = css_computed_position(style->styles[CSS_PSEUDO_ELEMENT_NONE]); + + switch(position) { + case CSS_POSITION_FIXED: + case CSS_POSITION_ABSOLUTE: + display = CSS_DISPLAY_BLOCK; + case CSS_POSITION_RELATIVE: + mStyleCallbacks.push_back(setPosition); + mAutoSize = false; + break; + default: + mAutoSize = true; + } + } + + // color + if(setColor(*this)) { + mStyleCallbacks.push_back(setColor); + } + + // background color + if(setBackgroundColor(*this)) { + mStyleCallbacks.push_back(setBackgroundColor); + } + + // padding + if(display != CSS_DISPLAY_INLINE) { + if(setPadding(*this)) { + mStyleCallbacks.push_back(setPadding); + } + } + + // margins + { + if(setMargins(*this)) { + mStyleCallbacks.push_back(setMargins); + } + } + + // border radius + if(setBorderRadius(*this)) { + mStyleCallbacks.push_back(setBorderRadius); + } + + // border styles + if(setBorder(*this)) { + mStyleCallbacks.push_back(setBorder); + } + + element->display = css_computed_display(style->styles[CSS_PSEUDO_ELEMENT_NONE], false); + end(); + + return true; + } + + void ComputedStyle::begin(Element* e) + { + element = e; + if(!style) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + ImGuiWindow* window = GetCurrentWindowNoDefault(); + if(window){ + Element* parent = e->getParent(); + while(parent && parent->display == CSS_DISPLAY_INLINE) { + parent = parent->getParent(); + } + + if(parent && (parent->getFlags() & Element::WINDOW) == 0){ + contentRegion = parent->getSize() - ImVec2(parent->padding[0] + parent->padding[2], parent->padding[1] + parent->padding[3]); + } else { + contentRegion = ImGui::GetWindowSize() - ImGui::GetCursorStartPos() - window->Scroll - ImGui::GetStyle().WindowPadding; + } + } else { + contentRegion = io.DisplaySize; + } + + elementScreenPosition = ImGui::GetCursorScreenPos(); + + for(int i = 0; i < mStyleCallbacks.size(); i++) + { + mStyleCallbacks[i](*this); + } + + if(element->display == CSS_DISPLAY_BLOCK && mAutoSize) { + if(mWidthMode == CSS_WIDTH_AUTO) { + ImRect margins = element->getMarginsRect(); + element->size.x = contentRegion.x - margins.Max.x - margins.Min.x; + } + } + } + + void ComputedStyle::end() + { + ImGui::PopStyleColor(nCol); + ImGui::PopStyleVar(nStyle); + if(fontScale) { + ImGui::GetIO().FontGlobalScale = fontScale; + } + while(nFonts-- > 0) { + ImGui::PopFont(); + } + nCol = 0; + nStyle = 0; + nFonts = 0; + fontScale = 0; + } + + void ComputedStyle::destroy() + { + end(); + if (libcssData != 0) { + css_libcss_node_data_handler(&selectHandler, CSS_NODE_DELETED, + NULL, element, NULL, libcssData); + libcssData = 0; + } + + if(!style) { + return; + } + + if(mSelectCtx) { + css_select_ctx_destroy(mSelectCtx); + } + + css_error code = css_select_results_destroy(style); + style = 0; + if (code != CSS_OK) { + IMVUE_EXCEPTION(StyleError, "failed to destroy style %s", css_error_to_string(code)); + } + } + + void ComputedStyle::updateClassCache() + { + cleanupClassCache(); + for(Element::Classes::iterator iter = element->classes.begin(); iter != element->classes.end(); ++iter) { + lwc_string* str = 0; + if(lwc_intern_string(iter->first, strlen(iter->first), &str) != lwc_error_ok) { + continue; + } + + mClasses.push_back(str); + } + } + + void ComputedStyle::setInlineStyle(const char* style) + { + if(mInlineStyle) { + css_stylesheet_destroy(mInlineStyle); + mInlineStyle = 0; + } + + if(style[0] == '\0') { + return; + } + + element->context()->style->parse(style, &mInlineStyle, true); + } + + ImVector& ComputedStyle::getClasses() + { + return mClasses; + } + + void ComputedStyle::initFonts(css_media* media) + { + lwc_string** fontNames = NULL; + css_computed_font_family(style->styles[CSS_PSEUDO_ELEMENT_NONE], &fontNames); + css_error code; + + if(fontNames) { + for(int i = 0;;i++) { + if(!fontNames[i]) { + break; + } + + const char* data = lwc_string_data(fontNames[i]); + if(fontName && strcmp(fontName, data) == 0) + break; + + css_select_font_faces_results* fontFaces = NULL; + css_select_font_faces(mSelectCtx, + media, fontNames[i], + &fontFaces); + + if(fontFaces && fontFaces->n_font_faces != 0) { + for(uint32_t j = 0; j < fontFaces->n_font_faces; j++) { + css_font_face* fontFace = fontFaces->font_faces[j]; + uint32_t count = 0; + code = css_font_face_count_srcs(fontFace, &count); + if(code != CSS_OK) { + continue; + } + + uint32_t rangeCount = 0; + code = css_font_face_count_ranges(fontFace, &rangeCount); + + if(code != CSS_OK) { + continue; + } + + for(uint32_t k = 0; k < count; k++) { + const css_font_face_src* src = NULL; + code = css_font_face_get_src(fontFace, k, &src); + if(code != CSS_OK) { + IMVUE_EXCEPTION(StyleError, "failed to get font src %s", css_error_to_string(code)); + continue; + } + + css_font_face_location_type location = css_font_face_src_location_type(src); + css_font_face_format fmt = css_font_face_src_format(src); + + // currently we support only ttf and otf + // unspecified falls back to otf/ttf + if(fmt != CSS_FONT_FACE_FORMAT_OPENTYPE && fmt != CSS_FONT_FACE_FORMAT_UNSPECIFIED) { + IMVUE_EXCEPTION(StyleError, "unsupported font format %d", fmt); + continue; + } + + // only local location is supported + if(location == CSS_FONT_FACE_LOCATION_TYPE_URI) { + IMVUE_EXCEPTION(StyleError, "uri location is not supported"); + continue; + } + + lwc_string* path = NULL; + code = css_font_face_src_get_location(src, &path); + if(code != CSS_OK) { + IMVUE_EXCEPTION(StyleError, "failed to get font src location %s", css_error_to_string(code)); + continue; + } + + if(fontName != 0) { + ImGui::MemFree(fontName); + fontName = 0; + } + + // read ranges + const css_unicode_range* range = NULL; + ImVector glyphRanges; + int index = 0; + if(rangeCount > 0) { + glyphRanges.resize(rangeCount * 2 + 1); + for(uint8_t l = 0; l < rangeCount; ++l) { + code = css_font_face_get_range(fontFace, l, &range); + glyphRanges[index++] = (ImWchar)(*range)[0]; + glyphRanges[index++] = (ImWchar)(*range)[1]; + } + } + + // finally load font using specified path + if(element->context()->fontManager->loadFontFromFile(data, lwc_string_data(path), glyphRanges)) { + fontName = ImStrdup(data); + } else { + IMVUE_EXCEPTION(StyleError, "failed to load font %s", data); + } + } + } + + css_select_font_faces_results_destroy(fontFaces); + } + } + } + + css_fixed fs; + css_unit unit = CSS_UNIT_PX; + uint8_t fstype = css_computed_font_size(style->styles[CSS_PSEUDO_ELEMENT_NONE], &fs, &unit); + + if(fontName || fstype != CSS_FONT_SIZE_INHERIT) { + fontSize = parseUnits(fs, unit, *this, ParseUnitsAxis::Y); + mStyleCallbacks.push_back(setFont); + } + + if(fstype == CSS_FONT_SIZE_INHERIT) { + Element* parent = element->getParent(); + float defaultFontSize = ImGui::GetFont() ? ImGui::GetFont()->FontSize : 0.0f; + fontSize = parent ? parent->style()->fontSize : defaultFontSize; + } + } + + void ComputedStyle::cleanupClassCache() + { + while(mClasses.size() > 0) { + lwc_string_unref(mClasses[mClasses.size() - 1]); + mClasses.pop_back(); + } + } + + const char* cssBase = "* {" + "display: block;" + "}\n" + "html {" + "width: 100%;" + "height: 100%;" + "}" + "body {" + "width: 100%;" + "height: 100%;" + "}\n" + "h1, h2, h3, h4, h5, h6 {" + "font-weight: bold;" + "}\n" + "h1 { font-size: 2em; margin: 0.67em 0 }\n" + "h2 { font-size: 1.5em; margin: 0.75em 0 }\n" + "h3 { font-size: 1.17em; margin: 0.83em 0 }\n" + "h4 { font-size: 1em; margin: 1.33em 0 }\n" + "h5 { font-size: 0.83em; margin: 1.67em 0 }\n" + "h6 { font-size: 0.67em; margin: 2.33em 0 }\n" + // define inline elements + "a, abbr, acronym, b, bdi, bdo, big, br, button, cite, code, " + "data, datalist, del, dfn, em, embed, i, iframe, img, input, " + "ins, kbd, label, map, mark, meter, noscript, object, output, " + "picture, progress, q, ruby, s, samp, script, select, " + "select, small, span, strong, sub, sup, svg, textarea, time, u, tt, var, wbr" + // imgui items that should be displayed inline + ", menu, same-line, tab-item" + "{" + "display: inline;" + "}" + // imgui items that are actually positioned absolute + "tooltip, popup-modal, indent, unindent, next-column {" + "position: fixed;" + "}"; + + + Style::Style(Style* parent) + : mBase(0) + , mParent(parent) + { + parse(cssBase, &mBase); + } + + Style::~Style() + { + while(mSheets.size() > 0) { + css_stylesheet_destroy(mSheets[mSheets.size() - 1].sheet); + mSheets.pop_back(); + } + + if(mBase) { + css_stylesheet_destroy(mBase); + } + } + + css_select_ctx* Style::select() + { + css_select_ctx* ctx = 0; + css_error code = css_select_ctx_create(&ctx); + if (code != CSS_OK) { + IMVUE_EXCEPTION(StyleError, "failed to create select context %s", css_error_to_string(code)); + return 0; + } + + code = css_select_ctx_append_sheet(ctx, mBase, CSS_ORIGIN_AUTHOR, + NULL); + if (code != CSS_OK) + IMVUE_EXCEPTION(StyleError, "failed to append base stylesheet: %s", css_error_to_string(code)); + + if(mParent) + mParent->appendSheets(ctx, false); + appendSheets(ctx, true); + return ctx; + } + + void Style::load(const char* data, bool scoped) + { + css_stylesheet* sheet = NULL; + parse(data, &sheet); + if(sheet) { + mSheets.push_back( + Sheet{ + sheet, + scoped + }); + } + } + + void Style::appendSheets(css_select_ctx* ctx, bool scoped) + { + for(int i = 0; i < mSheets.size(); ++i) { + if(mSheets[i].scoped && !scoped) { + continue; + } + + css_error code = css_select_ctx_append_sheet(ctx, mSheets[i].sheet, CSS_ORIGIN_AUTHOR, NULL); + if(code != CSS_OK) + { + IMVUE_EXCEPTION(StyleError, "failed to append stylesheet: %s", css_error_to_string(code)); + } + } + } + + void Style::parse(const char* data, css_stylesheet** dest, bool isInline) + { + css_stylesheet* sheet = *dest; + css_error code; + if(sheet) { + code = css_stylesheet_destroy(sheet); + if(code != CSS_OK) { + IMVUE_EXCEPTION(StyleError, "failed to destroy stylesheet: %s", css_error_to_string(code)); + } + } + + css_stylesheet_params params; + params.params_version = CSS_STYLESHEET_PARAMS_VERSION_1; + params.level = CSS_LEVEL_21; + params.charset = "UTF-8"; + params.url = "-"; + params.title = "-"; + params.allow_quirks = false; + params.inline_style = isInline; + params.resolve = resolve_url; + params.resolve_pw = NULL; + params.import = NULL; + params.import_pw = NULL; + params.color = NULL; + params.color_pw = NULL; + params.font = resolve_font; + params.font_pw = NULL; + code = css_stylesheet_create(¶ms, &sheet); + if (code != CSS_OK) + IMVUE_EXCEPTION(StyleError, "failed to create stylesheet: %s", css_error_to_string(code)); + code = css_stylesheet_append_data(sheet, (const uint8_t *) data, + strlen(data)); + if (code != CSS_OK && code != CSS_NEEDDATA) + IMVUE_EXCEPTION(StyleError, "css_stylesheet_append_data failed: %s", css_error_to_string(code)); + code = css_stylesheet_data_done(sheet); + if (code != CSS_OK) + IMVUE_EXCEPTION(StyleError, "css_stylesheet_data_done failed: %s", css_error_to_string(code)); + + *dest = sheet; + } +} diff --git a/src/imvue_style.h b/src/imvue_style.h index e69de29..c72d712 100644 --- a/src/imvue_style.h +++ b/src/imvue_style.h @@ -0,0 +1,254 @@ +/* +MIT License + +Copyright (c) 2019 Artem Chernyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef __IMVUE_STYLE_H__ +#define __IMVUE_STYLE_H__ + +#if __APPLE__ +#define DEFAULT_DPI 72.0f +#else +#define DEFAULT_DPI 96.0f +#endif + +extern "C" { +#ifndef restrict +#define restrict +#endif +#include +} + +#include + +#include "imgui.h" +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui_internal.h" + +struct css_computed_style; +struct css_select_ctx; + +namespace ImVue { + + ImGuiWindow* GetCurrentWindowNoDefault(); // like GetCurrentWindowRead, but returns NULL if current window is fallback window + + class Element; + class ComputedStyle; + class Context; + + typedef bool (*styleCallback)(ComputedStyle& style); + + inline ImU32 parseColor(css_color color) + { + return (((color >> 16) & 0xFF) << IM_COL32_R_SHIFT) | + (((color >> 8) & 0xFF) << IM_COL32_G_SHIFT) | + (((color >> 0) & 0xFF) << IM_COL32_B_SHIFT) | + (((color >> 24) & 0xFF) << IM_COL32_A_SHIFT); + } + + enum ParseUnitsAxis { X, Y }; + + float parseUnits(css_fixed value, css_unit unit, ComputedStyle& style, const ImVec2& parentSize, ParseUnitsAxis axis); + + bool setDimensions(ComputedStyle& style); + + bool setPosition(ComputedStyle& style); + + bool setColor(ComputedStyle& style); + + bool setBackgroundColor(ComputedStyle& style); + + bool setPadding(ComputedStyle& style); + + bool setMargins(ComputedStyle& style); + + bool setFont(ComputedStyle& style); + + bool setBorderRadius(ComputedStyle& style); + + bool setBorder(ComputedStyle& style); + + class Style; + /** + * Style state + */ + class ComputedStyle + { + public: + + ComputedStyle(Element* target); + ~ComputedStyle(); + /** + * Compute style for an element + * + * @param element Target element + */ + bool compute(Element* element); + + /** + * Apply computed style + */ + void begin(Element* element); + + /** + * Roll back any changes + */ + void end(); + + void destroy(); + + void updateClassCache(); + + void setInlineStyle(const char* data); + + ImVector& getClasses(); + + // override copy constructor + ComputedStyle(ComputedStyle& other) + : libcssData(0) + , fontName(0) + , fontSize(0) + , element(other.element) + , style(0) + , context(0) + , nCol(0) + , nStyle(0) + , nFonts(0) + , fontScale(0) + , mSelectCtx(0) + , mInlineStyle(0) + , mAutoSize(false) + { + compute(element); + } + + void* libcssData; + char* fontName; + float fontSize; + uint16_t position; + + Element* element; + + css_select_results* style; + + ImVec2 elementScreenPosition; + ImVec2 contentRegion; + ImVec2 parentSize; + ImVec4 borderRadius; + + Context* context; + + struct Decoration { + void renderBackground(ImRect rect); + void render(ImRect rect); + + void drawJoint( + const ImVec2& p1, + const ImVec2& p2, + const ImVec2& p3, + const ImVec2& p1i, + const ImVec2& p2i, + const ImVec2& p3i, + ImU32 startColor, + ImU32 endColor, + int numSegments + ); + + void drawJoint( + const ImVec2& p1, + const ImVec2& p2, + const ImVec2& p3, + const ImVec2& center, + ImU32 startColor, + ImU32 endColor, + const ImVec2& startPoint, + const ImVec2& endPoint, + int numSegments + ); + + float thickness[4]; + float rounding[4]; + ImU32 col[4]; + ImU32 bgCol; + }; + + Decoration decoration; + // style stack + int nCol; + int nStyle; + int nFonts; + float fontScale; + + private: + + void initFonts(css_media* media); + + void cleanupClassCache(); + + friend class Style; + ImVector mStyleCallbacks; + css_select_ctx* mSelectCtx; + ImVector mClasses; + uint16_t mWidthMode; + uint16_t mHeightMode; + css_stylesheet* mInlineStyle; + bool mAutoSize; + }; + + /** + * Wrapper for libcss + */ + class Style { + public: + struct Sheet { + css_stylesheet* sheet; + bool scoped; + }; + + Style(Style* parent = 0); + ~Style(); + + css_select_ctx* select(); + + /** + * Parse sheet and append it to sheet list + * + * @param data raw sheet data + * @param scoped adds list to local + */ + void load(const char* data, bool scoped = false); + + void parse(const char* data, css_stylesheet** dest, bool isInline = false); + + private: + + css_stylesheet* mBase; + ImVector mSheets; + + Style* mParent; + + void appendSheets(css_select_ctx* ctx, bool scoped = false); + + }; +} + +#endif diff --git a/src/lua/script.cpp b/src/lua/script.cpp index f875ba5..5bb65fc 100644 --- a/src/lua/script.cpp +++ b/src/lua/script.cpp @@ -144,6 +144,8 @@ extern "C" { Object createObject(lua_State* L); Object createObject(lua_State* L, int ref); + Object createObject(lua_State* L, int ref, const char* key); + Object createObject(lua_State* L, int ref, int index); /** * Abstract lua object @@ -152,6 +154,7 @@ extern "C" { public: LuaObject(lua_State* L) : mLuaState(L) + , mType(ObjectType::NIL) { } @@ -186,9 +189,42 @@ extern "C" { } StackGuard g(mLuaState); unwrap(); - lua_pushstring(mLuaState, key); - lua_gettable(mLuaState, -2); - return createObject(mLuaState); + return createObject(mLuaState, luaL_ref(mLuaState, LUA_REGISTRYINDEX), key); + } + + virtual Object get(int index) { + if(type() != ObjectType::OBJECT && type() != ObjectType::USERDATA && type() != ObjectType::ARRAY) { + IMVUE_EXCEPTION(ScriptError, "failed to get key: not an object"); + return Object(); + } + StackGuard g(mLuaState); + unwrap(); + return createObject(mLuaState, luaL_ref(mLuaState, LUA_REGISTRYINDEX), index); + } + + virtual void erase(const Object& key) + { + StackGuard g(mLuaState); + const ObjectImpl* impl = key.getImpl(); + if(!impl) { + IMVUE_EXCEPTION(ScriptError, "null pointer access"); + return; + } + const LuaObject* obj = static_cast(impl); + + if(type() == ObjectType::USERDATA) { + unwrap(); + lua_getfield(mLuaState, -1, "remove"); + unwrap(mLuaState); + obj->unwrap(mLuaState); + lua_call(mLuaState, 2, 1); + } else { + unwrap(); + int table = lua_gettop(mLuaState); + obj->unwrap(mLuaState); + lua_pushnil(mLuaState); + lua_settable(mLuaState, table); + } } virtual bool call(Object* args, Object* rets, int nargs, int nrets) { @@ -288,6 +324,17 @@ extern "C" { assign(); } + void initObject() + { + StackGuard g(mLuaState); + unwrap(mLuaState); + if(lua_type(mLuaState, -1) == LUA_TTABLE) + return; + + lua_createtable(mLuaState, 0, 0); + assign(); + } + virtual void unwrap(lua_State* L = 0) const = 0; virtual void assign(lua_State* L = 0) = 0; protected: @@ -407,6 +454,16 @@ extern "C" { { lua_pushstring(L, key); mKeyReference = luaL_ref(L, LUA_REGISTRYINDEX); + initType(); + } + + LuaFieldReference(lua_State* L, int ref, int index) + : LuaObject(L) + , mRef(ref) + { + lua_pushinteger(L, index); + mKeyReference = luaL_ref(L, LUA_REGISTRYINDEX); + initType(); } LuaFieldReference(lua_State* L, int index) @@ -428,6 +485,7 @@ extern "C" { lua_rawgeti(L, LUA_REGISTRYINDEX, mRef); lua_rawgeti(L, LUA_REGISTRYINDEX, mKeyReference); lua_gettable(L, -2); + lua_remove(L, -2); } void assign(lua_State* L = 0) @@ -455,6 +513,15 @@ extern "C" { return Object(new LuaReference(L, ref)); } + Object createObject(lua_State* L, int ref, const char* key) { + return Object(new LuaFieldReference(L, ref, key)); + } + + Object createObject(lua_State* L, int ref, int index) + { + return Object(new LuaFieldReference(L, ref, index)); + } + #define IMVUE "ImVue" #define IMVUE_REFMAPPER "ImVueRefMapper" @@ -619,6 +686,7 @@ extern "C" { // TODO: make all script tag parsers use the same code path rapidxml::xml_attribute<>* src = script->first_attribute("src"); + const char* scriptData = NULL; if(src) { lua_pushstring(L, src->value()); @@ -643,6 +711,9 @@ extern "C" { std::string str; rapidxml::print(std::back_inserter(str), *tmpl, rapidxml::print_no_indenting); + for (rapidxml::xml_node<>* node = doc.first_node("style"); node; node = node->next_sibling("style")) { + rapidxml::print(std::back_inserter(str), *node, rapidxml::print_no_indenting); + } lua_pushstring(L, "template"); lua_pushstring(L, str.c_str()); @@ -1351,6 +1422,10 @@ extern "C" { void registerBindings(lua_State* L) { + lua_pushnumber(L, ImGuiWindowFlags_NoBackground); + lua_setglobal(L, "ImGuiWindowFlags_NoBackground"); + lua_pushnumber(L, ImGuiWindowFlags_NoDecoration); + lua_setglobal(L, "ImGuiWindowFlags_NoDecoration"); lua_pushnumber(L, ImGuiWindowFlags_NoTitleBar); lua_setglobal(L, "ImGuiWindowFlags_NoTitleBar"); lua_pushnumber(L, ImGuiWindowFlags_NoResize); @@ -1427,6 +1502,14 @@ extern "C" { lua_setglobal(L, "ImGuiSelectableFlags_DontClosePopups"); lua_pushnumber(L, ImGuiSelectableFlags_SpanAllColumns); lua_setglobal(L, "ImGuiSelectableFlags_SpanAllColumns"); + lua_pushnumber(L, ImGuiTabBarFlags_Reorderable); + lua_setglobal(L, "ImGuiTabBarFlags_Reorderable"); + lua_pushnumber(L, ImGuiTabBarFlags_DockNode); + lua_setglobal(L, "ImGuiTabBarFlags_DockNode"); + lua_pushnumber(L, ImGuiTabBarFlags_SaveSettings); + lua_setglobal(L, "ImGuiTabBarFlags_SaveSettings"); + lua_pushnumber(L, ImGuiTabBarFlags_AutoSelectNewTabs); + lua_setglobal(L, "ImGuiTabBarFlags_AutoSelectNewTabs"); lua_pushnumber(L, ImGuiStyleVar_FramePadding); lua_setglobal(L, "ImGuiStyleVar_FramePadding"); lua_pushnumber(L, ImGuiStyleVar_FrameRounding); @@ -1459,8 +1542,6 @@ extern "C" { lua_setglobal(L, "ImGuiCol_FrameBg"); lua_pushnumber(L, ImGuiCol_WindowBg); lua_setglobal(L, "ImGuiCol_WindowBg"); - lua_pushnumber(L, ImGuiCol_ChildWindowBg); - lua_setglobal(L, "ImGuiCol_ChildWindowBg"); lua_pushnumber(L, ImGuiCol_Border); lua_setglobal(L, "ImGuiCol_Border"); lua_pushnumber(L, ImGuiCol_BorderShadow); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 86604b3..6af9b68 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ set(SOURCES unit/parser.cpp unit/imvue_tests.cpp unit/state.cpp + unit/style.cpp ) include_directories(${GTEST_INCLUDE_DIRS}) diff --git a/tests/benchmark/main.cpp b/tests/benchmark/main.cpp index 682691b..cae92f1 100644 --- a/tests/benchmark/main.cpp +++ b/tests/benchmark/main.cpp @@ -6,7 +6,7 @@ #include #include "imvue_generated.h" -#define WINDOW_COUNT 1000 +#define WINDOW_COUNT 100 class ImVueBenchmark : public benchmark::Fixture { public: @@ -158,10 +158,46 @@ BENCHMARK_DEFINE_F(ImVueBenchmark, RenderImVueScripted)(benchmark::State& state) lua_close(L); } + +BENCHMARK_DEFINE_F(ImVueBenchmark, RenderImVueStyled)(benchmark::State& state) { + lua_State * L = luaL_newstate(); + luaL_openlibs(L); + ImVue::registerBindings(L); + { + ImVue::Document document(ImVue::createContext( + ImVue::createElementFactory(), + new ImVue::LuaScriptState(L) + )); + std::stringstream ss; + + ss << ""; + ss << ""; + for(size_t i = 0; i < WINDOW_COUNT; ++i) { + ss << "test"; + } + ss << ""; + + ss << ""; + + document.parse(&ss.str()[0]); + for (auto _ : state) { + beforeRender(); + document.render(); + afterRender(); + } + } + lua_close(L); +} + BENCHMARK_REGISTER_F(ImVueBenchmark, RenderImVueScripted); +BENCHMARK_REGISTER_F(ImVueBenchmark, RenderImVueStyled); #endif -BENCHMARK_REGISTER_F(ImVueBenchmark, RenderImGuiStatic); BENCHMARK_REGISTER_F(ImVueBenchmark, RenderImVueStatic); +BENCHMARK_REGISTER_F(ImVueBenchmark, RenderImGuiStatic); // Run the benchmark BENCHMARK_MAIN(); diff --git a/tests/resources/selection.xml b/tests/resources/selection.xml new file mode 100644 index 0000000..0e487c1 --- /dev/null +++ b/tests/resources/selection.xml @@ -0,0 +1,230 @@ + + + + + + the button + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + a + + + + a + + + + + a + + + + a + + + + + diff --git a/tests/resources/selection_scripted.xml b/tests/resources/selection_scripted.xml new file mode 100644 index 0000000..57f2cf4 --- /dev/null +++ b/tests/resources/selection_scripted.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/unit/imvue_tests.cpp b/tests/unit/imvue_tests.cpp index 713dfb0..e507f98 100644 --- a/tests/unit/imvue_tests.cpp +++ b/tests/unit/imvue_tests.cpp @@ -1068,4 +1068,4 @@ TEST(TestGeneratedElement, DragIntRange2) ImVue::Document document; document.parse(testDocumentDragIntRange2); renderDocument(document); -} +} \ No newline at end of file diff --git a/tests/unit/style.cpp b/tests/unit/style.cpp new file mode 100644 index 0000000..594569f --- /dev/null +++ b/tests/unit/style.cpp @@ -0,0 +1,762 @@ + +#include +#include "imvue.h" +#include "imvue_generated.h" +#include "imvue_errors.h" +#include "utils.h" + +#if defined(WITH_LUA) +#include "lua/script.h" +extern "C" { + #include + #include + #include +} +#endif + +class TestElement : public ImVue::ContainerElement { + + public: + TestElement() + : mForceState(0) + , color(0) + { + } + + ImVec2 screenPos; + ImU32 color; + + void lockState(char* id) + { + if(ImStricmp(id, "hover") == 0) { + mForceState = ImVue::Element::HOVERED; + } else if(ImStricmp(id, "active") == 0) { + mForceState = ImVue::Element::ACTIVE; + } else if(ImStricmp(id, "disabled") == 0) { + mForceState = ImVue::Element::DISABLED; + } else if(ImStricmp(id, "checked") == 0) { + mForceState = ImVue::Element::CHECKED; + } else if(ImStricmp(id, "link") == 0) { + mForceState = ImVue::Element::LINK; + } else if(ImStricmp(id, "focus") == 0) { + mForceState = ImVue::Element::FOCUSED; + } else if(ImStricmp(id, "visited") == 0) { + mForceState = ImVue::Element::VISITED; + } + mState = mForceState; + ImGui::MemFree(id); + } + + void renderBody() + { + screenPos = ImGui::GetCursorScreenPos(); + ImVec2 pos = ImGui::GetCursorPos(); + ImVec2 size(20.0f + ImMax(0.0f, padding[0] + padding[2]), 20.0f + ImMax(0.0f, padding[1] + padding[3])); + + ImRect bb(pos, pos + size); + ImGui::ItemSize(bb.GetSize(), 0); + ImGui::ItemAdd(bb, index); + ContainerElement::renderBody(); + + color = ImGui::GetColorU32(ImGuiCol_Text); + } + + private: + + unsigned int mForceState; + +}; + +class TestStyles : public ::testing::Test { + + public: + + TestStyles() + : mDoc(0) + { + } + + ~TestStyles() + { + if(mDoc) { + delete mDoc; + mDoc = 0; + } + } + + void TearDown() override + { + if(mDoc) { + delete mDoc; + mDoc = 0; + } + } + + ImVue::Document& createDoc(const char* data) + { + if(mDoc) { + delete mDoc; + } + ImVue::ElementFactory* factory = ImVue::createElementFactory(); + factory->element("test"); + ImVue::Context* ctx = ImVue::createContext(factory); + mDoc = new ImVue::Document(ctx); + mDoc->parse(data); + return *mDoc; + } + + private: + ImVue::Document* mDoc; +}; + +/** + * Test various styles settings + */ +TEST_F(TestStyles, Padding) +{ + { + ImVec2 pbefore = ImGui::GetStyle().WindowPadding; + + const char* doc = ""; + + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + ImVec2 pafter = ImGui::GetStyle().WindowPadding; + EXPECT_EQ(pbefore.x, pafter.x); + EXPECT_EQ(pbefore.y, pafter.y); + + ImVector els = d.getChildren(".padded"); + ASSERT_EQ(els.size(), 1); + + TestElement* element = els[0]; + EXPECT_FLOAT_EQ(element->padding[0], 5.0f); + EXPECT_FLOAT_EQ(element->padding[1], 5.0f); + EXPECT_FLOAT_EQ(element->padding[2], 5.0f); + EXPECT_FLOAT_EQ(element->padding[3], 5.0f); + } + + { + const char* doc = ""; + + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + ImVector els = d.getChildren(".padded"); + ASSERT_EQ(els.size(), 1); + + TestElement* element = els[0]; + EXPECT_FLOAT_EQ(element->padding[1], 5.0f); + EXPECT_FLOAT_EQ(element->padding[2], 10.0f); + EXPECT_FLOAT_EQ(element->padding[3], 20.0f); + EXPECT_FLOAT_EQ(element->padding[0], 30.0f); + } + + { + const char* doc = ""; + + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + ImVector els = d.getChildren(".padded"); + ASSERT_EQ(els.size(), 1); + + TestElement* element = els[0]; + EXPECT_FLOAT_EQ(element->padding[1], 5.0f); + EXPECT_FLOAT_EQ(element->padding[2], 10.0f); + EXPECT_FLOAT_EQ(element->padding[3], 20.0f); + EXPECT_FLOAT_EQ(element->padding[0], ImGui::GetIO().DisplaySize.x / 100 * 30); + } +} + +TEST_F(TestStyles, Display) +{ + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + + // display block + { + const char* doc = "" + "" + "" + "" + ""; + + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + ImVector els = d.getChildren(".display-test", true); + ASSERT_EQ(els.size(), 2); + + EXPECT_FLOAT_EQ(els[0]->pos.x, 0.0f); + EXPECT_FLOAT_EQ(els[1]->pos.x, 0.0f); + EXPECT_FLOAT_EQ(els[0]->pos.x, 0.0f); + EXPECT_FLOAT_EQ(els[1]->pos.y, els[0]->computedSize.y); + } + + // display inline + { + const char* doc = "" + "" + "" + "" + ""; + + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + ImVector els = d.getChildren(".display-test", true); + ASSERT_EQ(els.size(), 2); + + EXPECT_FLOAT_EQ(els[0]->pos.x, 0.0f); + EXPECT_FLOAT_EQ(els[0]->pos.x, 0.0f); + EXPECT_FLOAT_EQ(els[1]->pos.x, els[0]->computedSize.x); + EXPECT_FLOAT_EQ(els[1]->pos.y, 0.0f); + } + + // display inline-block + { + const char* doc = "" + "" + "" + "" + ""; + + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + ImVector els = d.getChildren(".display-test", true); + ASSERT_EQ(els.size(), 2); + + EXPECT_FLOAT_EQ(els[0]->pos.x, 0.0f); + EXPECT_FLOAT_EQ(els[0]->pos.x, 0.0f); + EXPECT_FLOAT_EQ(els[1]->pos.x, els[0]->computedSize.x); + EXPECT_FLOAT_EQ(els[1]->pos.y, 0.0f); + + EXPECT_FLOAT_EQ(els[0]->computedSize.x, 24.0f); + } + + ImGui::PopStyleVar(3); +} + +TEST_F(TestStyles, Dimensions) +{ + const char* doc = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + ImVector windows = d.getChildren("window"); + ASSERT_EQ(windows.size(), 3); + + { + ImVue::Window* window = windows[0]; + ImVector els = window->getChildren(".full-size"); + ASSERT_EQ(els.size(), 1); + + EXPECT_EQ(window->size.x, 320.0f); + EXPECT_EQ(window->size.y, 240.0f); + + EXPECT_EQ(els[0]->size.x, window->size.x); + EXPECT_EQ(els[0]->size.y, window->size.y); + } + + { + ImVue::Window* window = windows[1]; + ImVector els = window->getChildren(".fixed-size"); + ASSERT_EQ(els.size(), 1); + + EXPECT_EQ(window->size.x, 320.0f); + EXPECT_EQ(window->size.y, 240.0f); + + EXPECT_EQ(els[0]->size.x, 40.0f); + EXPECT_EQ(els[0]->size.y, 45.0f); + } + + { + ImVue::Window* window = windows[2]; + ImVector els = window->getChildren(".full-size"); + ASSERT_EQ(els.size(), 1); + + EXPECT_EQ(window->size.x, 320.0f); + EXPECT_EQ(window->size.y, 240.0f); + + EXPECT_EQ(els[0]->screenPos.x, 5.0f); + EXPECT_EQ(els[0]->screenPos.y, 5.0f); + + EXPECT_EQ(els[0]->size.x, window->size.x - 10.0f); + EXPECT_EQ(els[0]->size.y, window->size.y - 10.0f); + } +} + +TEST_F(TestStyles, Position) +{ + const char* doc = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + ImVector windows = d.getChildren("window"); + ASSERT_GT(windows.size(), 0); + { + ImVue::Window* window = windows[0]; + ImVector els = window->getChildren(".absolute", true); + ASSERT_EQ(els.size(), 10); + + ImVec2 wPos(30.0f, 20.0f); + float margin = 5.0f; + float left = wPos.x + margin; + float right = wPos.x + window->size.x - margin; + float top = wPos.y + margin; + float bottom = wPos.y + window->size.y - margin; + + ImVec2 defaultSize(30.0f, 20.0f); + + EXPECT_EQ(window->size.x, 320.0f); + EXPECT_EQ(window->size.y, 240.0f); + + // top left + EXPECT_EQ(els[0]->screenPos.x, left); + EXPECT_EQ(els[0]->screenPos.y, top); + + EXPECT_EQ(els[0]->size.x, defaultSize.x); + EXPECT_EQ(els[0]->size.y, defaultSize.y); + + // top right + EXPECT_EQ(els[1]->screenPos.x, right - els[1]->size.x); + EXPECT_EQ(els[1]->screenPos.y, top); + + EXPECT_EQ(els[1]->size.x, defaultSize.x); + EXPECT_EQ(els[1]->size.y, defaultSize.y); + + // bottom left + EXPECT_EQ(els[2]->screenPos.x, left); + EXPECT_EQ(els[2]->screenPos.y, bottom - els[2]->size.y); + + EXPECT_EQ(els[2]->size.x, defaultSize.x); + EXPECT_EQ(els[2]->size.y, defaultSize.y); + + // bottom right + EXPECT_EQ(els[3]->screenPos.x, right - els[3]->size.x); + EXPECT_EQ(els[3]->screenPos.y, bottom - els[3]->size.y); + + EXPECT_EQ(els[3]->size.x, defaultSize.x); + EXPECT_EQ(els[3]->size.y, defaultSize.y); + + // full-width-top + EXPECT_EQ(els[4]->screenPos.x, left); + EXPECT_EQ(els[4]->screenPos.y, top); + + float itemWidth = window->size.x - margin * 2; + float itemHeight = window->size.y - margin * 2; + + EXPECT_EQ(els[4]->size.x, itemWidth); + EXPECT_EQ(els[4]->size.y, defaultSize.y); + + // full-width-bottom + EXPECT_EQ(els[5]->screenPos.x, left); + EXPECT_EQ(els[5]->screenPos.y, bottom - els[3]->size.y); + + EXPECT_EQ(els[5]->size.x, itemWidth); + EXPECT_EQ(els[5]->size.y, defaultSize.y); + + // full-height-left + EXPECT_EQ(els[6]->screenPos.x, left); + EXPECT_EQ(els[6]->screenPos.y, top); + + EXPECT_EQ(els[6]->size.x, defaultSize.x); + EXPECT_EQ(els[6]->size.y, itemHeight); + + // full-height-right + EXPECT_EQ(els[7]->screenPos.x, right - els[7]->size.x); + EXPECT_EQ(els[7]->screenPos.y, top); + + EXPECT_EQ(els[7]->size.x, defaultSize.x); + EXPECT_EQ(els[7]->size.y, itemHeight); + + // full-size + EXPECT_EQ(els[8]->screenPos.x, left); + EXPECT_EQ(els[8]->screenPos.y, top); + + EXPECT_EQ(els[8]->size.x, itemWidth); + EXPECT_EQ(els[8]->size.y, itemHeight); + + ImVector inl = window->getChildren(".inline"); + ASSERT_EQ(inl.size(), 2); + // finally verify wrap mode + EXPECT_EQ(inl[0]->pos.x, 0); + EXPECT_EQ(inl[0]->pos.y, 0); + + // test same-line behaviour + EXPECT_EQ(inl[1]->pos.x, inl[0]->getSize().x); + EXPECT_EQ(inl[1]->pos.y, 0); + + ImVector rel = window->getChildren(".relative-parent"); + ASSERT_GT(rel.size(), 0); + + EXPECT_EQ(els[9]->screenPos.x, rel[0]->screenPos.x + 5); + EXPECT_EQ(els[9]->screenPos.y, rel[0]->screenPos.y + 5); + } +} + +TEST_F(TestStyles, Color) +{ + const char* doc = "" + "" + "" + "" + "" + "" + "" + "" + ; + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + { + ImVector els = d.getChildren("#col-direct", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_EQ(els[0]->color, 0xFF00CCFF); + } + + { + ImVector els = d.getChildren("#col-inherit", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_EQ(els[0]->color, 0xFF00CCFF); + } + + { + ImVector els = d.getChildren("#default-col", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_EQ(els[0]->color, 0xFFFFFFFF); + } +} + +TEST_F(TestStyles, BgColor) +{ + const char* doc = "" + "" + "" + "" + "" + "" + "" + "" + ; + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + { + ImVector els = d.getChildren("#col-direct", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_EQ(els[0]->bgColor, 0xFF00CCFF); + } + + { + ImVector els = d.getChildren("#default-col", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_EQ(els[0]->bgColor, 0); + } +} + +TEST_F(TestStyles, FontSize) +{ + const char* doc = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ; + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + { + ImVector els = d.getChildren("#font-size-absolute", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_FLOAT_EQ(els[0]->style()->fontSize, 20.0f); + } + + { + ImVector els = d.getChildren("#font-size-relative", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_TRUE(els[0]->style()->fontSize - 20.0f * 1.2f < 0.005); + } + + { + ImVector els = d.getChildren("#font-size-inherited", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_FLOAT_EQ(els[0]->style()->fontSize, 20.0f); + } +} + +TEST_F(TestStyles, ScaleRelativeToFontSize) { + const char* doc = "" + "" + "" + "" + "" + "" + "" + ; + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + { + ImVector els = d.getChildren("#relative-to-parent", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_FLOAT_EQ(els[0]->size.y, 40.0f); + } + + { + ImVector els = d.getChildren("#relative-to-root", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_FLOAT_EQ(els[0]->size.y, 60.0f); + } +} + + +TEST_F(TestStyles, FontFamily) { + const char* doc = "" + "" + "" + "" + "" + "" + ; + + ImVue::Document& d = createDoc(doc); + renderDocument(d); + + { + ImVector els = d.getChildren("#font-family-set-directly", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_STREQ(els[0]->style()->fontName, "DroidSans"); + } + + { + ImVector els = d.getChildren("#font-family-fallback", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_STREQ(els[0]->style()->fontName, "DroidSans"); + } + + { + ImVector els = d.getChildren("#fail-to-set", true); + ASSERT_EQ(els.size(), 1); + + EXPECT_STREQ(els[0]->style()->fontName, NULL); + } +} + +typedef std::tuple SelectionTestParam; + +class SelectionTest : public ::testing::Test, public testing::WithParamInterface { +#if defined(WITH_LUA) + protected: + void SetUp() override { + L = luaL_newstate(); + luaL_openlibs(L); + ImVue::registerBindings(L); + } + + void TearDown() override { + lua_close(L); + } + + lua_State* L; +#endif +}; + +bool shouldApplyStyle(int index, int* indices) { + for(int i = 0; i < 1024; ++i) { + if(indices[i] == -1) { + break; + } + + if(indices[i] == index) { + return true; + } + } + + return false; +} + +TEST_P(SelectionTest, TestStylesApplied) { + ImVue::ElementFactory* factory = ImVue::createElementFactory(); + factory->element("test") + .setter("state", &TestElement::lockState); + + const char* testcase; + const char* file; + const char* query; + int* expectedIndices; + std::tie(testcase, expectedIndices, query, file) = GetParam(); + + ImVue::Context* ctx = ImVue::createContext(factory +#if defined(WITH_LUA) + , + new ImVue::LuaScriptState(L) +#endif + ); + ImVue::Document doc(ctx); + char* data = ctx->fs->load(file); + try { + doc.parse(data); + } catch(...) { + ImGui::MemFree(data); + throw; + } + ImGui::MemFree(data); + + renderDocument(doc); + + ImVector containers = doc.getChildren(testcase, true); + ASSERT_EQ(containers.size(), 1); + + ImVector elements = containers[0]->getChildren(query, true); + ASSERT_GT(elements.size(), 0); + for(int i = 0; i < (int)elements.size(); i++) { + if(shouldApplyStyle(i, expectedIndices)) { + EXPECT_EQ(elements[i]->bgColor, 0xFF00FF00); + } else { + EXPECT_EQ(elements[i]->bgColor, 0xFF0000FF); + } + } +} + +const char* staticTest = "selection.xml"; +const char* scriptedTest = "selection_scripted.xml"; + +static int nameSelect[] = {0, -1}; +static int classSelect[] = {1, 2, -1}; +static int idSelect[] = {2, -1}; +static int matchingAllClasses[] = {1, 2, -1}; +static int matchingClassAndID[] = {1, -1}; +static int nested[] = {0, 1, -1}; +static int directParent[] = {0, 2, -1}; +static int nextElement[] = {3, 5, -1}; +static int previousElement[] = {2, 4, -1}; +static int attrDefined[] = {1, -1}; +static int attrEqual[] = {0, -1}; +static int stateHover[] = {0, -1}; +static int stateActive[] = {1, -1}; +static int stateDisabled[] = {2, -1}; +static int stateChecked[] = {3, -1}; +static int stateOther[] = {4, -1}; +static int array[] = {1, 4, -1}; +static int arrayOfType[] = {0, 4, -1}; +#if defined(WITH_LUA) +static int pseudoElement[] = {0, 4, -1}; +static int pseudoElementFlat[] = {0, 6, 7, -1}; +#endif + +INSTANTIATE_TEST_CASE_P( + TestStylesApplied, + SelectionTest, + ::testing::Values( + std::make_tuple(".name-select", nameSelect, "*", staticTest), + std::make_tuple(".class-select", classSelect, "test", staticTest), + std::make_tuple(".id-select", idSelect, "test", staticTest), + std::make_tuple(".matching-all-classes", matchingAllClasses, "test", staticTest), + std::make_tuple(".class-plus-id", matchingClassAndID, "test", staticTest), + std::make_tuple(".nested", nested, "test", staticTest), + std::make_tuple(".direct-parent", directParent, "test", staticTest), + std::make_tuple(".next-element", nextElement, "test", staticTest), + std::make_tuple(".previous-element", previousElement, "test", staticTest), + std::make_tuple(".attr-defined", attrDefined, "test", staticTest), + std::make_tuple(".attr-equal", attrEqual, "test", staticTest), + std::make_tuple(".states-hover", stateHover, "test", staticTest), + std::make_tuple(".states-active", stateActive, "test", staticTest), + std::make_tuple(".states-disabled", stateDisabled, "test", staticTest), + std::make_tuple(".states-checked", stateChecked, "test", staticTest), + std::make_tuple(".states-link", stateOther, "test", staticTest), + std::make_tuple(".states-focus", stateOther, "test", staticTest), + std::make_tuple(".states-visited", stateOther, "test", staticTest), + std::make_tuple(".array", array, "test", staticTest), + std::make_tuple(".array-of-type", arrayOfType, "test", staticTest) +#if defined(WITH_LUA) + , + std::make_tuple(".pseudo-element", pseudoElement, "test", scriptedTest), + std::make_tuple(".pseudo-element-flat", pseudoElementFlat, "test", scriptedTest) +#endif + )); + diff --git a/tests/unit/utils.h b/tests/unit/utils.h index 02ff6ff..8d04889 100644 --- a/tests/unit/utils.h +++ b/tests/unit/utils.h @@ -6,6 +6,10 @@ static void renderDocument(ImVue::Document& document, int times = 1) { ImGuiIO& io = ImGui::GetIO(); + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + for(int i = 0; i < times; ++i) { io.DisplaySize = ImVec2(1024, 768); io.DeltaTime = 1.0f / 60.0f; diff --git a/tools/config.yaml b/tools/config.yaml index 28ad03e..92504a4 100644 --- a/tools/config.yaml +++ b/tools/config.yaml @@ -10,6 +10,8 @@ blacklist: # rewrite classes names rewrites: Combo: ComboItem +rewrite_fields: + size_arg: size rules: label: from_text text: from_text diff --git a/tools/generate.py b/tools/generate.py index 54dc1f0..cc11ca7 100644 --- a/tools/generate.py +++ b/tools/generate.py @@ -84,6 +84,7 @@ def parse_elements(filename, config): blacklist = [re.compile(value) for value in config.get('blacklist', [])] rewrites = config.get('rewrites', {}) + rewrite_fields = config.get('rewrite_fields', {}) rules = config.get('rules', {}) elements_rules = config.get('elements_rules', {}) @@ -187,9 +188,14 @@ def parse_elements(filename, config): field_type.strip() param_name = re.sub(r'\[\d*\]', '', parts[-1]) + if param_name in rewrite_fields: + param_name = rewrite_fields[param_name] + attr_name = param_name.replace('_', '-') param_read = param_name field_name = parts[-1] + if field_name in rewrite_fields: + field_name = rewrite_fields[field_name] array_size = re.match(r'\w*\[(\d+)\]', parts[-1]) if array_size: (array_size,) = array_size.groups() @@ -204,6 +210,7 @@ def parse_elements(filename, config): param_name = '&' + param_name default = None + define = field_name != 'size' params.append({ 'default': default, 'attr_name': attr_name, @@ -214,10 +221,11 @@ def parse_elements(filename, config): 'param_name': param_name, 'param_read': param_read, 'required': param_read in required_fields, + 'define': define }) initializer, destructor = gen_init_destroy(field_type, field_name, param_read, default) - if initializer: + if initializer and define: initializers.append(initializer) if destructor: diff --git a/tools/templates/imvue_generated.h.j2 b/tools/templates/imvue_generated.h.j2 index 938f345..37ef37c 100644 --- a/tools/templates/imvue_generated.h.j2 +++ b/tools/templates/imvue_generated.h.j2 @@ -71,7 +71,9 @@ namespace ImVue { } {%- if element.fields %} {%- for field in element.fields %} +{%- if field.define %} {{ field.field_type | replace('const', '') | trim }} {{ field.field_name }}; +{%- endif %} {%- endfor %} {%- endif %} }; @@ -106,10 +108,15 @@ namespace ImVue { {%- for element in elements %} factory.element<{{element.cls}}>("{{ element.tag }}") {%- for field in element.fields %} +{%- if field.define %} .{{ 'attribute("' + field.attr_name + '", ' if field.param_read != element.from_text | d(None) else 'text(' }}&{{ element.cls }}::{{ field.param_read }}{{ ', true' if field.required else '' }}) +{%- endif %} {%- endfor %}; {% endfor %} + factory.element(TEXT_NODE) + .text(&Text::setText, true); + factory.element("collapsing-header") .attribute("label", &CollapsingHeader::label) .attribute("flags", &CollapsingHeader::flags); @@ -118,6 +125,7 @@ namespace ImVue { factory.element("template"); factory.element("__element__") .handler("click") + .handler("doubleclick") .handler("mousedown") .handler("mouseup") .handler("mouseover") @@ -126,9 +134,11 @@ namespace ImVue { .handler("keydown") .handler("keyup") .handler("keypress") + .attribute("size", &Element::size) .attribute("id", &Element::id) .attribute("key", &Element::key) - .attribute("ref", &Element::ref); + .attribute("ref", &Element::ref) + .setter("style", &Element::setInlineStyle); factory.element("slot"); return res;