diff --git a/CMakeLists.txt b/CMakeLists.txt index b2db2a5834..a08678130e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -381,6 +381,8 @@ if (HAVE_VULKAN) add_subdirectory( libraries/ZVulkan ) endif() +add_subdirectory( libraries/ZWidget ) + add_subdirectory( libraries/discordrpc EXCLUDE_FROM_ALL ) set( DRPC_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/discordrpc/include" ) set( DRPC_LIBRARIES discord-rpc ) diff --git a/libraries/ZWidget/CMakeLists.txt b/libraries/ZWidget/CMakeLists.txt new file mode 100644 index 0000000000..8c96e60586 --- /dev/null +++ b/libraries/ZWidget/CMakeLists.txt @@ -0,0 +1,144 @@ +cmake_minimum_required(VERSION 3.11) +project(zwidget) + +set(ZWIDGET_SOURCES + src/core/canvas.cpp + src/core/font.cpp + src/core/image.cpp + src/core/span_layout.cpp + src/core/timer.cpp + src/core/widget.cpp + src/core/utf8reader.cpp + src/core/schrift/schrift.cpp + src/core/schrift/schrift.h + src/core/picopng/picopng.cpp + src/core/picopng/picopng.h + src/core/nanosvg/nanosvg.cpp + src/core/nanosvg/nanosvg.h + src/core/nanosvg/nanosvgrast.h + src/widgets/lineedit/lineedit.cpp + src/widgets/mainwindow/mainwindow.cpp + src/widgets/menubar/menubar.cpp + src/widgets/scrollbar/scrollbar.cpp + src/widgets/statusbar/statusbar.cpp + src/widgets/textedit/textedit.cpp + src/widgets/toolbar/toolbar.cpp + src/widgets/toolbar/toolbarbutton.cpp + src/widgets/imagebox/imagebox.cpp + src/widgets/textlabel/textlabel.cpp + src/widgets/pushbutton/pushbutton.cpp + src/widgets/checkboxlabel/checkboxlabel.cpp + src/widgets/listview/listview.cpp + src/window/window.cpp +) + +set(ZWIDGET_INCLUDES + include/zwidget/core/canvas.h + include/zwidget/core/colorf.h + include/zwidget/core/font.h + include/zwidget/core/image.h + include/zwidget/core/rect.h + include/zwidget/core/span_layout.h + include/zwidget/core/timer.h + include/zwidget/core/widget.h + include/zwidget/core/utf8reader.h + include/zwidget/core/resourcedata.h + include/zwidget/widgets/lineedit/lineedit.h + include/zwidget/widgets/mainwindow/mainwindow.h + include/zwidget/widgets/menubar/menubar.h + include/zwidget/widgets/scrollbar/scrollbar.h + include/zwidget/widgets/statusbar/statusbar.h + include/zwidget/widgets/textedit/textedit.h + include/zwidget/widgets/toolbar/toolbar.h + include/zwidget/widgets/toolbar/toolbarbutton.h + include/zwidget/widgets/imagebox/imagebox.h + include/zwidget/widgets/textlabel/textlabel.h + include/zwidget/widgets/pushbutton/pushbutton.h + include/zwidget/widgets/checkboxlabel/checkboxlabel.h + include/zwidget/widgets/listview/listview.h + include/zwidget/window/window.h +) + +set(ZWIDGET_WIN32_SOURCES + src/window/win32/win32window.cpp + src/window/win32/win32window.h +) + +set(ZWIDGET_COCOA_SOURCES +) + +set(ZWIDGET_SDL2_SOURCES + src/window/sdl2/sdl2displaywindow.cpp + src/window/sdl2/sdl2displaywindow.h +) + +source_group("src" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/.+") +source_group("src\\core" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/core/.+") +source_group("src\\core\\schrift" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/core/schrift/.+") +source_group("src\\core\\picopng" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/core/picopng/.+") +source_group("src\\core\\nanosvg" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/core/nanosvg/.+") +source_group("src\\widgets" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/.+") +source_group("src\\widgets\\lineedit" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/lineedit/.+") +source_group("src\\widgets\\mainwindow" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/mainwindow/.+") +source_group("src\\widgets\\menubar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/menubar/.+") +source_group("src\\widgets\\scrollbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/scrollbar/.+") +source_group("src\\widgets\\statusbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/statusbar/.+") +source_group("src\\widgets\\textedit" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/textedit/.+") +source_group("src\\widgets\\toolbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/toolbar/.+") +source_group("src\\widgets\\imagebox" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/imagebox/.+") +source_group("src\\widgets\\textlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/textlabel/.+") +source_group("src\\widgets\\pushbutton" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/pushbutton/.+") +source_group("src\\widgets\\checkboxlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/checkboxlabel/.+") +source_group("src\\widgets\\listview" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/listview/.+") +source_group("src\\window" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/.+") +source_group("include" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/.+") +source_group("include\\core" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/core/.+") +source_group("include\\widgets" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/.+") +source_group("include\\widgets\\lineedit" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/lineedit/.+") +source_group("include\\widgets\\mainwindow" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/mainwindow/.+") +source_group("include\\widgets\\menubar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/menubar/.+") +source_group("include\\widgets\\scrollbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/scrollbar/.+") +source_group("include\\widgets\\statusbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/statusbar/.+") +source_group("include\\widgets\\textedit" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/textedit/.+") +source_group("include\\widgets\\toolbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/toolbar/.+") +source_group("include\\widgets\\imagebox" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/imagebox/.+") +source_group("include\\widgets\\textlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/textlabel/.+") +source_group("include\\widgets\\pushbutton" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/pushbutton/.+") +source_group("include\\widgets\\checkboxlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/checkboxlabel/.+") +source_group("include\\widgets\\listview" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/listview/.+") +source_group("include\\window" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/.+") +source_group("include\\window\\win32" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/win32/.+") +source_group("include\\window\\sdl2" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/sdl2/.+") + +include_directories(include include/zwidget src) + +if(WIN32) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_WIN32_SOURCES}) + add_definitions(-DUNICODE -D_UNICODE) +elseif(APPLE) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_COCOA_SOURCES}) + set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl) + add_definitions(-DUNIX -D_UNIX) + add_link_options(-pthread) +else() + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES}) + set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl) + add_definitions(-DUNIX -D_UNIX) + add_link_options(-pthread) +endif() + +if(MSVC) + # Use all cores for compilation + set(CMAKE_CXX_FLAGS "/MP ${CMAKE_CXX_FLAGS}") + + # Ignore warnings in third party code + #set_source_files_properties(${ZWIDGET_SOURCES} PROPERTIES COMPILE_FLAGS "/wd4244 /wd4267 /wd4005 /wd4018 -D_CRT_SECURE_NO_WARNINGS") +endif() + +add_library(zwidget STATIC ${ZWIDGET_SOURCES} ${ZWIDGET_INCLUDES}) +target_link_libraries(zwidget ${ZWIDGET_LIBS}) +set_target_properties(zwidget PROPERTIES CXX_STANDARD 17) + +if(MSVC) + set_property(TARGET zwidget PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() diff --git a/libraries/ZWidget/LICENSE.md b/libraries/ZWidget/LICENSE.md new file mode 100644 index 0000000000..fdc06221ca --- /dev/null +++ b/libraries/ZWidget/LICENSE.md @@ -0,0 +1,21 @@ +# License information + +## License for ZWidget itself + + // Copyright (c) 2023 Magnus Norddahl + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. diff --git a/libraries/ZWidget/README.md b/libraries/ZWidget/README.md new file mode 100644 index 0000000000..c3bb8e2194 --- /dev/null +++ b/libraries/ZWidget/README.md @@ -0,0 +1,2 @@ +# ZWidget +A framework for building user interface applications diff --git a/libraries/ZWidget/include/zwidget/core/canvas.h b/libraries/ZWidget/include/zwidget/core/canvas.h new file mode 100644 index 0000000000..a546e62cc8 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/canvas.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include + +class Font; +class Image; +class Point; +class Rect; +class Colorf; +class DisplayWindow; +struct VerticalTextPosition; + +class FontMetrics +{ +public: + double ascent = 0.0; + double descent = 0.0; + double external_leading = 0.0; + double height = 0.0; +}; + +class Canvas +{ +public: + static std::unique_ptr create(DisplayWindow* window); + + virtual ~Canvas() = default; + + virtual void begin(const Colorf& color) = 0; + virtual void end() = 0; + + virtual void begin3d() = 0; + virtual void end3d() = 0; + + virtual Point getOrigin() = 0; + virtual void setOrigin(const Point& origin) = 0; + + virtual void pushClip(const Rect& box) = 0; + virtual void popClip() = 0; + + virtual void fillRect(const Rect& box, const Colorf& color) = 0; + virtual void line(const Point& p0, const Point& p1, const Colorf& color) = 0; + + virtual void drawText(const Point& pos, const Colorf& color, const std::string& text) = 0; + virtual Rect measureText(const std::string& text) = 0; + virtual VerticalTextPosition verticalTextAlign() = 0; + + virtual void drawText(const std::shared_ptr& font, const Point& pos, const std::string& text, const Colorf& color) = 0; + virtual void drawTextEllipsis(const std::shared_ptr& font, const Point& pos, const Rect& clipBox, const std::string& text, const Colorf& color) = 0; + virtual Rect measureText(const std::shared_ptr& font, const std::string& text) = 0; + virtual FontMetrics getFontMetrics(const std::shared_ptr& font) = 0; + virtual int getCharacterIndex(const std::shared_ptr& font, const std::string& text, const Point& hitPoint) = 0; + + virtual void drawImage(const std::shared_ptr& image, const Point& pos) = 0; +}; + +struct VerticalTextPosition +{ + double top = 0.0; + double baseline = 0.0; + double bottom = 0.0; +}; diff --git a/libraries/ZWidget/include/zwidget/core/colorf.h b/libraries/ZWidget/include/zwidget/core/colorf.h new file mode 100644 index 0000000000..9352147990 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/colorf.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +class Colorf +{ +public: + Colorf() = default; + Colorf(float r, float g, float b, float a = 1.0f) : r(r), g(g), b(b), a(a) { } + + static Colorf transparent() { return { 0.0f, 0.0f, 0.0f, 0.0f }; } + + static Colorf fromRgba8(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) + { + float s = 1.0f / 255.0f; + return { r * s, g * s, b * s, a * s }; + } + + uint32_t toBgra8() const + { + uint32_t cr = (int)(std::max(std::min(r * 255.0f, 255.0f), 0.0f)); + uint32_t cg = (int)(std::max(std::min(g * 255.0f, 255.0f), 0.0f)); + uint32_t cb = (int)(std::max(std::min(b * 255.0f, 255.0f), 0.0f)); + uint32_t ca = (int)(std::max(std::min(a * 255.0f, 255.0f), 0.0f)); + return (ca << 24) | (cr << 16) | (cg << 8) | cb; + } + + bool operator==(const Colorf& v) const { return r == v.r && g == v.g && b == v.b && a == v.a; } + bool operator!=(const Colorf& v) const { return r != v.r || g != v.g || b != v.b || a != v.a; } + + float r = 0.0f; + float g = 0.0f; + float b = 0.0f; + float a = 1.0f; +}; diff --git a/libraries/ZWidget/include/zwidget/core/font.h b/libraries/ZWidget/include/zwidget/core/font.h new file mode 100644 index 0000000000..d4af360616 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/font.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class Font +{ +public: + virtual ~Font() = default; + + virtual const std::string& GetName() const = 0; + virtual double GetHeight() const = 0; + + static std::shared_ptr Create(const std::string& name, double height); +}; diff --git a/libraries/ZWidget/include/zwidget/core/image.h b/libraries/ZWidget/include/zwidget/core/image.h new file mode 100644 index 0000000000..4cffc195ad --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/image.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +enum class ImageFormat +{ + R8G8B8A8, + B8G8R8A8 +}; + +class Image +{ +public: + virtual ~Image() = default; + + virtual int GetWidth() const = 0; + virtual int GetHeight() const = 0; + virtual ImageFormat GetFormat() const = 0; + virtual void* GetData() const = 0; + + static std::shared_ptr Create(int width, int height, ImageFormat format, const void* data); + static std::shared_ptr LoadResource(const std::string& resourcename, double dpiscale = 1.0); +}; diff --git a/libraries/ZWidget/include/zwidget/core/rect.h b/libraries/ZWidget/include/zwidget/core/rect.h new file mode 100644 index 0000000000..ad2a4e09ca --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/rect.h @@ -0,0 +1,83 @@ +#pragma once + +class Point +{ +public: + Point() = default; + Point(double x, double y) : x(x), y(y) { } + + double x = 0; + double y = 0; + + Point& operator+=(const Point& p) { x += p.x; y += p.y; return *this; } + Point& operator-=(const Point& p) { x -= p.x; y -= p.y; return *this; } +}; + +class Size +{ +public: + Size() = default; + Size(double width, double height) : width(width), height(height) { } + + double width = 0; + double height = 0; +}; + +class Rect +{ +public: + Rect() = default; + Rect(const Point& p, const Size& s) : x(p.x), y(p.y), width(s.width), height(s.height) { } + Rect(double x, double y, double width, double height) : x(x), y(y), width(width), height(height) { } + + Point pos() const { return { x, y }; } + Size size() const { return { width, height }; } + + Point topLeft() const { return { x, y }; } + Point topRight() const { return { x + width, y }; } + Point bottomLeft() const { return { x, y + height }; } + Point bottomRight() const { return { x + width, y + height }; } + + double left() const { return x; } + double top() const { return y; } + double right() const { return x + width; } + double bottom() const { return y + height; } + + static Rect xywh(double x, double y, double width, double height) { return Rect(x, y, width, height); } + static Rect ltrb(double left, double top, double right, double bottom) { return Rect(left, top, right - left, bottom - top); } + + static Rect shrink(Rect box, double left, double top, double right, double bottom) + { + box.x += left; + box.y += top; + box.width = std::max(box.width - left - right, 0.0); + box.height = std::max(box.height - bottom - top, 0.0); + return box; + } + + bool contains(const Point& p) const { return (p.x >= x && p.x < x + width) && (p.y >= y && p.y < y + height); } + + double x = 0; + double y = 0; + double width = 0; + double height = 0; +}; + +inline Point operator+(const Point& a, const Point& b) { return Point(a.x + b.x, a.y + b.y); } +inline Point operator-(const Point& a, const Point& b) { return Point(a.x - b.x, a.y - b.y); } +inline bool operator==(const Point& a, const Point& b) { return a.x == b.x && a.y == b.y; } +inline bool operator!=(const Point& a, const Point& b) { return a.x != b.x || a.y != b.y; } +inline bool operator==(const Size& a, const Size& b) { return a.width == b.width && a.height == b.height; } +inline bool operator!=(const Size& a, const Size& b) { return a.width != b.width || a.height != b.height; } +inline bool operator==(const Rect& a, const Rect& b) { return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height; } +inline bool operator!=(const Rect& a, const Rect& b) { return a.x != b.x || a.y != b.y || a.width != b.width || a.height != b.height; } + +inline Point operator+(const Point& a, double b) { return Point(a.x + b, a.y + b); } +inline Point operator-(const Point& a, double b) { return Point(a.x - b, a.y - b); } +inline Point operator*(const Point& a, double b) { return Point(a.x * b, a.y * b); } +inline Point operator/(const Point& a, double b) { return Point(a.x / b, a.y / b); } + +inline Size operator+(const Size& a, double b) { return Size(a.width + b, a.height + b); } +inline Size operator-(const Size& a, double b) { return Size(a.width - b, a.height - b); } +inline Size operator*(const Size& a, double b) { return Size(a.width * b, a.height * b); } +inline Size operator/(const Size& a, double b) { return Size(a.width / b, a.height / b); } diff --git a/libraries/ZWidget/include/zwidget/core/resourcedata.h b/libraries/ZWidget/include/zwidget/core/resourcedata.h new file mode 100644 index 0000000000..5eaedcfae4 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/resourcedata.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include + +struct SingleFontData +{ + std::vector fontdata; + std::vector> ranges; + int language = -1; // mainly useful if we start supporting Chinese so that we can use another font there than for Japanese. +}; + +std::vector LoadWidgetFontData(const std::string& name); +std::vector LoadWidgetImageData(const std::string& name); diff --git a/libraries/ZWidget/include/zwidget/core/span_layout.h b/libraries/ZWidget/include/zwidget/core/span_layout.h new file mode 100644 index 0000000000..974feca405 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/span_layout.h @@ -0,0 +1,238 @@ +#pragma once + +#include +#include + +#include "colorf.h" +#include "rect.h" +#include "font.h" +#include "canvas.h" + +class Widget; +class Image; +class Canvas; + +enum SpanAlign +{ + span_left, + span_right, + span_center, + span_justify +}; + +class SpanLayout +{ +public: + SpanLayout(); + ~SpanLayout(); + + struct HitTestResult + { + enum Type + { + no_objects_available, + outside_top, + outside_left, + outside_right, + outside_bottom, + inside + }; + + Type type = {}; + int object_id = -1; + size_t offset = 0; + }; + + void Clear(); + + void AddText(const std::string& text, std::shared_ptr font, const Colorf& color = Colorf(), int id = -1); + void AddImage(const std::shared_ptr image, double baseline_offset = 0, int id = -1); + void AddWidget(Widget* component, double baseline_offset = 0, int id = -1); + + void Layout(Canvas* canvas, double max_width); + + void SetPosition(const Point& pos); + + Size GetSize() const; + Rect GetRect() const; + + std::vector GetRectById(int id) const; + + HitTestResult HitTest(Canvas* canvas, const Point& pos); + + void DrawLayout(Canvas* canvas); + + /// Draw layout generating ellipsis for clipped text + void DrawLayoutEllipsis(Canvas* canvas, const Rect& content_rect); + + void SetComponentGeometry(); + + Size FindPreferredSize(Canvas* canvas); + + void SetSelectionRange(std::string::size_type start, std::string::size_type end); + void SetSelectionColors(const Colorf& foreground, const Colorf& background); + + void ShowCursor(); + void HideCursor(); + + void SetCursorPos(std::string::size_type pos); + void SetCursorOverwriteMode(bool enable); + void SetCursorColor(const Colorf& color); + + std::string GetCombinedText() const; + + void SetAlign(SpanAlign align); + + double GetFirstBaselineOffset(); + double GetLastBaselineOffset(); + +private: + struct TextBlock + { + size_t start = 0; + size_t end = 0; + }; + + enum ObjectType + { + object_text, + object_image, + object_component + }; + + enum FloatType + { + float_none, + float_left, + float_right + }; + + struct SpanObject + { + ObjectType type = object_text; + FloatType float_type = float_none; + + std::shared_ptr font; + Colorf color; + size_t start = 0, end = 0; + + std::shared_ptr image; + Widget* component = nullptr; + double baseline_offset = 0; + + int id = -1; + }; + + struct LineSegment + { + ObjectType type = object_text; + + std::shared_ptr font; + Colorf color; + size_t start = 0; + size_t end = 0; + double ascender = 0; + double descender = 0; + + double x_position = 0; + double width = 0; + + std::shared_ptr image; + Widget* component = nullptr; + double baseline_offset = 0; + + int id = -1; + }; + + struct Line + { + double width = 0; // Width of the entire line (including spaces) + double height = 0; + double ascender = 0; + std::vector segments; + }; + + struct TextSizeResult + { + size_t start = 0; + size_t end = 0; + double width = 0; + double height = 0; + double ascender = 0; + double descender = 0; + int objects_traversed = 0; + std::vector segments; + }; + + struct CurrentLine + { + std::vector::size_type object_index = 0; + Line cur_line; + double x_position = 0; + double y_position = 0; + }; + + struct FloatBox + { + Rect rect; + ObjectType type = object_image; + std::shared_ptr image; + Widget* component = nullptr; + int id = -1; + }; + + TextSizeResult FindTextSize(Canvas* canvas, const TextBlock& block, size_t object_index); + std::vector FindTextBlocks(); + void LayoutLines(Canvas* canvas, double max_width); + void LayoutText(Canvas* canvas, std::vector blocks, std::vector::size_type block_index, CurrentLine& current_line, double max_width); + void LayoutBlock(CurrentLine& current_line, double max_width, std::vector& blocks, std::vector::size_type block_index); + void LayoutFloatBlock(CurrentLine& current_line, double max_width); + void LayoutInlineBlock(CurrentLine& current_line, double max_width, std::vector& blocks, std::vector::size_type block_index); + void ReflowLine(CurrentLine& current_line, double max_width); + FloatBox FloatBoxLeft(FloatBox float_box, double max_width); + FloatBox FloatBoxRight(FloatBox float_box, double max_width); + FloatBox FloatBoxAny(FloatBox box, double max_width, const std::vector& floats1); + bool BoxFitsOnLine(const FloatBox& box, double max_width); + void PlaceLineSegments(CurrentLine& current_line, TextSizeResult& text_size_result); + void ForcePlaceLineSegments(CurrentLine& current_line, TextSizeResult& text_size_result, double max_width); + void NextLine(CurrentLine& current_line); + bool IsNewline(const TextBlock& block); + bool IsWhitespace(const TextBlock& block); + bool FitsOnLine(double x_position, const TextSizeResult& text_size_result, double max_width); + bool LargerThanLine(const TextSizeResult& text_size_result, double max_width); + void AlignJustify(double max_width); + void AlignCenter(double max_width); + void AlignRight(double max_width); + void DrawLayoutImage(Canvas* canvas, Line& line, LineSegment& segment, double x, double y); + void DrawLayoutText(Canvas* canvas, Line& line, LineSegment& segment, double x, double y); + + bool cursor_visible = false; + std::string::size_type cursor_pos = 0; + bool cursor_overwrite_mode = false; + Colorf cursor_color; + + std::string::size_type sel_start = 0, sel_end = 0; + Colorf sel_foreground, sel_background = Colorf::fromRgba8(153, 201, 239); + + std::string text; + std::vector objects; + std::vector lines; + Point position; + + std::vector floats_left, floats_right; + + SpanAlign alignment = span_left; + + struct LayoutCache + { + int object_index = -1; + FontMetrics metrics; + }; + LayoutCache layout_cache; + + bool is_ellipsis_draw = false; + Rect ellipsis_content_rect; + + template + static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } +}; diff --git a/libraries/ZWidget/include/zwidget/core/timer.h b/libraries/ZWidget/include/zwidget/core/timer.h new file mode 100644 index 0000000000..25e6fb3461 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/timer.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +class Widget; + +class Timer +{ +public: + Timer(Widget* owner); + ~Timer(); + + void Start(int timeoutMilliseconds, bool repeat = true); + void Stop(); + + std::function FuncExpired; + +private: + Widget* OwnerObj = nullptr; + Timer* PrevTimerObj = nullptr; + Timer* NextTimerObj = nullptr; + + void* TimerId = nullptr; + + friend class Widget; +}; diff --git a/libraries/ZWidget/include/zwidget/core/utf8reader.h b/libraries/ZWidget/include/zwidget/core/utf8reader.h new file mode 100644 index 0000000000..f9c186b47d --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/utf8reader.h @@ -0,0 +1,78 @@ +/* +** Copyright (c) 1997-2015 Mark Page +** +** This software is provided 'as-is', without any express or implied +** warranty. In no event will the authors be held liable for any damages +** arising from the use of this software. +** +** Permission is granted to anyone to use this software for any purpose, +** including commercial applications, and to alter it and redistribute it +** freely, subject to the following restrictions: +** +** 1. The origin of this software must not be misrepresented; you must not +** claim that you wrote the original software. If you use this software +** in a product, an acknowledgment in the product documentation would be +** appreciated but is not required. +** 2. Altered source versions must be plainly marked as such, and must not be +** misrepresented as being the original software. +** 3. This notice may not be removed or altered from any source distribution. +** +*/ + +#pragma once + +#include + +/// \brief UTF8 reader helper functions. +class UTF8Reader +{ +public: + /// Important: text is not copied by this class and must remain valid during its usage. + UTF8Reader(const std::string::value_type *text, std::string::size_type length); + + /// \brief Returns true if the current position is at the end of the string + bool is_end(); + + /// \brief Get the character at the current position + unsigned int character(); + + /// \brief Returns the length of the current character + std::string::size_type char_length(); + + /// \brief Moves position to the previous character + void prev(); + + /// \brief Moves position to the next character + void next(); + + /// \brief Moves position to the lead byte of the character + void move_to_leadbyte(); + + /// \brief Get the current position of the reader + std::string::size_type position(); + + /// \brief Set the current position of the reader + void set_position(std::string::size_type position); + + static size_t utf8_length(const std::string& text) + { + return utf8_length(text.data(), text.size()); + } + + static size_t utf8_length(const std::string::value_type* text, std::string::size_type length) + { + UTF8Reader reader(text, length); + size_t i = 0; + while (!reader.is_end()) + { + reader.next(); + i++; + } + return i; + } + +private: + std::string::size_type current_position = 0; + std::string::size_type length = 0; + const unsigned char *data = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/core/widget.h b/libraries/ZWidget/include/zwidget/core/widget.h new file mode 100644 index 0000000000..69862fdb49 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/widget.h @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include "canvas.h" +#include "rect.h" +#include "colorf.h" +#include "../window/window.h" + +class Canvas; +class Timer; + +enum class WidgetType +{ + Child, + Window, + Popup +}; + +class Widget : DisplayWindowHost +{ +public: + Widget(Widget* parent = nullptr, WidgetType type = WidgetType::Child); + virtual ~Widget(); + + void SetParent(Widget* parent); + void MoveBefore(Widget* sibling); + + std::string GetWindowTitle() const; + void SetWindowTitle(const std::string& text); + + // Icon GetWindowIcon() const; + // void SetWindowIcon(const Icon& icon); + + // Widget content box + Size GetSize() const; + double GetWidth() const { return GetSize().width; } + double GetHeight() const { return GetSize().height; } + + // Widget noncontent area + void SetNoncontentSizes(double left, double top, double right, double bottom); + + // Widget frame box + Rect GetFrameGeometry() const; + void SetFrameGeometry(const Rect& geometry); + void SetFrameGeometry(double x, double y, double width, double height) { SetFrameGeometry(Rect::xywh(x, y, width, height)); } + + void SetWindowBackground(const Colorf& color); + void SetWindowBorderColor(const Colorf& color); + void SetWindowCaptionColor(const Colorf& color); + void SetWindowCaptionTextColor(const Colorf& color); + + void SetVisible(bool enable) { if (enable) Show(); else Hide(); } + void Show(); + void ShowFullscreen(); + void ShowMaximized(); + void ShowMinimized(); + void ShowNormal(); + void Hide(); + + void ActivateWindow(); + + void Close(); + + void Update(); + void Repaint(); + + bool HasFocus(); + bool IsEnabled(); + bool IsVisible(); + + void SetFocus(); + void SetEnabled(bool value); + void SetDisabled(bool value) { SetEnabled(!value); } + void SetHidden(bool value) { if (value) Hide(); else Show(); } + + void LockCursor(); + void UnlockCursor(); + void SetCursor(StandardCursor cursor); + void CaptureMouse(); + void ReleaseMouseCapture(); + + bool GetKeyState(EInputKey key); + + std::string GetClipboardText(); + void SetClipboardText(const std::string& text); + + Widget* Window(); + Canvas* GetCanvas(); + Widget* ChildAt(double x, double y) { return ChildAt(Point(x, y)); } + Widget* ChildAt(const Point& pos); + + Widget* Parent() const { return ParentObj; } + Widget* PrevSibling() const { return PrevSiblingObj; } + Widget* NextSibling() const { return NextSiblingObj; } + Widget* FirstChild() const { return FirstChildObj; } + Widget* LastChild() const { return LastChildObj; } + + Point MapFrom(const Widget* parent, const Point& pos) const; + Point MapFromGlobal(const Point& pos) const; + Point MapFromParent(const Point& pos) const { return MapFrom(Parent(), pos); } + + Point MapTo(const Widget* parent, const Point& pos) const; + Point MapToGlobal(const Point& pos) const; + Point MapToParent(const Point& pos) const { return MapTo(Parent(), pos); } + + static Size GetScreenSize(); + +protected: + virtual void OnPaintFrame(Canvas* canvas) { } + virtual void OnPaint(Canvas* canvas) { } + virtual void OnMouseMove(const Point& pos) { } + virtual void OnMouseDown(const Point& pos, int key) { } + virtual void OnMouseDoubleclick(const Point& pos, int key) { } + virtual void OnMouseUp(const Point& pos, int key) { } + virtual void OnMouseWheel(const Point& pos, EInputKey key) { } + virtual void OnMouseLeave() { } + virtual void OnRawMouseMove(int dx, int dy) { } + virtual void OnKeyChar(std::string chars) { } + virtual void OnKeyDown(EInputKey key) { } + virtual void OnKeyUp(EInputKey key) { } + virtual void OnGeometryChanged() { } + virtual void OnClose() { delete this; } + virtual void OnSetFocus() { } + virtual void OnLostFocus() { } + virtual void OnEnableChanged() { } + +private: + void DetachFromParent(); + + void Paint(Canvas* canvas); + + // DisplayWindowHost + void OnWindowPaint() override; + void OnWindowMouseMove(const Point& pos) override; + void OnWindowMouseDown(const Point& pos, EInputKey key) override; + void OnWindowMouseDoubleclick(const Point& pos, EInputKey key) override; + void OnWindowMouseUp(const Point& pos, EInputKey key) override; + void OnWindowMouseWheel(const Point& pos, EInputKey key) override; + void OnWindowRawMouseMove(int dx, int dy) override; + void OnWindowKeyChar(std::string chars) override; + void OnWindowKeyDown(EInputKey key) override; + void OnWindowKeyUp(EInputKey key) override; + void OnWindowGeometryChanged() override; + void OnWindowClose() override; + void OnWindowActivated() override; + void OnWindowDeactivated() override; + void OnWindowDpiScaleChanged() override; + + WidgetType Type = {}; + + Widget* ParentObj = nullptr; + Widget* PrevSiblingObj = nullptr; + Widget* NextSiblingObj = nullptr; + Widget* FirstChildObj = nullptr; + Widget* LastChildObj = nullptr; + + Timer* FirstTimerObj = nullptr; + + Rect FrameGeometry = Rect::xywh(0.0, 0.0, 0.0, 0.0); + Rect ContentGeometry = Rect::xywh(0.0, 0.0, 0.0, 0.0); + + Colorf WindowBackground = Colorf::fromRgba8(240, 240, 240); + + struct + { + double Left = 0.0; + double Top = 0.0; + double Right = 0.0; + double Bottom = 0.0; + } Noncontent; + + std::string WindowTitle; + std::unique_ptr DispWindow; + std::unique_ptr DispCanvas; + Widget* FocusWidget = nullptr; + Widget* CaptureWidget = nullptr; + Widget* HoverWidget = nullptr; + + StandardCursor CurrentCursor = StandardCursor::arrow; + + Widget(const Widget&) = delete; + Widget& operator=(const Widget&) = delete; + + friend class Timer; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h b/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h new file mode 100644 index 0000000000..03d9089ebe --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h @@ -0,0 +1,35 @@ + +#pragma once + +#include "../../core/widget.h" + +class CheckboxLabel : public Widget +{ +public: + CheckboxLabel(Widget* parent = nullptr); + + void SetText(const std::string& value); + const std::string& GetText() const; + + void SetChecked(bool value); + bool GetChecked() const; + void Toggle(); + + double GetPreferredHeight() const; + std::function FuncChanged; + void SetRadioStyle(bool on) { radiostyle = on; } + +protected: + void OnPaint(Canvas* canvas) override; + void OnMouseDown(const Point& pos, int key) override; + void OnMouseUp(const Point& pos, int key) override; + void OnMouseLeave() override; + void OnKeyUp(EInputKey key) override; + +private: + std::string text; + bool checked = false; + bool radiostyle = false; + bool mouseDownActive = false; + +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h b/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h new file mode 100644 index 0000000000..e3a724381f --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h @@ -0,0 +1,21 @@ + +#pragma once + +#include "../../core/widget.h" +#include "../../core/image.h" + +class ImageBox : public Widget +{ +public: + ImageBox(Widget* parent); + + void SetImage(std::shared_ptr newImage); + + double GetPreferredHeight() const; + +protected: + void OnPaint(Canvas* canvas) override; + +private: + std::shared_ptr image; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h b/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h new file mode 100644 index 0000000000..c34ab11905 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h @@ -0,0 +1,160 @@ + +#pragma once + +#include "../../core/widget.h" +#include "../../core/timer.h" +#include + +class LineEdit : public Widget +{ +public: + LineEdit(Widget* parent); + ~LineEdit(); + + enum Alignment + { + align_left, + align_center, + align_right + }; + + Alignment GetAlignment() const; + bool IsReadOnly() const; + bool IsLowercase() const; + bool IsUppercase() const; + bool IsPasswordMode() const; + int GetMaxLength() const; + + std::string GetText() const; + int GetTextInt() const; + float GetTextFloat() const; + + std::string GetSelection() const; + int GetSelectionStart() const; + int GetSelectionLength() const; + + int GetCursorPos() const; + Size GetTextSize(); + + Size GetTextSize(const std::string& str); + double GetPreferredContentWidth(); + double GetPreferredContentHeight(double width); + + void SetSelectAllOnFocusGain(bool enable); + void SelectAll(); + void SetAlignment(Alignment alignment); + void SetReadOnly(bool enable = true); + void SetLowercase(bool enable = true); + void SetUppercase(bool enable = true); + void SetPasswordMode(bool enable = true); + void SetNumericMode(bool enable = true, bool decimals = false); + void SetMaxLength(int length); + void SetText(const std::string& text); + void SetTextInt(int number); + void SetTextFloat(float number, int num_decimal_places = 6); + void SetSelection(int pos, int length); + void ClearSelection(); + void SetCursorPos(int pos); + void DeleteSelectedText(); + void SetInputMask(const std::string& mask); + void SetDecimalCharacter(const std::string& decimal_char); + + std::function FuncIgnoreKeyDown; + std::function FuncFilterKeyChar; + std::function FuncBeforeEditChanged; + std::function FuncAfterEditChanged; + std::function FuncSelectionChanged; + std::function FuncFocusGained; + std::function FuncFocusLost; + std::function FuncEnterPressed; + +protected: + void OnPaintFrame(Canvas* canvas) override; + void OnPaint(Canvas* canvas) override; + void OnMouseMove(const Point& pos) override; + void OnMouseDown(const Point& pos, int key) override; + void OnMouseDoubleclick(const Point& pos, int key) override; + void OnMouseUp(const Point& pos, int key) override; + void OnKeyChar(std::string chars) override; + void OnKeyDown(EInputKey key) override; + void OnKeyUp(EInputKey key) override; + void OnGeometryChanged() override; + void OnEnableChanged() override; + void OnSetFocus() override; + void OnLostFocus() override; + +private: + void OnTimerExpired(); + void OnScrollTimerExpired(); + void UpdateTextClipping(); + + void Move(int steps, bool ctrl, bool shift); + bool InsertText(int pos, const std::string& str); + void Backspace(); + void Del(); + int GetCharacterIndex(double x); + int FindNextBreakCharacter(int pos); + int FindPreviousBreakCharacter(int pos); + std::string GetVisibleTextBeforeSelection(); + std::string GetVisibleTextAfterSelection(); + std::string GetVisibleSelectedText(); + std::string CreatePassword(std::string::size_type num_letters) const; + Size GetVisualTextSize(Canvas* canvas, int pos, int npos) const; + Size GetVisualTextSize(Canvas* canvas) const; + Rect GetCursorRect(); + Rect GetSelectionRect(); + bool InputMaskAcceptsInput(int cursor_pos, const std::string& str); + void SetSelectionStart(int start); + void SetSelectionLength(int length); + void SetTextSelection(int start, int length); + + static std::string ToFixed(float number, int num_decimal_places); + static std::string ToLower(const std::string& text); + static std::string ToUpper(const std::string& text); + + Timer* timer = nullptr; + std::string text; + Alignment alignment = align_left; + int cursor_pos = 0; + int max_length = -1; + bool mouse_selecting = false; + bool lowercase = false; + bool uppercase = false; + bool password_mode = false; + bool numeric_mode = false; + bool numeric_mode_decimals = false; + bool readonly = false; + int selection_start = -1; + int selection_length = 0; + std::string input_mask; + std::string decimal_char = "."; + + VerticalTextPosition vertical_text_align; + Timer* scroll_timer = nullptr; + + bool mouse_moves_left = false; + bool cursor_blink_visible = true; + unsigned int blink_timer = 0; + int clip_start_offset = 0; + int clip_end_offset = 0; + bool ignore_mouse_events = false; + + struct UndoInfo + { + /* set undo text when: + - added char after moving + - destructive block operation (del, cut etc) + - beginning erase + */ + std::string undo_text; + bool first_erase = false; + bool first_text_insert = false; + }; + + UndoInfo undo_info; + + bool select_all_on_focus_gain = true; + + static const std::string break_characters; + static const std::string numeric_mode_characters; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/listview/listview.h b/libraries/ZWidget/include/zwidget/widgets/listview/listview.h new file mode 100644 index 0000000000..ad30cd17c2 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/listview/listview.h @@ -0,0 +1,37 @@ + +#pragma once + +#include "../../core/widget.h" +#include +#include + +class Scrollbar; + +class ListView : public Widget +{ +public: + ListView(Widget* parent = nullptr); + + void AddItem(const std::string& text); + int GetSelectedItem() const { return selectedItem; } + void ScrollToItem(int index); + + void Activate(); + + std::function OnActivated; + +protected: + void OnPaint(Canvas* canvas) override; + void OnPaintFrame(Canvas* canvas) override; + void OnMouseDown(const Point& pos, int key) override; + void OnMouseDoubleclick(const Point& pos, int key) override; + void OnMouseWheel(const Point& pos, EInputKey key) override; + void OnKeyDown(EInputKey key) override; + void OnGeometryChanged() override; + void OnScrollbarScroll(); + + Scrollbar* scrollbar = nullptr; + + std::vector items; + int selectedItem = 0; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h b/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h new file mode 100644 index 0000000000..abba23d6c4 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h @@ -0,0 +1,31 @@ + +#pragma once + +#include "../../core/widget.h" + +class Menubar; +class Toolbar; +class Statusbar; + +class MainWindow : public Widget +{ +public: + MainWindow(); + ~MainWindow(); + + Menubar* GetMenubar() const { return MenubarWidget; } + Toolbar* GetToolbar() const { return ToolbarWidget; } + Statusbar* GetStatusbar() const { return StatusbarWidget; } + Widget* GetCentralWidget() const { return CentralWidget; } + + void SetCentralWidget(Widget* widget); + +protected: + void OnGeometryChanged() override; + +private: + Menubar* MenubarWidget = nullptr; + Toolbar* ToolbarWidget = nullptr; + Widget* CentralWidget = nullptr; + Statusbar* StatusbarWidget = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h b/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h new file mode 100644 index 0000000000..9c13283ccc --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h @@ -0,0 +1,14 @@ + +#pragma once + +#include "../../core/widget.h" + +class Menubar : public Widget +{ +public: + Menubar(Widget* parent); + ~Menubar(); + +protected: + void OnPaint(Canvas* canvas) override; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h b/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h new file mode 100644 index 0000000000..80d827d392 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h @@ -0,0 +1,35 @@ + +#pragma once + +#include "../../core/widget.h" +#include + +class PushButton : public Widget +{ +public: + PushButton(Widget* parent = nullptr); + + void SetText(const std::string& value); + const std::string& GetText() const; + + double GetPreferredHeight() const; + + void Click(); + + std::function OnClick; + +protected: + void OnPaintFrame(Canvas* canvas) override; + void OnPaint(Canvas* canvas) override; + void OnMouseMove(const Point& pos) override; + void OnMouseDown(const Point& pos, int key) override; + void OnMouseUp(const Point& pos, int key) override; + void OnMouseLeave() override; + void OnKeyDown(EInputKey key) override; + void OnKeyUp(EInputKey key) override; + +private: + std::string text; + bool buttonDown = false; + bool hot = false; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h b/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h new file mode 100644 index 0000000000..38afda3832 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h @@ -0,0 +1,99 @@ + +#pragma once + +#include "../../core/widget.h" +#include "../../core/timer.h" +#include + +class Scrollbar : public Widget +{ +public: + Scrollbar(Widget* parent); + ~Scrollbar(); + + bool IsVertical() const; + bool IsHorizontal() const; + double GetMin() const; + double GetMax() const; + double GetLineStep() const; + double GetPageStep() const; + double GetPosition() const; + + void SetVertical(); + void SetHorizontal(); + + void SetMin(double scroll_min); + void SetMax(double scroll_max); + void SetLineStep(double step); + void SetPageStep(double step); + + void SetRanges(double scroll_min, double scroll_max, double line_step, double page_step); + void SetRanges(double view_size, double total_size); + + void SetPosition(double pos); + + double GetPreferredWidth() const { return 16.0; } + double GetPreferredHeight() const { return 16.0; } + + std::function FuncScroll; + std::function FuncScrollMin; + std::function FuncScrollMax; + std::function FuncScrollLineDecrement; + std::function FuncScrollLineIncrement; + std::function FuncScrollPageDecrement; + std::function FuncScrollPageIncrement; + std::function FuncScrollThumbRelease; + std::function FuncScrollThumbTrack; + std::function FuncScrollEnd; + +protected: + void OnMouseMove(const Point& pos) override; + void OnMouseDown(const Point& pos, int key) override; + void OnMouseUp(const Point& pos, int key) override; + void OnMouseLeave() override; + void OnPaint(Canvas* canvas) override; + void OnEnableChanged() override; + void OnGeometryChanged() override; + +private: + bool UpdatePartPositions(); + double CalculateThumbSize(double track_size); + double CalculateThumbPosition(double thumb_size, double track_size); + Rect CreateRect(double start, double end); + void InvokeScrollEvent(std::function* event_ptr); + void OnTimerExpired(); + + bool vertical = true; + double scroll_min = 0.0; + double scroll_max = 1.0; + double line_step = 1.0; + double page_step = 10.0; + double position = 0.0; + + bool showbuttons = false; + + enum MouseDownMode + { + mouse_down_none, + mouse_down_button_decr, + mouse_down_button_incr, + mouse_down_track_decr, + mouse_down_track_incr, + mouse_down_thumb_drag + } mouse_down_mode = mouse_down_none; + + double thumb_start_position = 0.0; + Point mouse_drag_start_pos; + double thumb_start_pixel_position = 0.0; + + Timer* mouse_down_timer = nullptr; + double last_step_size = 0.0; + + Rect rect_button_decrement; + Rect rect_track_decrement; + Rect rect_thumb; + Rect rect_track_increment; + Rect rect_button_increment; + + std::function* FuncScrollOnMouseDown = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/statusbar/statusbar.h b/libraries/ZWidget/include/zwidget/widgets/statusbar/statusbar.h new file mode 100644 index 0000000000..c03a3e282c --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/statusbar/statusbar.h @@ -0,0 +1,19 @@ + +#pragma once + +#include "../../core/widget.h" + +class LineEdit; + +class Statusbar : public Widget +{ +public: + Statusbar(Widget* parent); + ~Statusbar(); + +protected: + void OnPaint(Canvas* canvas) override; + +private: + LineEdit* CommandEdit = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h b/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h new file mode 100644 index 0000000000..e2c6c760c9 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h @@ -0,0 +1,156 @@ + +#pragma once + +#include "../../core/widget.h" +#include "../../core/timer.h" +#include "../../core/span_layout.h" +#include "../../core/font.h" +#include + +class Scrollbar; + +class TextEdit : public Widget +{ +public: + TextEdit(Widget* parent); + ~TextEdit(); + + bool IsReadOnly() const; + bool IsLowercase() const; + bool IsUppercase() const; + int GetMaxLength() const; + std::string GetText() const; + int GetLineCount() const; + std::string GetLineText(int line) const; + std::string GetSelection() const; + int GetSelectionStart() const; + int GetSelectionLength() const; + int GetCursorPos() const; + int GetCursorLineNumber() const; + double GetTotalHeight(); + + void SetSelectAllOnFocusGain(bool enable); + void SelectAll(); + void SetReadOnly(bool enable = true); + void SetLowercase(bool enable = true); + void SetUppercase(bool enable = true); + void SetMaxLength(int length); + void SetText(const std::string& text); + void AddText(const std::string& text); + void SetSelection(int pos, int length); + void ClearSelection(); + void SetCursorPos(int pos); + void DeleteSelectedText(); + void SetInputMask(const std::string& mask); + void SetCursorDrawingEnabled(bool enable); + + std::function FuncFilterKeyChar; + std::function FuncBeforeEditChanged; + std::function FuncAfterEditChanged; + std::function FuncSelectionChanged; + std::function FuncFocusGained; + std::function FuncFocusLost; + std::function FuncEnterPressed; + +protected: + void OnPaintFrame(Canvas* canvas) override; + void OnPaint(Canvas* canvas) override; + void OnMouseMove(const Point& pos) override; + void OnMouseDown(const Point& pos, int key) override; + void OnMouseDoubleclick(const Point& pos, int key) override; + void OnMouseUp(const Point& pos, int key) override; + void OnKeyChar(std::string chars) override; + void OnKeyDown(EInputKey key) override; + void OnKeyUp(EInputKey key) override; + void OnGeometryChanged() override; + void OnEnableChanged() override; + void OnSetFocus() override; + void OnLostFocus() override; + +private: + void LayoutLines(Canvas* canvas); + + void OnTimerExpired(); + void OnScrollTimerExpired(); + void CreateComponents(); + void OnVerticalScroll(); + void UpdateVerticalScroll(); + void MoveVerticalScroll(); + double GetTotalLineHeight(); + + struct Line + { + std::string text; + SpanLayout layout; + Rect box; + bool invalidated = true; + }; + + struct ivec2 + { + ivec2() = default; + ivec2(int x, int y) : x(x), y(y) { } + int x = 0; + int y = 0; + + bool operator==(const ivec2& b) const { return x == b.x && y == b.y; } + bool operator!=(const ivec2& b) const { return x != b.x || y != b.y; } + }; + + Scrollbar* vert_scrollbar; + Timer* timer = nullptr; + std::vector lines = { Line() }; + ivec2 cursor_pos = { 0, 0 }; + int max_length = -1; + bool mouse_selecting = false; + bool lowercase = false; + bool uppercase = false; + bool readonly = false; + ivec2 selection_start = { -1, 0 }; + int selection_length = 0; + std::string input_mask; + + static std::string break_characters; + + void Move(int steps, bool shift, bool ctrl); + void InsertText(ivec2 pos, const std::string& str); + void Backspace(); + void Del(); + ivec2 GetCharacterIndex(Point mouse_wincoords); + ivec2 FindNextBreakCharacter(ivec2 pos); + ivec2 FindPreviousBreakCharacter(ivec2 pos); + bool InputMaskAcceptsInput(ivec2 cursor_pos, const std::string& str); + + std::string::size_type ToOffset(ivec2 pos) const; + ivec2 FromOffset(std::string::size_type offset) const; + + VerticalTextPosition vertical_text_align; + Timer* scroll_timer = nullptr; + + bool mouse_moves_left = false; + bool cursor_blink_visible = true; + unsigned int blink_timer = 0; + int clip_start_offset = 0; + int clip_end_offset = 0; + bool ignore_mouse_events = false; + + struct UndoInfo + { + /* set undo text when: + - added char after moving + - destructive block operation (del, cut etc) + - beginning erase + */ + + std::string undo_text; + bool first_erase = false; + bool first_text_insert = false; + } undo_info; + + bool select_all_on_focus_gain = false; + + std::shared_ptr font = Font::Create("NotoSans", 12.0); + + template + static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h b/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h new file mode 100644 index 0000000000..32f250c6cc --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h @@ -0,0 +1,32 @@ + +#pragma once + +#include "../../core/widget.h" + +enum TextLabelAlignment +{ + Left, + Center, + Right +}; + +class TextLabel : public Widget +{ +public: + TextLabel(Widget* parent = nullptr); + + void SetText(const std::string& value); + const std::string& GetText() const; + + void SetTextAlignment(TextLabelAlignment alignment); + TextLabelAlignment GetTextAlignment() const; + + double GetPreferredHeight() const; + +protected: + void OnPaint(Canvas* canvas) override; + +private: + std::string text; + TextLabelAlignment textAlignment = TextLabelAlignment::Left; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h new file mode 100644 index 0000000000..ca0a89c0dc --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h @@ -0,0 +1,11 @@ + +#pragma once + +#include "../../core/widget.h" + +class Toolbar : public Widget +{ +public: + Toolbar(Widget* parent); + ~Toolbar(); +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h new file mode 100644 index 0000000000..933a6517ed --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h @@ -0,0 +1,14 @@ + +#pragma once + +#include "../../core/widget.h" + +class ToolbarButton : public Widget +{ +public: + ToolbarButton(Widget* parent); + ~ToolbarButton(); + +protected: + void OnPaint(Canvas* canvas) override; +}; diff --git a/libraries/ZWidget/include/zwidget/window/window.h b/libraries/ZWidget/include/zwidget/window/window.h new file mode 100644 index 0000000000..0539f773f8 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/window.h @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include +#include "../core/rect.h" + +class Engine; + +enum class StandardCursor +{ + arrow, + appstarting, + cross, + hand, + ibeam, + no, + size_all, + size_nesw, + size_ns, + size_nwse, + size_we, + uparrow, + wait +}; + +enum EInputKey +{ + IK_None, IK_LeftMouse, IK_RightMouse, IK_Cancel, + IK_MiddleMouse, IK_Unknown05, IK_Unknown06, IK_Unknown07, + IK_Backspace, IK_Tab, IK_Unknown0A, IK_Unknown0B, + IK_Unknown0C, IK_Enter, IK_Unknown0E, IK_Unknown0F, + IK_Shift, IK_Ctrl, IK_Alt, IK_Pause, + IK_CapsLock, IK_Unknown15, IK_Unknown16, IK_Unknown17, + IK_Unknown18, IK_Unknown19, IK_Unknown1A, IK_Escape, + IK_Unknown1C, IK_Unknown1D, IK_Unknown1E, IK_Unknown1F, + IK_Space, IK_PageUp, IK_PageDown, IK_End, + IK_Home, IK_Left, IK_Up, IK_Right, + IK_Down, IK_Select, IK_Print, IK_Execute, + IK_PrintScrn, IK_Insert, IK_Delete, IK_Help, + IK_0, IK_1, IK_2, IK_3, + IK_4, IK_5, IK_6, IK_7, + IK_8, IK_9, IK_Unknown3A, IK_Unknown3B, + IK_Unknown3C, IK_Unknown3D, IK_Unknown3E, IK_Unknown3F, + IK_Unknown40, IK_A, IK_B, IK_C, + IK_D, IK_E, IK_F, IK_G, + IK_H, IK_I, IK_J, IK_K, + IK_L, IK_M, IK_N, IK_O, + IK_P, IK_Q, IK_R, IK_S, + IK_T, IK_U, IK_V, IK_W, + IK_X, IK_Y, IK_Z, IK_Unknown5B, + IK_Unknown5C, IK_Unknown5D, IK_Unknown5E, IK_Unknown5F, + IK_NumPad0, IK_NumPad1, IK_NumPad2, IK_NumPad3, + IK_NumPad4, IK_NumPad5, IK_NumPad6, IK_NumPad7, + IK_NumPad8, IK_NumPad9, IK_GreyStar, IK_GreyPlus, + IK_Separator, IK_GreyMinus, IK_NumPadPeriod, IK_GreySlash, + IK_F1, IK_F2, IK_F3, IK_F4, + IK_F5, IK_F6, IK_F7, IK_F8, + IK_F9, IK_F10, IK_F11, IK_F12, + IK_F13, IK_F14, IK_F15, IK_F16, + IK_F17, IK_F18, IK_F19, IK_F20, + IK_F21, IK_F22, IK_F23, IK_F24, + IK_Unknown88, IK_Unknown89, IK_Unknown8A, IK_Unknown8B, + IK_Unknown8C, IK_Unknown8D, IK_Unknown8E, IK_Unknown8F, + IK_NumLock, IK_ScrollLock, IK_Unknown92, IK_Unknown93, + IK_Unknown94, IK_Unknown95, IK_Unknown96, IK_Unknown97, + IK_Unknown98, IK_Unknown99, IK_Unknown9A, IK_Unknown9B, + IK_Unknown9C, IK_Unknown9D, IK_Unknown9E, IK_Unknown9F, + IK_LShift, IK_RShift, IK_LControl, IK_RControl, + IK_UnknownA4, IK_UnknownA5, IK_UnknownA6, IK_UnknownA7, + IK_UnknownA8, IK_UnknownA9, IK_UnknownAA, IK_UnknownAB, + IK_UnknownAC, IK_UnknownAD, IK_UnknownAE, IK_UnknownAF, + IK_UnknownB0, IK_UnknownB1, IK_UnknownB2, IK_UnknownB3, + IK_UnknownB4, IK_UnknownB5, IK_UnknownB6, IK_UnknownB7, + IK_UnknownB8, IK_UnknownB9, IK_Semicolon, IK_Equals, + IK_Comma, IK_Minus, IK_Period, IK_Slash, + IK_Tilde, IK_UnknownC1, IK_UnknownC2, IK_UnknownC3, + IK_UnknownC4, IK_UnknownC5, IK_UnknownC6, IK_UnknownC7, + IK_Joy1, IK_Joy2, IK_Joy3, IK_Joy4, + IK_Joy5, IK_Joy6, IK_Joy7, IK_Joy8, + IK_Joy9, IK_Joy10, IK_Joy11, IK_Joy12, + IK_Joy13, IK_Joy14, IK_Joy15, IK_Joy16, + IK_UnknownD8, IK_UnknownD9, IK_UnknownDA, IK_LeftBracket, + IK_Backslash, IK_RightBracket, IK_SingleQuote, IK_UnknownDF, + IK_JoyX, IK_JoyY, IK_JoyZ, IK_JoyR, + IK_MouseX, IK_MouseY, IK_MouseZ, IK_MouseW, + IK_JoyU, IK_JoyV, IK_UnknownEA, IK_UnknownEB, + IK_MouseWheelUp, IK_MouseWheelDown, IK_Unknown10E, IK_Unknown10F, + IK_JoyPovUp, IK_JoyPovDown, IK_JoyPovLeft, IK_JoyPovRight, + IK_UnknownF4, IK_UnknownF5, IK_Attn, IK_CrSel, + IK_ExSel, IK_ErEof, IK_Play, IK_Zoom, + IK_NoName, IK_PA1, IK_OEMClear +}; + +enum EInputType +{ + IST_None, + IST_Press, + IST_Hold, + IST_Release, + IST_Axis +}; + +class KeyEvent +{ +public: + EInputKey Key; + bool Ctrl = false; + bool Alt = false; + bool Shift = false; + Point MousePos = Point(0.0, 0.0); +}; + +class DisplayWindow; + +class DisplayWindowHost +{ +public: + virtual void OnWindowPaint() = 0; + virtual void OnWindowMouseMove(const Point& pos) = 0; + virtual void OnWindowMouseDown(const Point& pos, EInputKey key) = 0; + virtual void OnWindowMouseDoubleclick(const Point& pos, EInputKey key) = 0; + virtual void OnWindowMouseUp(const Point& pos, EInputKey key) = 0; + virtual void OnWindowMouseWheel(const Point& pos, EInputKey key) = 0; + virtual void OnWindowRawMouseMove(int dx, int dy) = 0; + virtual void OnWindowKeyChar(std::string chars) = 0; + virtual void OnWindowKeyDown(EInputKey key) = 0; + virtual void OnWindowKeyUp(EInputKey key) = 0; + virtual void OnWindowGeometryChanged() = 0; + virtual void OnWindowClose() = 0; + virtual void OnWindowActivated() = 0; + virtual void OnWindowDeactivated() = 0; + virtual void OnWindowDpiScaleChanged() = 0; +}; + +class DisplayWindow +{ +public: + static std::unique_ptr Create(DisplayWindowHost* windowHost); + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + + static Size GetScreenSize(); + + virtual ~DisplayWindow() = default; + + virtual void SetWindowTitle(const std::string& text) = 0; + virtual void SetWindowFrame(const Rect& box) = 0; + virtual void SetClientFrame(const Rect& box) = 0; + virtual void Show() = 0; + virtual void ShowFullscreen() = 0; + virtual void ShowMaximized() = 0; + virtual void ShowMinimized() = 0; + virtual void ShowNormal() = 0; + virtual void Hide() = 0; + virtual void Activate() = 0; + virtual void ShowCursor(bool enable) = 0; + virtual void LockCursor() = 0; + virtual void UnlockCursor() = 0; + virtual void CaptureMouse() = 0; + virtual void ReleaseMouseCapture() = 0; + virtual void Update() = 0; + virtual bool GetKeyState(EInputKey key) = 0; + + virtual void SetCursor(StandardCursor cursor) = 0; + + virtual Rect GetWindowFrame() const = 0; + virtual Size GetClientSize() const = 0; + virtual int GetPixelWidth() const = 0; + virtual int GetPixelHeight() const = 0; + virtual double GetDpiScale() const = 0; + + virtual void SetBorderColor(uint32_t bgra8) = 0; + virtual void SetCaptionColor(uint32_t bgra8) = 0; + virtual void SetCaptionTextColor(uint32_t bgra8) = 0; + + virtual void PresentBitmap(int width, int height, const uint32_t* pixels) = 0; + + virtual std::string GetClipboardText() = 0; + virtual void SetClipboardText(const std::string& text) = 0; +}; diff --git a/libraries/ZWidget/src/core/canvas.cpp b/libraries/ZWidget/src/core/canvas.cpp new file mode 100644 index 0000000000..26f137ca89 --- /dev/null +++ b/libraries/ZWidget/src/core/canvas.cpp @@ -0,0 +1,736 @@ + +#include "core/canvas.h" +#include "core/rect.h" +#include "core/colorf.h" +#include "core/utf8reader.h" +#include "core/resourcedata.h" +#include "core/image.h" +#include "window/window.h" +#include "schrift/schrift.h" +#include +#include +#include +#include + +class CanvasTexture +{ +public: + int Width = 0; + int Height = 0; + std::vector Data; +}; + +class CanvasGlyph +{ +public: + SFT_Glyph id; + SFT_GMetrics metrics; + + double u = 0.0; + double v = 0.0; + double uvwidth = 0.0f; + double uvheight = 0.0f; + std::shared_ptr texture; +}; + +class CanvasFont +{ +public: + CanvasFont(const std::string& fontname, double height, std::vector& _data) : fontname(fontname), height(height) + { + data = std::move(_data); + loadFont(data.data(), data.size()); + + try + { + if (sft_lmetrics(&sft, &textmetrics) < 0) + throw std::runtime_error("Could not get truetype font metrics"); + } + catch (...) + { + sft_freefont(sft.font); + throw; + } + } + + ~CanvasFont() + { + sft_freefont(sft.font); + sft.font = nullptr; + } + + CanvasGlyph* getGlyph(uint32_t utfchar) + { + auto& glyph = glyphs[utfchar]; + if (glyph) + return glyph.get(); + + glyph = std::make_unique(); + + if (sft_lookup(&sft, utfchar, &glyph->id) < 0) + return glyph.get(); + + if (sft_gmetrics(&sft, glyph->id, &glyph->metrics) < 0) + return glyph.get(); + + glyph->metrics.advanceWidth /= 3.0; + glyph->metrics.leftSideBearing /= 3.0; + + if (glyph->metrics.minWidth <= 0 || glyph->metrics.minHeight <= 0) + return glyph.get(); + + int w = (glyph->metrics.minWidth + 3) & ~3; + int h = glyph->metrics.minHeight; + + int destwidth = (w + 2) / 3; + + auto texture = std::make_shared(); + texture->Width = destwidth; + texture->Height = h; + texture->Data.resize(destwidth * h); + uint32_t* dest = (uint32_t*)texture->Data.data(); + + std::unique_ptr grayscalebuffer(new uint8_t[w * h]); + uint8_t* grayscale = grayscalebuffer.get(); + + SFT_Image img = {}; + img.width = w; + img.height = h; + img.pixels = grayscale; + if (sft_render(&sft, glyph->id, img) < 0) + return glyph.get(); + + for (int y = 0; y < h; y++) + { + uint8_t* sline = grayscale + y * w; + uint32_t* dline = dest + y * destwidth; + for (int x = 0; x < w; x += 3) + { + uint32_t values[5] = + { + x > 0 ? sline[x - 1] : 0U, + sline[x], + x + 1 < w ? sline[x + 1] : 0U, + x + 2 < w ? sline[x + 2] : 0U, + x + 3 < w ? sline[x + 3] : 0U + }; + + uint32_t red = (values[0] + values[1] + values[1] + values[2] + 2) >> 2; + uint32_t green = (values[1] + values[2] + values[2] + values[3] + 2) >> 2; + uint32_t blue = (values[2] + values[3] + values[3] + values[4] + 2) >> 2; + uint32_t alpha = (red | green | blue) ? 255 : 0; + + *(dline++) = (alpha << 24) | (red << 16) | (green << 8) | blue; + } + } + + glyph->u = 0.0; + glyph->v = 0.0; + glyph->uvwidth = destwidth; + glyph->uvheight = h; + glyph->texture = std::move(texture); + + return glyph.get(); + } + + std::string fontname; + double height = 0.0; + + SFT_LMetrics textmetrics = {}; + std::unordered_map> glyphs; + +private: + void loadFont(const void* data, size_t size) + { + sft.xScale = height * 3; + sft.yScale = height; + sft.flags = SFT_DOWNWARD_Y; + sft.font = sft_loadmem(data, size); + } + + SFT sft = {}; + std::vector data; +}; + +class CanvasFontGroup +{ +public: + struct SingleFont + { + std::unique_ptr font; + std::vector> ranges; + int language; // mainly useful if we start supporting Chinese so that we can use another font there than for Japanese. + }; + CanvasFontGroup(const std::string& fontname, double height) : height(height) + { + auto fontdata = LoadWidgetFontData(fontname); + fonts.resize(fontdata.size()); + for (size_t i = 0; i < fonts.size(); i++) + { + fonts[i].font = std::make_unique(fontname, height, fontdata[i].fontdata); + fonts[i].ranges = std::move(fontdata[i].ranges); + fonts[i].language = fontdata[i].language; + } + } + + CanvasGlyph* getGlyph(uint32_t utfchar) + { + if (utfchar >= 0x530 && utfchar < 0x590) + { + int a = 0; + } + for (auto& fd : fonts) + { + bool get = false; + if (fd.ranges.size() == 0) get = true; + else for (auto r : fd.ranges) + { + if (utfchar >= r.first && utfchar <= r.second) + { + get = true; + break; + } + } + if (get) + { + auto g = fd.font->getGlyph(utfchar); + if (g) return g; + } + } + + return nullptr; + } + + SFT_LMetrics& GetTextMetrics() + { + return fonts[0].font->textmetrics; + } + + double height; + std::vector fonts; + +}; + +class BitmapCanvas : public Canvas +{ +public: + BitmapCanvas(DisplayWindow* window); + ~BitmapCanvas(); + + void begin(const Colorf& color) override; + void end() override; + + void begin3d() override; + void end3d() override; + + Point getOrigin() override; + void setOrigin(const Point& origin) override; + + void pushClip(const Rect& box) override; + void popClip() override; + + void fillRect(const Rect& box, const Colorf& color) override; + void line(const Point& p0, const Point& p1, const Colorf& color) override; + + void drawText(const Point& pos, const Colorf& color, const std::string& text) override; + Rect measureText(const std::string& text) override; + VerticalTextPosition verticalTextAlign() override; + + void drawText(const std::shared_ptr& font, const Point& pos, const std::string& text, const Colorf& color) override { drawText(pos, color, text); } + void drawTextEllipsis(const std::shared_ptr& font, const Point& pos, const Rect& clipBox, const std::string& text, const Colorf& color) override { drawText(pos, color, text); } + Rect measureText(const std::shared_ptr& font, const std::string& text) override { return measureText(text); } + FontMetrics getFontMetrics(const std::shared_ptr& font) override + { + VerticalTextPosition vtp = verticalTextAlign(); + FontMetrics metrics; + metrics.external_leading = vtp.top; + metrics.ascent = vtp.baseline - vtp.top; + metrics.descent = vtp.bottom - vtp.baseline; + metrics.height = metrics.ascent + metrics.descent; + return metrics; + } + int getCharacterIndex(const std::shared_ptr& font, const std::string& text, const Point& hitPoint) override { return 0; } + + void drawImage(const std::shared_ptr& image, const Point& pos) override; + + void drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color); + + void drawTile(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color); + void drawGlyph(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color); + + int getClipMinX() const; + int getClipMinY() const; + int getClipMaxX() const; + int getClipMaxY() const; + + std::unique_ptr createTexture(int width, int height, const void* pixels, ImageFormat format = ImageFormat::B8G8R8A8); + + template + static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } + + DisplayWindow* window = nullptr; + + std::unique_ptr font; + std::unique_ptr whiteTexture; + + Point origin; + double uiscale = 1.0f; + std::vector clipStack; + + int width = 0; + int height = 0; + std::vector pixels; + + std::unordered_map, std::unique_ptr> imageTextures; +}; + +BitmapCanvas::BitmapCanvas(DisplayWindow* window) : window(window) +{ + uiscale = window->GetDpiScale(); + uint32_t white = 0xffffffff; + whiteTexture = createTexture(1, 1, &white); + font = std::make_unique("NotoSans", 16.0 * uiscale); +} + +BitmapCanvas::~BitmapCanvas() +{ +} + +Point BitmapCanvas::getOrigin() +{ + return origin; +} + +void BitmapCanvas::setOrigin(const Point& newOrigin) +{ + origin = newOrigin; +} + +void BitmapCanvas::pushClip(const Rect& box) +{ + if (!clipStack.empty()) + { + const Rect& clip = clipStack.back(); + + double x0 = box.x + origin.x; + double y0 = box.y + origin.y; + double x1 = x0 + box.width; + double y1 = y0 + box.height; + + x0 = std::max(x0, clip.x); + y0 = std::max(y0, clip.y); + x1 = std::min(x1, clip.x + clip.width); + y1 = std::min(y1, clip.y + clip.height); + + if (x0 < x1 && y0 < y1) + clipStack.push_back(Rect::ltrb(x0, y0, x1, y1)); + else + clipStack.push_back(Rect::xywh(0.0, 0.0, 0.0, 0.0)); + } + else + { + clipStack.push_back(box); + } +} + +void BitmapCanvas::popClip() +{ + clipStack.pop_back(); +} + +void BitmapCanvas::fillRect(const Rect& box, const Colorf& color) +{ + drawTile(whiteTexture.get(), (float)((origin.x + box.x) * uiscale), (float)((origin.y + box.y) * uiscale), (float)(box.width * uiscale), (float)(box.height * uiscale), 0.0, 0.0, 1.0, 1.0, color); +} + +void BitmapCanvas::drawImage(const std::shared_ptr& image, const Point& pos) +{ + auto& texture = imageTextures[image]; + if (!texture) + { + texture = createTexture(image->GetWidth(), image->GetHeight(), image->GetData(), image->GetFormat()); + } + Colorf color(1.0f, 1.0f, 1.0f); + drawTile(texture.get(), (float)((origin.x + pos.x) * uiscale), (float)((origin.y + pos.y) * uiscale), (float)(texture->Width * uiscale), (float)(texture->Height * uiscale), 0.0, 0.0, (float)texture->Width, (float)texture->Height, color); +} + +void BitmapCanvas::line(const Point& p0, const Point& p1, const Colorf& color) +{ + double x0 = origin.x + p0.x; + double y0 = origin.y + p0.y; + double x1 = origin.x + p1.x; + double y1 = origin.y + p1.y; + + if (clipStack.empty())// || (clipStack.back().contains({ x0, y0 }) && clipStack.back().contains({ x1, y1 }))) + { + drawLineUnclipped({ x0, y0 }, { x1, y1 }, color); + } + else + { + const Rect& clip = clipStack.back(); + + if (x0 > x1) + { + std::swap(x0, x1); + std::swap(y0, y1); + } + + if (x1 < clip.x || x0 >= clip.x + clip.width) + return; + + // Clip left edge + if (x0 < clip.x) + { + double dx = x1 - x0; + double dy = y1 - y0; + if (std::abs(dx) < 0.0001) + return; + y0 = y0 + (clip.x - x0) * dy / dx; + x0 = clip.x; + } + + // Clip right edge + if (x1 > clip.x + clip.width) + { + double dx = x1 - x0; + double dy = y1 - y0; + if (std::abs(dx) < 0.0001) + return; + y1 = y1 + (clip.x + clip.width - x1) * dy / dx; + x1 = clip.x + clip.width; + } + + if (y0 > y1) + { + std::swap(x0, x1); + std::swap(y0, y1); + } + + if (y1 < clip.y || y0 >= clip.y + clip.height) + return; + + // Clip top edge + if (y0 < clip.y) + { + double dx = x1 - x0; + double dy = y1 - y0; + if (std::abs(dy) < 0.0001) + return; + x0 = x0 + (clip.y - y0) * dx / dy; + y0 = clip.y; + } + + // Clip bottom edge + if (y1 > clip.y + clip.height) + { + double dx = x1 - x0; + double dy = y1 - y0; + if (std::abs(dy) < 0.0001) + return; + x1 = x1 + (clip.y + clip.height - y1) * dx / dy; + y1 = clip.y + clip.height; + } + + x0 = clamp(x0, clip.x, clip.x + clip.width); + x1 = clamp(x1, clip.x, clip.x + clip.width); + y0 = clamp(y0, clip.y, clip.y + clip.height); + y1 = clamp(y1, clip.y, clip.y + clip.height); + + if (x0 != x1 || y0 != y1) + drawLineUnclipped({ x0, y0 }, { x1, y1 }, color); + } +} + +void BitmapCanvas::drawText(const Point& pos, const Colorf& color, const std::string& text) +{ + double x = std::round((origin.x + pos.x) * uiscale); + double y = std::round((origin.y + pos.y) * uiscale); + + UTF8Reader reader(text.data(), text.size()); + while (!reader.is_end()) + { + CanvasGlyph* glyph = font->getGlyph(reader.character()); + if (!glyph->texture) + { + glyph = font->getGlyph(32); + } + + if (glyph->texture) + { + double gx = std::round(x + glyph->metrics.leftSideBearing); + double gy = std::round(y + glyph->metrics.yOffset); + drawGlyph(glyph->texture.get(), (float)std::round(gx), (float)std::round(gy), (float)glyph->uvwidth, (float)glyph->uvheight, (float)glyph->u, (float)glyph->v, (float)glyph->uvwidth, (float)glyph->uvheight, color); + } + + x += std::round(glyph->metrics.advanceWidth); + reader.next(); + } +} + +Rect BitmapCanvas::measureText(const std::string& text) +{ + double x = 0.0; + double y = font->GetTextMetrics().ascender - font->GetTextMetrics().descender; + + UTF8Reader reader(text.data(), text.size()); + while (!reader.is_end()) + { + CanvasGlyph* glyph = font->getGlyph(reader.character()); + if (!glyph->texture) + { + glyph = font->getGlyph(32); + } + + x += std::round(glyph->metrics.advanceWidth); + reader.next(); + } + + return Rect::xywh(0.0, 0.0, x / uiscale, y / uiscale); +} + +VerticalTextPosition BitmapCanvas::verticalTextAlign() +{ + VerticalTextPosition align; + align.top = 0.0f; + auto tm = font->GetTextMetrics(); + align.baseline = tm.ascender / uiscale; + align.bottom = (tm.ascender - tm.descender) / uiscale; + return align; +} + +std::unique_ptr BitmapCanvas::createTexture(int width, int height, const void* pixels, ImageFormat format) +{ + auto texture = std::make_unique(); + texture->Width = width; + texture->Height = height; + texture->Data.resize(width * height); + if (format == ImageFormat::B8G8R8A8) + { + memcpy(texture->Data.data(), pixels, width * height * sizeof(uint32_t)); + } + else + { + const uint32_t* src = (const uint32_t*)pixels; + uint32_t* dest = texture->Data.data(); + int count = width * height; + for (int i = 0; i < count; i++) + { + uint32_t a = (src[i] >> 24) & 0xff; + uint32_t b = (src[i] >> 16) & 0xff; + uint32_t g = (src[i] >> 8) & 0xff; + uint32_t r = src[i] & 0xff; + dest[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + } + return texture; +} + +void BitmapCanvas::drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color) +{ + if (p0.x == p1.x) + { + drawTile(whiteTexture.get(), (float)((p0.x - 0.5) * uiscale), (float)(p0.y * uiscale), (float)((p1.x + 0.5) * uiscale), (float)(p1.y * uiscale), 0.0f, 0.0f, 1.0f, 1.0f, color); + } + else if (p0.y == p1.y) + { + drawTile(whiteTexture.get(), (float)(p0.x * uiscale), (float)((p0.y - 0.5) * uiscale), (float)(p1.x * uiscale), (float)((p1.y + 0.5) * uiscale), 0.0f, 0.0f, 1.0f, 1.0f, color); + } + else + { + // To do: draw line using bresenham + } +} + +int BitmapCanvas::getClipMinX() const +{ + return clipStack.empty() ? 0 : (int)std::max(clipStack.back().x * uiscale, 0.0); +} + +int BitmapCanvas::getClipMinY() const +{ + return clipStack.empty() ? 0 : (int)std::max(clipStack.back().y * uiscale, 0.0); +} + +int BitmapCanvas::getClipMaxX() const +{ + return clipStack.empty() ? width : (int)std::min((clipStack.back().x + clipStack.back().width) * uiscale, (double)width); +} + +int BitmapCanvas::getClipMaxY() const +{ + return clipStack.empty() ? height : (int)std::min((clipStack.back().y + clipStack.back().height) * uiscale, (double)height); +} + +void BitmapCanvas::drawTile(CanvasTexture* texture, float left, float top, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) +{ + if (width <= 0.0f || height <= 0.0f) + return; + + int swidth = texture->Width; + int sheight = texture->Height; + const uint32_t* src = texture->Data.data(); + + int dwidth = this->width; + int dheight = this->height; + uint32_t* dest = this->pixels.data(); + + int x0 = (int)left; + int x1 = (int)(left + width); + int y0 = (int)top; + int y1 = (int)(top + height); + + x0 = std::max(x0, getClipMinX()); + y0 = std::max(y0, getClipMinY()); + x1 = std::min(x1, getClipMaxX()); + y1 = std::min(y1, getClipMaxY()); + if (x1 <= x0 || y1 <= y0) + return; + + uint32_t cred = (int32_t)clamp(color.r * 256.0f, 0.0f, 256.0f); + uint32_t cgreen = (int32_t)clamp(color.g * 256.0f, 0.0f, 256.0f); + uint32_t cblue = (int32_t)clamp(color.b * 256.0f, 0.0f, 256.0f); + uint32_t calpha = (int32_t)clamp(color.a * 256.0f, 0.0f, 256.0f); + + float uscale = uvwidth / width; + float vscale = uvheight / height; + + for (int y = y0; y < y1; y++) + { + float vpix = v + vscale * (y + 0.5f - top); + const uint32_t* sline = src + ((int)vpix) * swidth; + + uint32_t* dline = dest + y * dwidth; + for (int x = x0; x < x1; x++) + { + float upix = u + uscale * (x + 0.5f - left); + uint32_t spixel = sline[(int)upix]; + uint32_t salpha = spixel >> 24; + uint32_t sred = (spixel >> 16) & 0xff; + uint32_t sgreen = (spixel >> 8) & 0xff; + uint32_t sblue = spixel & 0xff; + + uint32_t dpixel = dline[x]; + uint32_t dalpha = dpixel >> 24; + uint32_t dred = (dpixel >> 16) & 0xff; + uint32_t dgreen = (dpixel >> 8) & 0xff; + uint32_t dblue = dpixel & 0xff; + + // Pixel shade + sred = (cred * sred + 127) >> 8; + sgreen = (cgreen * sgreen + 127) >> 8; + sblue = (cblue * sblue + 127) >> 8; + salpha = (calpha * salpha + 127) >> 8; + + // Rescale from [0,255] to [0,256] + uint32_t sa = salpha + (salpha >> 7); + uint32_t sinva = 256 - sa; + + // dest.rgba = color.rgba * src.rgba * src.a + dest.rgba * (1-src.a) + uint32_t a = (salpha * sa + dalpha * sinva + 127) >> 8; + uint32_t r = (sred * sa + dred * sinva + 127) >> 8; + uint32_t g = (sgreen * sa + dgreen * sinva + 127) >> 8; + uint32_t b = (sblue * sa + dblue * sinva + 127) >> 8; + dline[x] = (a << 24) | (r << 16) | (g << 8) | b; + } + } +} + +void BitmapCanvas::drawGlyph(CanvasTexture* texture, float left, float top, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) +{ + if (width <= 0.0f || height <= 0.0f) + return; + + int swidth = texture->Width; + int sheight = texture->Height; + const uint32_t* src = texture->Data.data(); + + int dwidth = this->width; + int dheight = this->height; + uint32_t* dest = this->pixels.data(); + + int x0 = (int)left; + int x1 = (int)(left + width); + int y0 = (int)top; + int y1 = (int)(top + height); + + x0 = std::max(x0, getClipMinX()); + y0 = std::max(y0, getClipMinY()); + x1 = std::min(x1, getClipMaxX()); + y1 = std::min(y1, getClipMaxY()); + if (x1 <= x0 || y1 <= y0) + return; + + uint32_t cred = (int32_t)clamp(color.r * 255.0f, 0.0f, 255.0f); + uint32_t cgreen = (int32_t)clamp(color.g * 255.0f, 0.0f, 255.0f); + uint32_t cblue = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); + + float uscale = uvwidth / width; + float vscale = uvheight / height; + + for (int y = y0; y < y1; y++) + { + float vpix = v + vscale * (y + 0.5f - top); + const uint32_t* sline = src + ((int)vpix) * swidth; + + uint32_t* dline = dest + y * dwidth; + for (int x = x0; x < x1; x++) + { + float upix = u + uscale * (x + 0.5f - left); + uint32_t spixel = sline[(int)upix]; + uint32_t sred = (spixel >> 16) & 0xff; + uint32_t sgreen = (spixel >> 8) & 0xff; + uint32_t sblue = spixel & 0xff; + + uint32_t dpixel = dline[x]; + uint32_t dred = (dpixel >> 16) & 0xff; + uint32_t dgreen = (dpixel >> 8) & 0xff; + uint32_t dblue = dpixel & 0xff; + + // Rescale from [0,255] to [0,256] + sred += sred >> 7; + sgreen += sgreen >> 7; + sblue += sblue >> 7; + + // dest.rgb = color.rgb * src.rgb + dest.rgb * (1-src.rgb) + uint32_t r = (cred * sred + dred * (256 - sred) + 127) >> 8; + uint32_t g = (cgreen * sgreen + dgreen * (256 - sgreen) + 127) >> 8; + uint32_t b = (cblue * sblue + dblue * (256 - sblue) + 127) >> 8; + dline[x] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } +} + +void BitmapCanvas::begin(const Colorf& color) +{ + uiscale = window->GetDpiScale(); + + uint32_t r = (int32_t)clamp(color.r * 255.0f, 0.0f, 255.0f); + uint32_t g = (int32_t)clamp(color.g * 255.0f, 0.0f, 255.0f); + uint32_t b = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); + uint32_t a = (int32_t)clamp(color.a * 255.0f, 0.0f, 255.0f); + uint32_t bgcolor = (a << 24) | (r << 16) | (g << 8) | b; + width = window->GetPixelWidth(); + height = window->GetPixelHeight(); + pixels.clear(); + pixels.resize(width * height, bgcolor); +} + +void BitmapCanvas::end() +{ + window->PresentBitmap(width, height, pixels.data()); +} + +void BitmapCanvas::begin3d() +{ +} + +void BitmapCanvas::end3d() +{ +} + +///////////////////////////////////////////////////////////////////////////// + +std::unique_ptr Canvas::create(DisplayWindow* window) +{ + return std::make_unique(window); +} diff --git a/libraries/ZWidget/src/core/font.cpp b/libraries/ZWidget/src/core/font.cpp new file mode 100644 index 0000000000..f646912a9c --- /dev/null +++ b/libraries/ZWidget/src/core/font.cpp @@ -0,0 +1,28 @@ + +#include "core/font.h" + +class FontImpl : public Font +{ +public: + FontImpl(const std::string& name, double height) : Name(name), Height(height) + { + } + + const std::string& GetName() const override + { + return Name; + } + + double GetHeight() const override + { + return Height; + } + + std::string Name; + double Height = 0.0; +}; + +std::shared_ptr Font::Create(const std::string& name, double height) +{ + return std::make_shared(name, height); +} diff --git a/libraries/ZWidget/src/core/image.cpp b/libraries/ZWidget/src/core/image.cpp new file mode 100644 index 0000000000..91e201a47f --- /dev/null +++ b/libraries/ZWidget/src/core/image.cpp @@ -0,0 +1,110 @@ + +#include "core/image.h" +#include "core/resourcedata.h" +#include "picopng/picopng.h" +#include "nanosvg/nanosvg.h" +#include "nanosvg/nanosvgrast.h" +#include +#include + +class ImageImpl : public Image +{ +public: + ImageImpl(int width, int height, ImageFormat format, const void* data) : Width(width), Height(height), Format(format) + { + Data = std::make_unique(width * height); + if (data) + memcpy(Data.get(), data, width * height * sizeof(uint32_t)); + } + + int GetWidth() const override + { + return Width; + } + + int GetHeight() const override + { + return Height; + } + + ImageFormat GetFormat() const override + { + return Format; + } + + void* GetData() const override + { + return Data.get(); + } + + int Width = 0; + int Height = 0; + ImageFormat Format = {}; + std::unique_ptr Data; +}; + +std::shared_ptr Image::Create(int width, int height, ImageFormat format, const void* data) +{ + return std::make_shared(width, height, format, data); +} + +std::shared_ptr Image::LoadResource(const std::string& resourcename, double dpiscale) +{ + size_t extensionpos = resourcename.find_last_of("./\\"); + if (extensionpos == std::string::npos || resourcename[extensionpos] != '.') + throw std::runtime_error("Unsupported image format"); + std::string extension = resourcename.substr(extensionpos + 1); + for (char& c : extension) + { + if (c >= 'A' && c <= 'Z') + c = c - 'A' + 'a'; + } + + if (extension == "png") + { + auto filedata = LoadWidgetImageData(resourcename); + + std::vector pixels; + unsigned long width = 0, height = 0; + int result = decodePNG(pixels, width, height, (const unsigned char*)filedata.data(), filedata.size(), true); + if (result != 0) + throw std::runtime_error("Could not decode PNG file"); + + return Image::Create(width, height, ImageFormat::R8G8B8A8, pixels.data()); + } + else if (extension == "svg") + { + auto filedata = LoadWidgetImageData(resourcename); + filedata.push_back(0); + + NSVGimage* svgimage = nsvgParse((char*)filedata.data(), "px", (float)(96.0 * dpiscale)); + if (!svgimage) + throw std::runtime_error("Could not parse SVG file"); + + try + { + int width = (int)(svgimage->width * dpiscale); + int height = (int)(svgimage->height * dpiscale); + std::shared_ptr image = Image::Create(width, height, ImageFormat::R8G8B8A8, nullptr); + + NSVGrasterizer* rast = nsvgCreateRasterizer(); + if (!rast) + throw std::runtime_error("Could not create SVG rasterizer"); + + nsvgRasterize(rast, svgimage, 0.0f, 0.0f, (float)dpiscale, (unsigned char*)image->GetData(), width, height, width * 4); + + nsvgDeleteRasterizer(rast); + nsvgDelete(svgimage); + return image; + } + catch (...) + { + nsvgDelete(svgimage); + throw; + } + } + else + { + throw std::runtime_error("Unsupported image format"); + } +} diff --git a/libraries/ZWidget/src/core/nanosvg/nanosvg.cpp b/libraries/ZWidget/src/core/nanosvg/nanosvg.cpp new file mode 100644 index 0000000000..764b854b07 --- /dev/null +++ b/libraries/ZWidget/src/core/nanosvg/nanosvg.cpp @@ -0,0 +1,6 @@ + +#define NANOSVG_IMPLEMENTATION +#include "nanosvg.h" + +#define NANOSVGRAST_IMPLEMENTATION +#include "nanosvgrast.h" diff --git a/libraries/ZWidget/src/core/nanosvg/nanosvg.h b/libraries/ZWidget/src/core/nanosvg/nanosvg.h new file mode 100644 index 0000000000..60a323820c --- /dev/null +++ b/libraries/ZWidget/src/core/nanosvg/nanosvg.h @@ -0,0 +1,3098 @@ +/* + * Copyright (c) 2013-14 Mikko Mononen memon@inside.org + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * The SVG parser is based on Anti-Grain Geometry 2.4 SVG example + * Copyright (C) 2002-2004 Maxim Shemanarev (McSeem) (http://www.antigrain.com/) + * + * Arc calculation code based on canvg (https://code.google.com/p/canvg/) + * + * Bounding box calculation based on http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + * + */ + +#ifndef NANOSVG_H +#define NANOSVG_H + +#ifndef NANOSVG_CPLUSPLUS +#ifdef __cplusplus +extern "C" { +#endif +#endif + +// NanoSVG is a simple stupid single-header-file SVG parse. The output of the parser is a list of cubic bezier shapes. +// +// The library suits well for anything from rendering scalable icons in your editor application to prototyping a game. +// +// NanoSVG supports a wide range of SVG features, but something may be missing, feel free to create a pull request! +// +// The shapes in the SVG images are transformed by the viewBox and converted to specified units. +// That is, you should get the same looking data as your designed in your favorite app. +// +// NanoSVG can return the paths in few different units. For example if you want to render an image, you may choose +// to get the paths in pixels, or if you are feeding the data into a CNC-cutter, you may want to use millimeters. +// +// The units passed to NanoSVG should be one of: 'px', 'pt', 'pc' 'mm', 'cm', or 'in'. +// DPI (dots-per-inch) controls how the unit conversion is done. +// +// If you don't know or care about the units stuff, "px" and 96 should get you going. + + +/* Example Usage: + // Load SVG + NSVGimage* image; + image = nsvgParseFromFile("test.svg", "px", 96); + printf("size: %f x %f\n", image->width, image->height); + // Use... + for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { + for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { + for (int i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + drawCubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7]); + } + } + } + // Delete + nsvgDelete(image); +*/ + +enum NSVGpaintType { + NSVG_PAINT_UNDEF = -1, + NSVG_PAINT_NONE = 0, + NSVG_PAINT_COLOR = 1, + NSVG_PAINT_LINEAR_GRADIENT = 2, + NSVG_PAINT_RADIAL_GRADIENT = 3 +}; + +enum NSVGspreadType { + NSVG_SPREAD_PAD = 0, + NSVG_SPREAD_REFLECT = 1, + NSVG_SPREAD_REPEAT = 2 +}; + +enum NSVGlineJoin { + NSVG_JOIN_MITER = 0, + NSVG_JOIN_ROUND = 1, + NSVG_JOIN_BEVEL = 2 +}; + +enum NSVGlineCap { + NSVG_CAP_BUTT = 0, + NSVG_CAP_ROUND = 1, + NSVG_CAP_SQUARE = 2 +}; + +enum NSVGfillRule { + NSVG_FILLRULE_NONZERO = 0, + NSVG_FILLRULE_EVENODD = 1 +}; + +enum NSVGflags { + NSVG_FLAGS_VISIBLE = 0x01 +}; + +typedef struct NSVGgradientStop { + unsigned int color; + float offset; +} NSVGgradientStop; + +typedef struct NSVGgradient { + float xform[6]; + char spread; + float fx, fy; + int nstops; + NSVGgradientStop stops[1]; +} NSVGgradient; + +typedef struct NSVGpaint { + signed char type; + union { + unsigned int color; + NSVGgradient* gradient; + }; +} NSVGpaint; + +typedef struct NSVGpath +{ + float* pts; // Cubic bezier points: x0,y0, [cpx1,cpx1,cpx2,cpy2,x1,y1], ... + int npts; // Total number of bezier points. + char closed; // Flag indicating if shapes should be treated as closed. + float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + struct NSVGpath* next; // Pointer to next path, or NULL if last element. +} NSVGpath; + +typedef struct NSVGshape +{ + char id[64]; // Optional 'id' attr of the shape or its group + NSVGpaint fill; // Fill paint + NSVGpaint stroke; // Stroke paint + float opacity; // Opacity of the shape. + float strokeWidth; // Stroke width (scaled). + float strokeDashOffset; // Stroke dash offset (scaled). + float strokeDashArray[8]; // Stroke dash array (scaled). + char strokeDashCount; // Number of dash values in dash array. + char strokeLineJoin; // Stroke join type. + char strokeLineCap; // Stroke cap type. + float miterLimit; // Miter limit + char fillRule; // Fill rule, see NSVGfillRule. + unsigned char flags; // Logical or of NSVG_FLAGS_* flags + float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + char fillGradient[64]; // Optional 'id' of fill gradient + char strokeGradient[64]; // Optional 'id' of stroke gradient + float xform[6]; // Root transformation for fill/stroke gradient + NSVGpath* paths; // Linked list of paths in the image. + struct NSVGshape* next; // Pointer to next shape, or NULL if last element. +} NSVGshape; + +typedef struct NSVGimage +{ + float width; // Width of the image. + float height; // Height of the image. + NSVGshape* shapes; // Linked list of shapes in the image. +} NSVGimage; + +// Parses SVG file from a file, returns SVG image as paths. +NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi); + +// Parses SVG file from a null terminated string, returns SVG image as paths. +// Important note: changes the string. +NSVGimage* nsvgParse(char* input, const char* units, float dpi); + +// Duplicates a path. +NSVGpath* nsvgDuplicatePath(NSVGpath* p); + +// Deletes an image. +void nsvgDelete(NSVGimage* image); + +#ifndef NANOSVG_CPLUSPLUS +#ifdef __cplusplus +} +#endif +#endif + +#ifdef NANOSVG_IMPLEMENTATION + +#include +#include +#include +#include + +#define NSVG_PI (3.14159265358979323846264338327f) +#define NSVG_KAPPA90 (0.5522847493f) // Length proportional to radius of a cubic bezier handle for 90deg arcs. + +#define NSVG_ALIGN_MIN 0 +#define NSVG_ALIGN_MID 1 +#define NSVG_ALIGN_MAX 2 +#define NSVG_ALIGN_NONE 0 +#define NSVG_ALIGN_MEET 1 +#define NSVG_ALIGN_SLICE 2 + +#define NSVG_NOTUSED(v) do { (void)(1 ? (void)0 : ( (void)(v) ) ); } while(0) +#define NSVG_RGB(r, g, b) (((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16)) + +#ifdef _MSC_VER + #pragma warning (disable: 4996) // Switch off security warnings + #pragma warning (disable: 4100) // Switch off unreferenced formal parameter warnings + #ifdef __cplusplus + #define NSVG_INLINE inline + #else + #define NSVG_INLINE + #endif +#else + #define NSVG_INLINE inline +#endif + + +static int nsvg__isspace(char c) +{ + return strchr(" \t\n\v\f\r", c) != 0; +} + +static int nsvg__isdigit(char c) +{ + return c >= '0' && c <= '9'; +} + +static NSVG_INLINE float nsvg__minf(float a, float b) { return a < b ? a : b; } +static NSVG_INLINE float nsvg__maxf(float a, float b) { return a > b ? a : b; } + + +// Simple XML parser + +#define NSVG_XML_TAG 1 +#define NSVG_XML_CONTENT 2 +#define NSVG_XML_MAX_ATTRIBS 256 + +static void nsvg__parseContent(char* s, + void (*contentCb)(void* ud, const char* s), + void* ud) +{ + // Trim start white spaces + while (*s && nsvg__isspace(*s)) s++; + if (!*s) return; + + if (contentCb) + (*contentCb)(ud, s); +} + +static void nsvg__parseElement(char* s, + void (*startelCb)(void* ud, const char* el, const char** attr), + void (*endelCb)(void* ud, const char* el), + void* ud) +{ + const char* attr[NSVG_XML_MAX_ATTRIBS]; + int nattr = 0; + char* name; + int start = 0; + int end = 0; + char quote; + + // Skip white space after the '<' + while (*s && nsvg__isspace(*s)) s++; + + // Check if the tag is end tag + if (*s == '/') { + s++; + end = 1; + } else { + start = 1; + } + + // Skip comments, data and preprocessor stuff. + if (!*s || *s == '?' || *s == '!') + return; + + // Get tag name + name = s; + while (*s && !nsvg__isspace(*s)) s++; + if (*s) { *s++ = '\0'; } + + // Get attribs + while (!end && *s && nattr < NSVG_XML_MAX_ATTRIBS-3) { + char* name = NULL; + char* value = NULL; + + // Skip white space before the attrib name + while (*s && nsvg__isspace(*s)) s++; + if (!*s) break; + if (*s == '/') { + end = 1; + break; + } + name = s; + // Find end of the attrib name. + while (*s && !nsvg__isspace(*s) && *s != '=') s++; + if (*s) { *s++ = '\0'; } + // Skip until the beginning of the value. + while (*s && *s != '\"' && *s != '\'') s++; + if (!*s) break; + quote = *s; + s++; + // Store value and find the end of it. + value = s; + while (*s && *s != quote) s++; + if (*s) { *s++ = '\0'; } + + // Store only well formed attributes + if (name && value) { + attr[nattr++] = name; + attr[nattr++] = value; + } + } + + // List terminator + attr[nattr++] = 0; + attr[nattr++] = 0; + + // Call callbacks. + if (start && startelCb) + (*startelCb)(ud, name, attr); + if (end && endelCb) + (*endelCb)(ud, name); +} + +int nsvg__parseXML(char* input, + void (*startelCb)(void* ud, const char* el, const char** attr), + void (*endelCb)(void* ud, const char* el), + void (*contentCb)(void* ud, const char* s), + void* ud) +{ + char* s = input; + char* mark = s; + int state = NSVG_XML_CONTENT; + while (*s) { + if (*s == '<' && state == NSVG_XML_CONTENT) { + // Start of a tag + *s++ = '\0'; + nsvg__parseContent(mark, contentCb, ud); + mark = s; + state = NSVG_XML_TAG; + } else if (*s == '>' && state == NSVG_XML_TAG) { + // Start of a content or new tag. + *s++ = '\0'; + nsvg__parseElement(mark, startelCb, endelCb, ud); + mark = s; + state = NSVG_XML_CONTENT; + } else { + s++; + } + } + + return 1; +} + + +/* Simple SVG parser. */ + +#define NSVG_MAX_ATTR 128 + +enum NSVGgradientUnits { + NSVG_USER_SPACE = 0, + NSVG_OBJECT_SPACE = 1 +}; + +#define NSVG_MAX_DASHES 8 + +enum NSVGunits { + NSVG_UNITS_USER, + NSVG_UNITS_PX, + NSVG_UNITS_PT, + NSVG_UNITS_PC, + NSVG_UNITS_MM, + NSVG_UNITS_CM, + NSVG_UNITS_IN, + NSVG_UNITS_PERCENT, + NSVG_UNITS_EM, + NSVG_UNITS_EX +}; + +typedef struct NSVGcoordinate { + float value; + int units; +} NSVGcoordinate; + +typedef struct NSVGlinearData { + NSVGcoordinate x1, y1, x2, y2; +} NSVGlinearData; + +typedef struct NSVGradialData { + NSVGcoordinate cx, cy, r, fx, fy; +} NSVGradialData; + +typedef struct NSVGgradientData +{ + char id[64]; + char ref[64]; + signed char type; + union { + NSVGlinearData linear; + NSVGradialData radial; + }; + char spread; + char units; + float xform[6]; + int nstops; + NSVGgradientStop* stops; + struct NSVGgradientData* next; +} NSVGgradientData; + +typedef struct NSVGattrib +{ + char id[64]; + float xform[6]; + unsigned int fillColor; + unsigned int strokeColor; + float opacity; + float fillOpacity; + float strokeOpacity; + char fillGradient[64]; + char strokeGradient[64]; + float strokeWidth; + float strokeDashOffset; + float strokeDashArray[NSVG_MAX_DASHES]; + int strokeDashCount; + char strokeLineJoin; + char strokeLineCap; + float miterLimit; + char fillRule; + float fontSize; + unsigned int stopColor; + float stopOpacity; + float stopOffset; + char hasFill; + char hasStroke; + char visible; +} NSVGattrib; + +typedef struct NSVGparser +{ + NSVGattrib attr[NSVG_MAX_ATTR]; + int attrHead; + float* pts; + int npts; + int cpts; + NSVGpath* plist; + NSVGimage* image; + NSVGgradientData* gradients; + NSVGshape* shapesTail; + float viewMinx, viewMiny, viewWidth, viewHeight; + int alignX, alignY, alignType; + float dpi; + char pathFlag; + char defsFlag; +} NSVGparser; + +static void nsvg__xformIdentity(float* t) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = 0.0f; t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetTranslation(float* t, float tx, float ty) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = 0.0f; t[3] = 1.0f; + t[4] = tx; t[5] = ty; +} + +static void nsvg__xformSetScale(float* t, float sx, float sy) +{ + t[0] = sx; t[1] = 0.0f; + t[2] = 0.0f; t[3] = sy; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetSkewX(float* t, float a) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = tanf(a); t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetSkewY(float* t, float a) +{ + t[0] = 1.0f; t[1] = tanf(a); + t[2] = 0.0f; t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetRotation(float* t, float a) +{ + float cs = cosf(a), sn = sinf(a); + t[0] = cs; t[1] = sn; + t[2] = -sn; t[3] = cs; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformMultiply(float* t, float* s) +{ + float t0 = t[0] * s[0] + t[1] * s[2]; + float t2 = t[2] * s[0] + t[3] * s[2]; + float t4 = t[4] * s[0] + t[5] * s[2] + s[4]; + t[1] = t[0] * s[1] + t[1] * s[3]; + t[3] = t[2] * s[1] + t[3] * s[3]; + t[5] = t[4] * s[1] + t[5] * s[3] + s[5]; + t[0] = t0; + t[2] = t2; + t[4] = t4; +} + +static void nsvg__xformInverse(float* inv, float* t) +{ + double invdet, det = (double)t[0] * t[3] - (double)t[2] * t[1]; + if (det > -1e-6 && det < 1e-6) { + nsvg__xformIdentity(t); + return; + } + invdet = 1.0 / det; + inv[0] = (float)(t[3] * invdet); + inv[2] = (float)(-t[2] * invdet); + inv[4] = (float)(((double)t[2] * t[5] - (double)t[3] * t[4]) * invdet); + inv[1] = (float)(-t[1] * invdet); + inv[3] = (float)(t[0] * invdet); + inv[5] = (float)(((double)t[1] * t[4] - (double)t[0] * t[5]) * invdet); +} + +static void nsvg__xformPremultiply(float* t, float* s) +{ + float s2[6]; + memcpy(s2, s, sizeof(float)*6); + nsvg__xformMultiply(s2, t); + memcpy(t, s2, sizeof(float)*6); +} + +static void nsvg__xformPoint(float* dx, float* dy, float x, float y, float* t) +{ + *dx = x*t[0] + y*t[2] + t[4]; + *dy = x*t[1] + y*t[3] + t[5]; +} + +static void nsvg__xformVec(float* dx, float* dy, float x, float y, float* t) +{ + *dx = x*t[0] + y*t[2]; + *dy = x*t[1] + y*t[3]; +} + +#define NSVG_EPSILON (1e-12) + +static int nsvg__ptInBounds(float* pt, float* bounds) +{ + return pt[0] >= bounds[0] && pt[0] <= bounds[2] && pt[1] >= bounds[1] && pt[1] <= bounds[3]; +} + + +static double nsvg__evalBezier(double t, double p0, double p1, double p2, double p3) +{ + double it = 1.0-t; + return it*it*it*p0 + 3.0*it*it*t*p1 + 3.0*it*t*t*p2 + t*t*t*p3; +} + +static void nsvg__curveBounds(float* bounds, float* curve) +{ + int i, j, count; + double roots[2], a, b, c, b2ac, t, v; + float* v0 = &curve[0]; + float* v1 = &curve[2]; + float* v2 = &curve[4]; + float* v3 = &curve[6]; + + // Start the bounding box by end points + bounds[0] = nsvg__minf(v0[0], v3[0]); + bounds[1] = nsvg__minf(v0[1], v3[1]); + bounds[2] = nsvg__maxf(v0[0], v3[0]); + bounds[3] = nsvg__maxf(v0[1], v3[1]); + + // Bezier curve fits inside the convex hull of it's control points. + // If control points are inside the bounds, we're done. + if (nsvg__ptInBounds(v1, bounds) && nsvg__ptInBounds(v2, bounds)) + return; + + // Add bezier curve inflection points in X and Y. + for (i = 0; i < 2; i++) { + a = -3.0 * v0[i] + 9.0 * v1[i] - 9.0 * v2[i] + 3.0 * v3[i]; + b = 6.0 * v0[i] - 12.0 * v1[i] + 6.0 * v2[i]; + c = 3.0 * v1[i] - 3.0 * v0[i]; + count = 0; + if (fabs(a) < NSVG_EPSILON) { + if (fabs(b) > NSVG_EPSILON) { + t = -c / b; + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + } + } else { + b2ac = b*b - 4.0*c*a; + if (b2ac > NSVG_EPSILON) { + t = (-b + sqrt(b2ac)) / (2.0 * a); + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + t = (-b - sqrt(b2ac)) / (2.0 * a); + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + } + } + for (j = 0; j < count; j++) { + v = nsvg__evalBezier(roots[j], v0[i], v1[i], v2[i], v3[i]); + bounds[0+i] = nsvg__minf(bounds[0+i], (float)v); + bounds[2+i] = nsvg__maxf(bounds[2+i], (float)v); + } + } +} + +static NSVGparser* nsvg__createParser(void) +{ + NSVGparser* p; + p = (NSVGparser*)malloc(sizeof(NSVGparser)); + if (p == NULL) goto error; + memset(p, 0, sizeof(NSVGparser)); + + p->image = (NSVGimage*)malloc(sizeof(NSVGimage)); + if (p->image == NULL) goto error; + memset(p->image, 0, sizeof(NSVGimage)); + + // Init style + nsvg__xformIdentity(p->attr[0].xform); + memset(p->attr[0].id, 0, sizeof p->attr[0].id); + p->attr[0].fillColor = NSVG_RGB(0,0,0); + p->attr[0].strokeColor = NSVG_RGB(0,0,0); + p->attr[0].opacity = 1; + p->attr[0].fillOpacity = 1; + p->attr[0].strokeOpacity = 1; + p->attr[0].stopOpacity = 1; + p->attr[0].strokeWidth = 1; + p->attr[0].strokeLineJoin = NSVG_JOIN_MITER; + p->attr[0].strokeLineCap = NSVG_CAP_BUTT; + p->attr[0].miterLimit = 4; + p->attr[0].fillRule = NSVG_FILLRULE_NONZERO; + p->attr[0].hasFill = 1; + p->attr[0].visible = 1; + + return p; + +error: + if (p) { + if (p->image) free(p->image); + free(p); + } + return NULL; +} + +static void nsvg__deletePaths(NSVGpath* path) +{ + while (path) { + NSVGpath *next = path->next; + if (path->pts != NULL) + free(path->pts); + free(path); + path = next; + } +} + +static void nsvg__deletePaint(NSVGpaint* paint) +{ + if (paint->type == NSVG_PAINT_LINEAR_GRADIENT || paint->type == NSVG_PAINT_RADIAL_GRADIENT) + free(paint->gradient); +} + +static void nsvg__deleteGradientData(NSVGgradientData* grad) +{ + NSVGgradientData* next; + while (grad != NULL) { + next = grad->next; + free(grad->stops); + free(grad); + grad = next; + } +} + +static void nsvg__deleteParser(NSVGparser* p) +{ + if (p != NULL) { + nsvg__deletePaths(p->plist); + nsvg__deleteGradientData(p->gradients); + nsvgDelete(p->image); + free(p->pts); + free(p); + } +} + +static void nsvg__resetPath(NSVGparser* p) +{ + p->npts = 0; +} + +static void nsvg__addPoint(NSVGparser* p, float x, float y) +{ + if (p->npts+1 > p->cpts) { + p->cpts = p->cpts ? p->cpts*2 : 8; + p->pts = (float*)realloc(p->pts, p->cpts*2*sizeof(float)); + if (!p->pts) return; + } + p->pts[p->npts*2+0] = x; + p->pts[p->npts*2+1] = y; + p->npts++; +} + +static void nsvg__moveTo(NSVGparser* p, float x, float y) +{ + if (p->npts > 0) { + p->pts[(p->npts-1)*2+0] = x; + p->pts[(p->npts-1)*2+1] = y; + } else { + nsvg__addPoint(p, x, y); + } +} + +static void nsvg__lineTo(NSVGparser* p, float x, float y) +{ + float px,py, dx,dy; + if (p->npts > 0) { + px = p->pts[(p->npts-1)*2+0]; + py = p->pts[(p->npts-1)*2+1]; + dx = x - px; + dy = y - py; + nsvg__addPoint(p, px + dx/3.0f, py + dy/3.0f); + nsvg__addPoint(p, x - dx/3.0f, y - dy/3.0f); + nsvg__addPoint(p, x, y); + } +} + +static void nsvg__cubicBezTo(NSVGparser* p, float cpx1, float cpy1, float cpx2, float cpy2, float x, float y) +{ + if (p->npts > 0) { + nsvg__addPoint(p, cpx1, cpy1); + nsvg__addPoint(p, cpx2, cpy2); + nsvg__addPoint(p, x, y); + } +} + +static NSVGattrib* nsvg__getAttr(NSVGparser* p) +{ + return &p->attr[p->attrHead]; +} + +static void nsvg__pushAttr(NSVGparser* p) +{ + if (p->attrHead < NSVG_MAX_ATTR-1) { + p->attrHead++; + memcpy(&p->attr[p->attrHead], &p->attr[p->attrHead-1], sizeof(NSVGattrib)); + } +} + +static void nsvg__popAttr(NSVGparser* p) +{ + if (p->attrHead > 0) + p->attrHead--; +} + +static float nsvg__actualOrigX(NSVGparser* p) +{ + return p->viewMinx; +} + +static float nsvg__actualOrigY(NSVGparser* p) +{ + return p->viewMiny; +} + +static float nsvg__actualWidth(NSVGparser* p) +{ + return p->viewWidth; +} + +static float nsvg__actualHeight(NSVGparser* p) +{ + return p->viewHeight; +} + +static float nsvg__actualLength(NSVGparser* p) +{ + float w = nsvg__actualWidth(p), h = nsvg__actualHeight(p); + return sqrtf(w*w + h*h) / sqrtf(2.0f); +} + +static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c, float orig, float length) +{ + NSVGattrib* attr = nsvg__getAttr(p); + switch (c.units) { + case NSVG_UNITS_USER: return c.value; + case NSVG_UNITS_PX: return c.value; + case NSVG_UNITS_PT: return c.value / 72.0f * p->dpi; + case NSVG_UNITS_PC: return c.value / 6.0f * p->dpi; + case NSVG_UNITS_MM: return c.value / 25.4f * p->dpi; + case NSVG_UNITS_CM: return c.value / 2.54f * p->dpi; + case NSVG_UNITS_IN: return c.value * p->dpi; + case NSVG_UNITS_EM: return c.value * attr->fontSize; + case NSVG_UNITS_EX: return c.value * attr->fontSize * 0.52f; // x-height of Helvetica. + case NSVG_UNITS_PERCENT: return orig + c.value / 100.0f * length; + default: return c.value; + } + return c.value; +} + +static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) +{ + NSVGgradientData* grad = p->gradients; + if (id == NULL || *id == '\0') + return NULL; + while (grad != NULL) { + if (strcmp(grad->id, id) == 0) + return grad; + grad = grad->next; + } + return NULL; +} + +static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, float *xform, signed char* paintType) +{ + NSVGgradientData* data = NULL; + NSVGgradientData* ref = NULL; + NSVGgradientStop* stops = NULL; + NSVGgradient* grad; + float ox, oy, sw, sh, sl; + int nstops = 0; + int refIter; + + data = nsvg__findGradientData(p, id); + if (data == NULL) return NULL; + + // TODO: use ref to fill in all unset values too. + ref = data; + refIter = 0; + while (ref != NULL) { + NSVGgradientData* nextRef = NULL; + if (stops == NULL && ref->stops != NULL) { + stops = ref->stops; + nstops = ref->nstops; + break; + } + nextRef = nsvg__findGradientData(p, ref->ref); + if (nextRef == ref) break; // prevent infite loops on malformed data + ref = nextRef; + refIter++; + if (refIter > 32) break; // prevent infite loops on malformed data + } + if (stops == NULL) return NULL; + + grad = (NSVGgradient*)malloc(sizeof(NSVGgradient) + sizeof(NSVGgradientStop)*(nstops-1)); + if (grad == NULL) return NULL; + + // The shape width and height. + if (data->units == NSVG_OBJECT_SPACE) { + ox = localBounds[0]; + oy = localBounds[1]; + sw = localBounds[2] - localBounds[0]; + sh = localBounds[3] - localBounds[1]; + } else { + ox = nsvg__actualOrigX(p); + oy = nsvg__actualOrigY(p); + sw = nsvg__actualWidth(p); + sh = nsvg__actualHeight(p); + } + sl = sqrtf(sw*sw + sh*sh) / sqrtf(2.0f); + + if (data->type == NSVG_PAINT_LINEAR_GRADIENT) { + float x1, y1, x2, y2, dx, dy; + x1 = nsvg__convertToPixels(p, data->linear.x1, ox, sw); + y1 = nsvg__convertToPixels(p, data->linear.y1, oy, sh); + x2 = nsvg__convertToPixels(p, data->linear.x2, ox, sw); + y2 = nsvg__convertToPixels(p, data->linear.y2, oy, sh); + // Calculate transform aligned to the line + dx = x2 - x1; + dy = y2 - y1; + grad->xform[0] = dy; grad->xform[1] = -dx; + grad->xform[2] = dx; grad->xform[3] = dy; + grad->xform[4] = x1; grad->xform[5] = y1; + } else { + float cx, cy, fx, fy, r; + cx = nsvg__convertToPixels(p, data->radial.cx, ox, sw); + cy = nsvg__convertToPixels(p, data->radial.cy, oy, sh); + fx = nsvg__convertToPixels(p, data->radial.fx, ox, sw); + fy = nsvg__convertToPixels(p, data->radial.fy, oy, sh); + r = nsvg__convertToPixels(p, data->radial.r, 0, sl); + // Calculate transform aligned to the circle + grad->xform[0] = r; grad->xform[1] = 0; + grad->xform[2] = 0; grad->xform[3] = r; + grad->xform[4] = cx; grad->xform[5] = cy; + grad->fx = fx / r; + grad->fy = fy / r; + } + + nsvg__xformMultiply(grad->xform, data->xform); + nsvg__xformMultiply(grad->xform, xform); + + grad->spread = data->spread; + memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop)); + grad->nstops = nstops; + + *paintType = data->type; + + return grad; +} + +static float nsvg__getAverageScale(float* t) +{ + float sx = sqrtf(t[0]*t[0] + t[2]*t[2]); + float sy = sqrtf(t[1]*t[1] + t[3]*t[3]); + return (sx + sy) * 0.5f; +} + +static void nsvg__getLocalBounds(float* bounds, NSVGshape *shape, float* xform) +{ + NSVGpath* path; + float curve[4*2], curveBounds[4]; + int i, first = 1; + for (path = shape->paths; path != NULL; path = path->next) { + nsvg__xformPoint(&curve[0], &curve[1], path->pts[0], path->pts[1], xform); + for (i = 0; i < path->npts-1; i += 3) { + nsvg__xformPoint(&curve[2], &curve[3], path->pts[(i+1)*2], path->pts[(i+1)*2+1], xform); + nsvg__xformPoint(&curve[4], &curve[5], path->pts[(i+2)*2], path->pts[(i+2)*2+1], xform); + nsvg__xformPoint(&curve[6], &curve[7], path->pts[(i+3)*2], path->pts[(i+3)*2+1], xform); + nsvg__curveBounds(curveBounds, curve); + if (first) { + bounds[0] = curveBounds[0]; + bounds[1] = curveBounds[1]; + bounds[2] = curveBounds[2]; + bounds[3] = curveBounds[3]; + first = 0; + } else { + bounds[0] = nsvg__minf(bounds[0], curveBounds[0]); + bounds[1] = nsvg__minf(bounds[1], curveBounds[1]); + bounds[2] = nsvg__maxf(bounds[2], curveBounds[2]); + bounds[3] = nsvg__maxf(bounds[3], curveBounds[3]); + } + curve[0] = curve[6]; + curve[1] = curve[7]; + } + } +} + +static void nsvg__addShape(NSVGparser* p) +{ + NSVGattrib* attr = nsvg__getAttr(p); + float scale = 1.0f; + NSVGshape* shape; + NSVGpath* path; + int i; + + if (p->plist == NULL) + return; + + shape = (NSVGshape*)malloc(sizeof(NSVGshape)); + if (shape == NULL) goto error; + memset(shape, 0, sizeof(NSVGshape)); + + memcpy(shape->id, attr->id, sizeof shape->id); + memcpy(shape->fillGradient, attr->fillGradient, sizeof shape->fillGradient); + memcpy(shape->strokeGradient, attr->strokeGradient, sizeof shape->strokeGradient); + memcpy(shape->xform, attr->xform, sizeof shape->xform); + scale = nsvg__getAverageScale(attr->xform); + shape->strokeWidth = attr->strokeWidth * scale; + shape->strokeDashOffset = attr->strokeDashOffset * scale; + shape->strokeDashCount = (char)attr->strokeDashCount; + for (i = 0; i < attr->strokeDashCount; i++) + shape->strokeDashArray[i] = attr->strokeDashArray[i] * scale; + shape->strokeLineJoin = attr->strokeLineJoin; + shape->strokeLineCap = attr->strokeLineCap; + shape->miterLimit = attr->miterLimit; + shape->fillRule = attr->fillRule; + shape->opacity = attr->opacity; + + shape->paths = p->plist; + p->plist = NULL; + + // Calculate shape bounds + shape->bounds[0] = shape->paths->bounds[0]; + shape->bounds[1] = shape->paths->bounds[1]; + shape->bounds[2] = shape->paths->bounds[2]; + shape->bounds[3] = shape->paths->bounds[3]; + for (path = shape->paths->next; path != NULL; path = path->next) { + shape->bounds[0] = nsvg__minf(shape->bounds[0], path->bounds[0]); + shape->bounds[1] = nsvg__minf(shape->bounds[1], path->bounds[1]); + shape->bounds[2] = nsvg__maxf(shape->bounds[2], path->bounds[2]); + shape->bounds[3] = nsvg__maxf(shape->bounds[3], path->bounds[3]); + } + + // Set fill + if (attr->hasFill == 0) { + shape->fill.type = NSVG_PAINT_NONE; + } else if (attr->hasFill == 1) { + shape->fill.type = NSVG_PAINT_COLOR; + shape->fill.color = attr->fillColor; + shape->fill.color |= (unsigned int)(attr->fillOpacity*255) << 24; + } else if (attr->hasFill == 2) { + shape->fill.type = NSVG_PAINT_UNDEF; + } + + // Set stroke + if (attr->hasStroke == 0) { + shape->stroke.type = NSVG_PAINT_NONE; + } else if (attr->hasStroke == 1) { + shape->stroke.type = NSVG_PAINT_COLOR; + shape->stroke.color = attr->strokeColor; + shape->stroke.color |= (unsigned int)(attr->strokeOpacity*255) << 24; + } else if (attr->hasStroke == 2) { + shape->stroke.type = NSVG_PAINT_UNDEF; + } + + // Set flags + shape->flags = (attr->visible ? NSVG_FLAGS_VISIBLE : 0x00); + + // Add to tail + if (p->image->shapes == NULL) + p->image->shapes = shape; + else + p->shapesTail->next = shape; + p->shapesTail = shape; + + return; + +error: + if (shape) free(shape); +} + +static void nsvg__addPath(NSVGparser* p, char closed) +{ + NSVGattrib* attr = nsvg__getAttr(p); + NSVGpath* path = NULL; + float bounds[4]; + float* curve; + int i; + + if (p->npts < 4) + return; + + if (closed) + nsvg__lineTo(p, p->pts[0], p->pts[1]); + + // Expect 1 + N*3 points (N = number of cubic bezier segments). + if ((p->npts % 3) != 1) + return; + + path = (NSVGpath*)malloc(sizeof(NSVGpath)); + if (path == NULL) goto error; + memset(path, 0, sizeof(NSVGpath)); + + path->pts = (float*)malloc(p->npts*2*sizeof(float)); + if (path->pts == NULL) goto error; + path->closed = closed; + path->npts = p->npts; + + // Transform path. + for (i = 0; i < p->npts; ++i) + nsvg__xformPoint(&path->pts[i*2], &path->pts[i*2+1], p->pts[i*2], p->pts[i*2+1], attr->xform); + + // Find bounds + for (i = 0; i < path->npts-1; i += 3) { + curve = &path->pts[i*2]; + nsvg__curveBounds(bounds, curve); + if (i == 0) { + path->bounds[0] = bounds[0]; + path->bounds[1] = bounds[1]; + path->bounds[2] = bounds[2]; + path->bounds[3] = bounds[3]; + } else { + path->bounds[0] = nsvg__minf(path->bounds[0], bounds[0]); + path->bounds[1] = nsvg__minf(path->bounds[1], bounds[1]); + path->bounds[2] = nsvg__maxf(path->bounds[2], bounds[2]); + path->bounds[3] = nsvg__maxf(path->bounds[3], bounds[3]); + } + } + + path->next = p->plist; + p->plist = path; + + return; + +error: + if (path != NULL) { + if (path->pts != NULL) free(path->pts); + free(path); + } +} + +// We roll our own string to float because the std library one uses locale and messes things up. +static double nsvg__atof(const char* s) +{ + char* cur = (char*)s; + char* end = NULL; + double res = 0.0, sign = 1.0; + long long intPart = 0, fracPart = 0; + char hasIntPart = 0, hasFracPart = 0; + + // Parse optional sign + if (*cur == '+') { + cur++; + } else if (*cur == '-') { + sign = -1; + cur++; + } + + // Parse integer part + if (nsvg__isdigit(*cur)) { + // Parse digit sequence + intPart = strtoll(cur, &end, 10); + if (cur != end) { + res = (double)intPart; + hasIntPart = 1; + cur = end; + } + } + + // Parse fractional part. + if (*cur == '.') { + cur++; // Skip '.' + if (nsvg__isdigit(*cur)) { + // Parse digit sequence + fracPart = strtoll(cur, &end, 10); + if (cur != end) { + res += (double)fracPart / pow(10.0, (double)(end - cur)); + hasFracPart = 1; + cur = end; + } + } + } + + // A valid number should have integer or fractional part. + if (!hasIntPart && !hasFracPart) + return 0.0; + + // Parse optional exponent + if (*cur == 'e' || *cur == 'E') { + long expPart = 0; + cur++; // skip 'E' + expPart = strtol(cur, &end, 10); // Parse digit sequence with sign + if (cur != end) { + res *= pow(10.0, (double)expPart); + } + } + + return res * sign; +} + + +static const char* nsvg__parseNumber(const char* s, char* it, const int size) +{ + const int last = size-1; + int i = 0; + + // sign + if (*s == '-' || *s == '+') { + if (i < last) it[i++] = *s; + s++; + } + // integer part + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + if (*s == '.') { + // decimal point + if (i < last) it[i++] = *s; + s++; + // fraction part + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + } + // exponent + if ((*s == 'e' || *s == 'E') && (s[1] != 'm' && s[1] != 'x')) { + if (i < last) it[i++] = *s; + s++; + if (*s == '-' || *s == '+') { + if (i < last) it[i++] = *s; + s++; + } + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + } + it[i] = '\0'; + + return s; +} + +static const char* nsvg__getNextPathItemWhenArcFlag(const char* s, char* it) +{ + it[0] = '\0'; + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '0' || *s == '1') { + it[0] = *s++; + it[1] = '\0'; + return s; + } + return s; +} + +static const char* nsvg__getNextPathItem(const char* s, char* it) +{ + it[0] = '\0'; + // Skip white spaces and commas + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '-' || *s == '+' || *s == '.' || nsvg__isdigit(*s)) { + s = nsvg__parseNumber(s, it, 64); + } else { + // Parse command + it[0] = *s++; + it[1] = '\0'; + return s; + } + + return s; +} + +static unsigned int nsvg__parseColorHex(const char* str) +{ + unsigned int r=0, g=0, b=0; + if (sscanf(str, "#%2x%2x%2x", &r, &g, &b) == 3 ) // 2 digit hex + return NSVG_RGB(r, g, b); + if (sscanf(str, "#%1x%1x%1x", &r, &g, &b) == 3 ) // 1 digit hex, e.g. #abc -> 0xccbbaa + return NSVG_RGB(r*17, g*17, b*17); // same effect as (r<<4|r), (g<<4|g), .. + return NSVG_RGB(128, 128, 128); +} + +// Parse rgb color. The pointer 'str' must point at "rgb(" (4+ characters). +// This function returns gray (rgb(128, 128, 128) == '#808080') on parse errors +// for backwards compatibility. Note: other image viewers return black instead. + +static unsigned int nsvg__parseColorRGB(const char* str) +{ + int i; + unsigned int rgbi[3]; + float rgbf[3]; + // try decimal integers first + if (sscanf(str, "rgb(%u, %u, %u)", &rgbi[0], &rgbi[1], &rgbi[2]) != 3) { + // integers failed, try percent values (float, locale independent) + const char delimiter[3] = {',', ',', ')'}; + str += 4; // skip "rgb(" + for (i = 0; i < 3; i++) { + while (*str && (nsvg__isspace(*str))) str++; // skip leading spaces + if (*str == '+') str++; // skip '+' (don't allow '-') + if (!*str) break; + rgbf[i] = nsvg__atof(str); + + // Note 1: it would be great if nsvg__atof() returned how many + // bytes it consumed but it doesn't. We need to skip the number, + // the '%' character, spaces, and the delimiter ',' or ')'. + + // Note 2: The following code does not allow values like "33.%", + // i.e. a decimal point w/o fractional part, but this is consistent + // with other image viewers, e.g. firefox, chrome, eog, gimp. + + while (*str && nsvg__isdigit(*str)) str++; // skip integer part + if (*str == '.') { + str++; + if (!nsvg__isdigit(*str)) break; // error: no digit after '.' + while (*str && nsvg__isdigit(*str)) str++; // skip fractional part + } + if (*str == '%') str++; else break; + while (nsvg__isspace(*str)) str++; + if (*str == delimiter[i]) str++; + else break; + } + if (i == 3) { + rgbi[0] = roundf(rgbf[0] * 2.55f); + rgbi[1] = roundf(rgbf[1] * 2.55f); + rgbi[2] = roundf(rgbf[2] * 2.55f); + } else { + rgbi[0] = rgbi[1] = rgbi[2] = 128; + } + } + // clip values as the CSS spec requires + for (i = 0; i < 3; i++) { + if (rgbi[i] > 255) rgbi[i] = 255; + } + return NSVG_RGB(rgbi[0], rgbi[1], rgbi[2]); +} + +typedef struct NSVGNamedColor { + const char* name; + unsigned int color; +} NSVGNamedColor; + +NSVGNamedColor nsvg__colors[] = { + + { "red", NSVG_RGB(255, 0, 0) }, + { "green", NSVG_RGB( 0, 128, 0) }, + { "blue", NSVG_RGB( 0, 0, 255) }, + { "yellow", NSVG_RGB(255, 255, 0) }, + { "cyan", NSVG_RGB( 0, 255, 255) }, + { "magenta", NSVG_RGB(255, 0, 255) }, + { "black", NSVG_RGB( 0, 0, 0) }, + { "grey", NSVG_RGB(128, 128, 128) }, + { "gray", NSVG_RGB(128, 128, 128) }, + { "white", NSVG_RGB(255, 255, 255) }, + +#ifdef NANOSVG_ALL_COLOR_KEYWORDS + { "aliceblue", NSVG_RGB(240, 248, 255) }, + { "antiquewhite", NSVG_RGB(250, 235, 215) }, + { "aqua", NSVG_RGB( 0, 255, 255) }, + { "aquamarine", NSVG_RGB(127, 255, 212) }, + { "azure", NSVG_RGB(240, 255, 255) }, + { "beige", NSVG_RGB(245, 245, 220) }, + { "bisque", NSVG_RGB(255, 228, 196) }, + { "blanchedalmond", NSVG_RGB(255, 235, 205) }, + { "blueviolet", NSVG_RGB(138, 43, 226) }, + { "brown", NSVG_RGB(165, 42, 42) }, + { "burlywood", NSVG_RGB(222, 184, 135) }, + { "cadetblue", NSVG_RGB( 95, 158, 160) }, + { "chartreuse", NSVG_RGB(127, 255, 0) }, + { "chocolate", NSVG_RGB(210, 105, 30) }, + { "coral", NSVG_RGB(255, 127, 80) }, + { "cornflowerblue", NSVG_RGB(100, 149, 237) }, + { "cornsilk", NSVG_RGB(255, 248, 220) }, + { "crimson", NSVG_RGB(220, 20, 60) }, + { "darkblue", NSVG_RGB( 0, 0, 139) }, + { "darkcyan", NSVG_RGB( 0, 139, 139) }, + { "darkgoldenrod", NSVG_RGB(184, 134, 11) }, + { "darkgray", NSVG_RGB(169, 169, 169) }, + { "darkgreen", NSVG_RGB( 0, 100, 0) }, + { "darkgrey", NSVG_RGB(169, 169, 169) }, + { "darkkhaki", NSVG_RGB(189, 183, 107) }, + { "darkmagenta", NSVG_RGB(139, 0, 139) }, + { "darkolivegreen", NSVG_RGB( 85, 107, 47) }, + { "darkorange", NSVG_RGB(255, 140, 0) }, + { "darkorchid", NSVG_RGB(153, 50, 204) }, + { "darkred", NSVG_RGB(139, 0, 0) }, + { "darksalmon", NSVG_RGB(233, 150, 122) }, + { "darkseagreen", NSVG_RGB(143, 188, 143) }, + { "darkslateblue", NSVG_RGB( 72, 61, 139) }, + { "darkslategray", NSVG_RGB( 47, 79, 79) }, + { "darkslategrey", NSVG_RGB( 47, 79, 79) }, + { "darkturquoise", NSVG_RGB( 0, 206, 209) }, + { "darkviolet", NSVG_RGB(148, 0, 211) }, + { "deeppink", NSVG_RGB(255, 20, 147) }, + { "deepskyblue", NSVG_RGB( 0, 191, 255) }, + { "dimgray", NSVG_RGB(105, 105, 105) }, + { "dimgrey", NSVG_RGB(105, 105, 105) }, + { "dodgerblue", NSVG_RGB( 30, 144, 255) }, + { "firebrick", NSVG_RGB(178, 34, 34) }, + { "floralwhite", NSVG_RGB(255, 250, 240) }, + { "forestgreen", NSVG_RGB( 34, 139, 34) }, + { "fuchsia", NSVG_RGB(255, 0, 255) }, + { "gainsboro", NSVG_RGB(220, 220, 220) }, + { "ghostwhite", NSVG_RGB(248, 248, 255) }, + { "gold", NSVG_RGB(255, 215, 0) }, + { "goldenrod", NSVG_RGB(218, 165, 32) }, + { "greenyellow", NSVG_RGB(173, 255, 47) }, + { "honeydew", NSVG_RGB(240, 255, 240) }, + { "hotpink", NSVG_RGB(255, 105, 180) }, + { "indianred", NSVG_RGB(205, 92, 92) }, + { "indigo", NSVG_RGB( 75, 0, 130) }, + { "ivory", NSVG_RGB(255, 255, 240) }, + { "khaki", NSVG_RGB(240, 230, 140) }, + { "lavender", NSVG_RGB(230, 230, 250) }, + { "lavenderblush", NSVG_RGB(255, 240, 245) }, + { "lawngreen", NSVG_RGB(124, 252, 0) }, + { "lemonchiffon", NSVG_RGB(255, 250, 205) }, + { "lightblue", NSVG_RGB(173, 216, 230) }, + { "lightcoral", NSVG_RGB(240, 128, 128) }, + { "lightcyan", NSVG_RGB(224, 255, 255) }, + { "lightgoldenrodyellow", NSVG_RGB(250, 250, 210) }, + { "lightgray", NSVG_RGB(211, 211, 211) }, + { "lightgreen", NSVG_RGB(144, 238, 144) }, + { "lightgrey", NSVG_RGB(211, 211, 211) }, + { "lightpink", NSVG_RGB(255, 182, 193) }, + { "lightsalmon", NSVG_RGB(255, 160, 122) }, + { "lightseagreen", NSVG_RGB( 32, 178, 170) }, + { "lightskyblue", NSVG_RGB(135, 206, 250) }, + { "lightslategray", NSVG_RGB(119, 136, 153) }, + { "lightslategrey", NSVG_RGB(119, 136, 153) }, + { "lightsteelblue", NSVG_RGB(176, 196, 222) }, + { "lightyellow", NSVG_RGB(255, 255, 224) }, + { "lime", NSVG_RGB( 0, 255, 0) }, + { "limegreen", NSVG_RGB( 50, 205, 50) }, + { "linen", NSVG_RGB(250, 240, 230) }, + { "maroon", NSVG_RGB(128, 0, 0) }, + { "mediumaquamarine", NSVG_RGB(102, 205, 170) }, + { "mediumblue", NSVG_RGB( 0, 0, 205) }, + { "mediumorchid", NSVG_RGB(186, 85, 211) }, + { "mediumpurple", NSVG_RGB(147, 112, 219) }, + { "mediumseagreen", NSVG_RGB( 60, 179, 113) }, + { "mediumslateblue", NSVG_RGB(123, 104, 238) }, + { "mediumspringgreen", NSVG_RGB( 0, 250, 154) }, + { "mediumturquoise", NSVG_RGB( 72, 209, 204) }, + { "mediumvioletred", NSVG_RGB(199, 21, 133) }, + { "midnightblue", NSVG_RGB( 25, 25, 112) }, + { "mintcream", NSVG_RGB(245, 255, 250) }, + { "mistyrose", NSVG_RGB(255, 228, 225) }, + { "moccasin", NSVG_RGB(255, 228, 181) }, + { "navajowhite", NSVG_RGB(255, 222, 173) }, + { "navy", NSVG_RGB( 0, 0, 128) }, + { "oldlace", NSVG_RGB(253, 245, 230) }, + { "olive", NSVG_RGB(128, 128, 0) }, + { "olivedrab", NSVG_RGB(107, 142, 35) }, + { "orange", NSVG_RGB(255, 165, 0) }, + { "orangered", NSVG_RGB(255, 69, 0) }, + { "orchid", NSVG_RGB(218, 112, 214) }, + { "palegoldenrod", NSVG_RGB(238, 232, 170) }, + { "palegreen", NSVG_RGB(152, 251, 152) }, + { "paleturquoise", NSVG_RGB(175, 238, 238) }, + { "palevioletred", NSVG_RGB(219, 112, 147) }, + { "papayawhip", NSVG_RGB(255, 239, 213) }, + { "peachpuff", NSVG_RGB(255, 218, 185) }, + { "peru", NSVG_RGB(205, 133, 63) }, + { "pink", NSVG_RGB(255, 192, 203) }, + { "plum", NSVG_RGB(221, 160, 221) }, + { "powderblue", NSVG_RGB(176, 224, 230) }, + { "purple", NSVG_RGB(128, 0, 128) }, + { "rosybrown", NSVG_RGB(188, 143, 143) }, + { "royalblue", NSVG_RGB( 65, 105, 225) }, + { "saddlebrown", NSVG_RGB(139, 69, 19) }, + { "salmon", NSVG_RGB(250, 128, 114) }, + { "sandybrown", NSVG_RGB(244, 164, 96) }, + { "seagreen", NSVG_RGB( 46, 139, 87) }, + { "seashell", NSVG_RGB(255, 245, 238) }, + { "sienna", NSVG_RGB(160, 82, 45) }, + { "silver", NSVG_RGB(192, 192, 192) }, + { "skyblue", NSVG_RGB(135, 206, 235) }, + { "slateblue", NSVG_RGB(106, 90, 205) }, + { "slategray", NSVG_RGB(112, 128, 144) }, + { "slategrey", NSVG_RGB(112, 128, 144) }, + { "snow", NSVG_RGB(255, 250, 250) }, + { "springgreen", NSVG_RGB( 0, 255, 127) }, + { "steelblue", NSVG_RGB( 70, 130, 180) }, + { "tan", NSVG_RGB(210, 180, 140) }, + { "teal", NSVG_RGB( 0, 128, 128) }, + { "thistle", NSVG_RGB(216, 191, 216) }, + { "tomato", NSVG_RGB(255, 99, 71) }, + { "turquoise", NSVG_RGB( 64, 224, 208) }, + { "violet", NSVG_RGB(238, 130, 238) }, + { "wheat", NSVG_RGB(245, 222, 179) }, + { "whitesmoke", NSVG_RGB(245, 245, 245) }, + { "yellowgreen", NSVG_RGB(154, 205, 50) }, +#endif +}; + +static unsigned int nsvg__parseColorName(const char* str) +{ + int i, ncolors = sizeof(nsvg__colors) / sizeof(NSVGNamedColor); + + for (i = 0; i < ncolors; i++) { + if (strcmp(nsvg__colors[i].name, str) == 0) { + return nsvg__colors[i].color; + } + } + + return NSVG_RGB(128, 128, 128); +} + +static unsigned int nsvg__parseColor(const char* str) +{ + size_t len = 0; + while(*str == ' ') ++str; + len = strlen(str); + if (len >= 1 && *str == '#') + return nsvg__parseColorHex(str); + else if (len >= 4 && str[0] == 'r' && str[1] == 'g' && str[2] == 'b' && str[3] == '(') + return nsvg__parseColorRGB(str); + return nsvg__parseColorName(str); +} + +static float nsvg__parseOpacity(const char* str) +{ + float val = nsvg__atof(str); + if (val < 0.0f) val = 0.0f; + if (val > 1.0f) val = 1.0f; + return val; +} + +static float nsvg__parseMiterLimit(const char* str) +{ + float val = nsvg__atof(str); + if (val < 0.0f) val = 0.0f; + return val; +} + +static int nsvg__parseUnits(const char* units) +{ + if (units[0] == 'p' && units[1] == 'x') + return NSVG_UNITS_PX; + else if (units[0] == 'p' && units[1] == 't') + return NSVG_UNITS_PT; + else if (units[0] == 'p' && units[1] == 'c') + return NSVG_UNITS_PC; + else if (units[0] == 'm' && units[1] == 'm') + return NSVG_UNITS_MM; + else if (units[0] == 'c' && units[1] == 'm') + return NSVG_UNITS_CM; + else if (units[0] == 'i' && units[1] == 'n') + return NSVG_UNITS_IN; + else if (units[0] == '%') + return NSVG_UNITS_PERCENT; + else if (units[0] == 'e' && units[1] == 'm') + return NSVG_UNITS_EM; + else if (units[0] == 'e' && units[1] == 'x') + return NSVG_UNITS_EX; + return NSVG_UNITS_USER; +} + +static int nsvg__isCoordinate(const char* s) +{ + // optional sign + if (*s == '-' || *s == '+') + s++; + // must have at least one digit, or start by a dot + return (nsvg__isdigit(*s) || *s == '.'); +} + +static NSVGcoordinate nsvg__parseCoordinateRaw(const char* str) +{ + NSVGcoordinate coord = {0, NSVG_UNITS_USER}; + char buf[64]; + coord.units = nsvg__parseUnits(nsvg__parseNumber(str, buf, 64)); + coord.value = nsvg__atof(buf); + return coord; +} + +static NSVGcoordinate nsvg__coord(float v, int units) +{ + NSVGcoordinate coord = {v, units}; + return coord; +} + +static float nsvg__parseCoordinate(NSVGparser* p, const char* str, float orig, float length) +{ + NSVGcoordinate coord = nsvg__parseCoordinateRaw(str); + return nsvg__convertToPixels(p, coord, orig, length); +} + +static int nsvg__parseTransformArgs(const char* str, float* args, int maxNa, int* na) +{ + const char* end; + const char* ptr; + char it[64]; + + *na = 0; + ptr = str; + while (*ptr && *ptr != '(') ++ptr; + if (*ptr == 0) + return 1; + end = ptr; + while (*end && *end != ')') ++end; + if (*end == 0) + return 1; + + while (ptr < end) { + if (*ptr == '-' || *ptr == '+' || *ptr == '.' || nsvg__isdigit(*ptr)) { + if (*na >= maxNa) return 0; + ptr = nsvg__parseNumber(ptr, it, 64); + args[(*na)++] = (float)nsvg__atof(it); + } else { + ++ptr; + } + } + return (int)(end - str); +} + + +static int nsvg__parseMatrix(float* xform, const char* str) +{ + float t[6]; + int na = 0; + int len = nsvg__parseTransformArgs(str, t, 6, &na); + if (na != 6) return len; + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseTranslate(float* xform, const char* str) +{ + float args[2]; + float t[6]; + int na = 0; + int len = nsvg__parseTransformArgs(str, args, 2, &na); + if (na == 1) args[1] = 0.0; + + nsvg__xformSetTranslation(t, args[0], args[1]); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseScale(float* xform, const char* str) +{ + float args[2]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 2, &na); + if (na == 1) args[1] = args[0]; + nsvg__xformSetScale(t, args[0], args[1]); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseSkewX(float* xform, const char* str) +{ + float args[1]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 1, &na); + nsvg__xformSetSkewX(t, args[0]/180.0f*NSVG_PI); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseSkewY(float* xform, const char* str) +{ + float args[1]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 1, &na); + nsvg__xformSetSkewY(t, args[0]/180.0f*NSVG_PI); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseRotate(float* xform, const char* str) +{ + float args[3]; + int na = 0; + float m[6]; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 3, &na); + if (na == 1) + args[1] = args[2] = 0.0f; + nsvg__xformIdentity(m); + + if (na > 1) { + nsvg__xformSetTranslation(t, -args[1], -args[2]); + nsvg__xformMultiply(m, t); + } + + nsvg__xformSetRotation(t, args[0]/180.0f*NSVG_PI); + nsvg__xformMultiply(m, t); + + if (na > 1) { + nsvg__xformSetTranslation(t, args[1], args[2]); + nsvg__xformMultiply(m, t); + } + + memcpy(xform, m, sizeof(float)*6); + + return len; +} + +static void nsvg__parseTransform(float* xform, const char* str) +{ + float t[6]; + int len; + nsvg__xformIdentity(xform); + while (*str) + { + if (strncmp(str, "matrix", 6) == 0) + len = nsvg__parseMatrix(t, str); + else if (strncmp(str, "translate", 9) == 0) + len = nsvg__parseTranslate(t, str); + else if (strncmp(str, "scale", 5) == 0) + len = nsvg__parseScale(t, str); + else if (strncmp(str, "rotate", 6) == 0) + len = nsvg__parseRotate(t, str); + else if (strncmp(str, "skewX", 5) == 0) + len = nsvg__parseSkewX(t, str); + else if (strncmp(str, "skewY", 5) == 0) + len = nsvg__parseSkewY(t, str); + else{ + ++str; + continue; + } + if (len != 0) { + str += len; + } else { + ++str; + continue; + } + + nsvg__xformPremultiply(xform, t); + } +} + +static void nsvg__parseUrl(char* id, const char* str) +{ + int i = 0; + str += 4; // "url("; + if (*str && *str == '#') + str++; + while (i < 63 && *str && *str != ')') { + id[i] = *str++; + i++; + } + id[i] = '\0'; +} + +static char nsvg__parseLineCap(const char* str) +{ + if (strcmp(str, "butt") == 0) + return NSVG_CAP_BUTT; + else if (strcmp(str, "round") == 0) + return NSVG_CAP_ROUND; + else if (strcmp(str, "square") == 0) + return NSVG_CAP_SQUARE; + // TODO: handle inherit. + return NSVG_CAP_BUTT; +} + +static char nsvg__parseLineJoin(const char* str) +{ + if (strcmp(str, "miter") == 0) + return NSVG_JOIN_MITER; + else if (strcmp(str, "round") == 0) + return NSVG_JOIN_ROUND; + else if (strcmp(str, "bevel") == 0) + return NSVG_JOIN_BEVEL; + // TODO: handle inherit. + return NSVG_JOIN_MITER; +} + +static char nsvg__parseFillRule(const char* str) +{ + if (strcmp(str, "nonzero") == 0) + return NSVG_FILLRULE_NONZERO; + else if (strcmp(str, "evenodd") == 0) + return NSVG_FILLRULE_EVENODD; + // TODO: handle inherit. + return NSVG_FILLRULE_NONZERO; +} + +static const char* nsvg__getNextDashItem(const char* s, char* it) +{ + int n = 0; + it[0] = '\0'; + // Skip white spaces and commas + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + // Advance until whitespace, comma or end. + while (*s && (!nsvg__isspace(*s) && *s != ',')) { + if (n < 63) + it[n++] = *s; + s++; + } + it[n++] = '\0'; + return s; +} + +static int nsvg__parseStrokeDashArray(NSVGparser* p, const char* str, float* strokeDashArray) +{ + char item[64]; + int count = 0, i; + float sum = 0.0f; + + // Handle "none" + if (str[0] == 'n') + return 0; + + // Parse dashes + while (*str) { + str = nsvg__getNextDashItem(str, item); + if (!*item) break; + if (count < NSVG_MAX_DASHES) + strokeDashArray[count++] = fabsf(nsvg__parseCoordinate(p, item, 0.0f, nsvg__actualLength(p))); + } + + for (i = 0; i < count; i++) + sum += strokeDashArray[i]; + if (sum <= 1e-6f) + count = 0; + + return count; +} + +static void nsvg__parseStyle(NSVGparser* p, const char* str); + +static int nsvg__parseAttr(NSVGparser* p, const char* name, const char* value) +{ + float xform[6]; + NSVGattrib* attr = nsvg__getAttr(p); + if (!attr) return 0; + + if (strcmp(name, "style") == 0) { + nsvg__parseStyle(p, value); + } else if (strcmp(name, "display") == 0) { + if (strcmp(value, "none") == 0) + attr->visible = 0; + // Don't reset ->visible on display:inline, one display:none hides the whole subtree + + } else if (strcmp(name, "fill") == 0) { + if (strcmp(value, "none") == 0) { + attr->hasFill = 0; + } else if (strncmp(value, "url(", 4) == 0) { + attr->hasFill = 2; + nsvg__parseUrl(attr->fillGradient, value); + } else { + attr->hasFill = 1; + attr->fillColor = nsvg__parseColor(value); + } + } else if (strcmp(name, "opacity") == 0) { + attr->opacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "fill-opacity") == 0) { + attr->fillOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "stroke") == 0) { + if (strcmp(value, "none") == 0) { + attr->hasStroke = 0; + } else if (strncmp(value, "url(", 4) == 0) { + attr->hasStroke = 2; + nsvg__parseUrl(attr->strokeGradient, value); + } else { + attr->hasStroke = 1; + attr->strokeColor = nsvg__parseColor(value); + } + } else if (strcmp(name, "stroke-width") == 0) { + attr->strokeWidth = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "stroke-dasharray") == 0) { + attr->strokeDashCount = nsvg__parseStrokeDashArray(p, value, attr->strokeDashArray); + } else if (strcmp(name, "stroke-dashoffset") == 0) { + attr->strokeDashOffset = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "stroke-opacity") == 0) { + attr->strokeOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "stroke-linecap") == 0) { + attr->strokeLineCap = nsvg__parseLineCap(value); + } else if (strcmp(name, "stroke-linejoin") == 0) { + attr->strokeLineJoin = nsvg__parseLineJoin(value); + } else if (strcmp(name, "stroke-miterlimit") == 0) { + attr->miterLimit = nsvg__parseMiterLimit(value); + } else if (strcmp(name, "fill-rule") == 0) { + attr->fillRule = nsvg__parseFillRule(value); + } else if (strcmp(name, "font-size") == 0) { + attr->fontSize = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "transform") == 0) { + nsvg__parseTransform(xform, value); + nsvg__xformPremultiply(attr->xform, xform); + } else if (strcmp(name, "stop-color") == 0) { + attr->stopColor = nsvg__parseColor(value); + } else if (strcmp(name, "stop-opacity") == 0) { + attr->stopOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "offset") == 0) { + attr->stopOffset = nsvg__parseCoordinate(p, value, 0.0f, 1.0f); + } else if (strcmp(name, "id") == 0) { + strncpy(attr->id, value, 63); + attr->id[63] = '\0'; + } else { + return 0; + } + return 1; +} + +static int nsvg__parseNameValue(NSVGparser* p, const char* start, const char* end) +{ + const char* str; + const char* val; + char name[512]; + char value[512]; + int n; + + str = start; + while (str < end && *str != ':') ++str; + + val = str; + + // Right Trim + while (str > start && (*str == ':' || nsvg__isspace(*str))) --str; + ++str; + + n = (int)(str - start); + if (n > 511) n = 511; + if (n) memcpy(name, start, n); + name[n] = 0; + + while (val < end && (*val == ':' || nsvg__isspace(*val))) ++val; + + n = (int)(end - val); + if (n > 511) n = 511; + if (n) memcpy(value, val, n); + value[n] = 0; + + return nsvg__parseAttr(p, name, value); +} + +static void nsvg__parseStyle(NSVGparser* p, const char* str) +{ + const char* start; + const char* end; + + while (*str) { + // Left Trim + while(*str && nsvg__isspace(*str)) ++str; + start = str; + while(*str && *str != ';') ++str; + end = str; + + // Right Trim + while (end > start && (*end == ';' || nsvg__isspace(*end))) --end; + ++end; + + nsvg__parseNameValue(p, start, end); + if (*str) ++str; + } +} + +static void nsvg__parseAttribs(NSVGparser* p, const char** attr) +{ + int i; + for (i = 0; attr[i]; i += 2) + { + if (strcmp(attr[i], "style") == 0) + nsvg__parseStyle(p, attr[i + 1]); + else + nsvg__parseAttr(p, attr[i], attr[i + 1]); + } +} + +static int nsvg__getArgsPerElement(char cmd) +{ + switch (cmd) { + case 'v': + case 'V': + case 'h': + case 'H': + return 1; + case 'm': + case 'M': + case 'l': + case 'L': + case 't': + case 'T': + return 2; + case 'q': + case 'Q': + case 's': + case 'S': + return 4; + case 'c': + case 'C': + return 6; + case 'a': + case 'A': + return 7; + case 'z': + case 'Z': + return 0; + } + return -1; +} + +static void nsvg__pathMoveTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) { + *cpx += args[0]; + *cpy += args[1]; + } else { + *cpx = args[0]; + *cpy = args[1]; + } + nsvg__moveTo(p, *cpx, *cpy); +} + +static void nsvg__pathLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) { + *cpx += args[0]; + *cpy += args[1]; + } else { + *cpx = args[0]; + *cpy = args[1]; + } + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathHLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) + *cpx += args[0]; + else + *cpx = args[0]; + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathVLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) + *cpy += args[0]; + else + *cpy = args[0]; + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathCubicBezTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x2, y2, cx1, cy1, cx2, cy2; + + if (rel) { + cx1 = *cpx + args[0]; + cy1 = *cpy + args[1]; + cx2 = *cpx + args[2]; + cy2 = *cpy + args[3]; + x2 = *cpx + args[4]; + y2 = *cpy + args[5]; + } else { + cx1 = args[0]; + cy1 = args[1]; + cx2 = args[2]; + cy2 = args[3]; + x2 = args[4]; + y2 = args[5]; + } + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx2; + *cpy2 = cy2; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathCubicBezShortTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + cx2 = *cpx + args[0]; + cy2 = *cpy + args[1]; + x2 = *cpx + args[2]; + y2 = *cpy + args[3]; + } else { + cx2 = args[0]; + cy2 = args[1]; + x2 = args[2]; + y2 = args[3]; + } + + cx1 = 2*x1 - *cpx2; + cy1 = 2*y1 - *cpy2; + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx2; + *cpy2 = cy2; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathQuadBezTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx, cy; + float cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + cx = *cpx + args[0]; + cy = *cpy + args[1]; + x2 = *cpx + args[2]; + y2 = *cpy + args[3]; + } else { + cx = args[0]; + cy = args[1]; + x2 = args[2]; + y2 = args[3]; + } + + // Convert to cubic bezier + cx1 = x1 + 2.0f/3.0f*(cx - x1); + cy1 = y1 + 2.0f/3.0f*(cy - y1); + cx2 = x2 + 2.0f/3.0f*(cx - x2); + cy2 = y2 + 2.0f/3.0f*(cy - y2); + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx; + *cpy2 = cy; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathQuadBezShortTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx, cy; + float cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + x2 = *cpx + args[0]; + y2 = *cpy + args[1]; + } else { + x2 = args[0]; + y2 = args[1]; + } + + cx = 2*x1 - *cpx2; + cy = 2*y1 - *cpy2; + + // Convert to cubix bezier + cx1 = x1 + 2.0f/3.0f*(cx - x1); + cy1 = y1 + 2.0f/3.0f*(cy - y1); + cx2 = x2 + 2.0f/3.0f*(cx - x2); + cy2 = y2 + 2.0f/3.0f*(cy - y2); + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx; + *cpy2 = cy; + *cpx = x2; + *cpy = y2; +} + +static float nsvg__sqr(float x) { return x*x; } +static float nsvg__vmag(float x, float y) { return sqrtf(x*x + y*y); } + +static float nsvg__vecrat(float ux, float uy, float vx, float vy) +{ + return (ux*vx + uy*vy) / (nsvg__vmag(ux,uy) * nsvg__vmag(vx,vy)); +} + +static float nsvg__vecang(float ux, float uy, float vx, float vy) +{ + float r = nsvg__vecrat(ux,uy, vx,vy); + if (r < -1.0f) r = -1.0f; + if (r > 1.0f) r = 1.0f; + return ((ux*vy < uy*vx) ? -1.0f : 1.0f) * acosf(r); +} + +static void nsvg__pathArcTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + // Ported from canvg (https://code.google.com/p/canvg/) + float rx, ry, rotx; + float x1, y1, x2, y2, cx, cy, dx, dy, d; + float x1p, y1p, cxp, cyp, s, sa, sb; + float ux, uy, vx, vy, a1, da; + float x, y, tanx, tany, a, px = 0, py = 0, ptanx = 0, ptany = 0, t[6]; + float sinrx, cosrx; + int fa, fs; + int i, ndivs; + float hda, kappa; + + rx = fabsf(args[0]); // y radius + ry = fabsf(args[1]); // x radius + rotx = args[2] / 180.0f * NSVG_PI; // x rotation angle + fa = fabsf(args[3]) > 1e-6 ? 1 : 0; // Large arc + fs = fabsf(args[4]) > 1e-6 ? 1 : 0; // Sweep direction + x1 = *cpx; // start point + y1 = *cpy; + if (rel) { // end point + x2 = *cpx + args[5]; + y2 = *cpy + args[6]; + } else { + x2 = args[5]; + y2 = args[6]; + } + + dx = x1 - x2; + dy = y1 - y2; + d = sqrtf(dx*dx + dy*dy); + if (d < 1e-6f || rx < 1e-6f || ry < 1e-6f) { + // The arc degenerates to a line + nsvg__lineTo(p, x2, y2); + *cpx = x2; + *cpy = y2; + return; + } + + sinrx = sinf(rotx); + cosrx = cosf(rotx); + + // Convert to center point parameterization. + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + // 1) Compute x1', y1' + x1p = cosrx * dx / 2.0f + sinrx * dy / 2.0f; + y1p = -sinrx * dx / 2.0f + cosrx * dy / 2.0f; + d = nsvg__sqr(x1p)/nsvg__sqr(rx) + nsvg__sqr(y1p)/nsvg__sqr(ry); + if (d > 1) { + d = sqrtf(d); + rx *= d; + ry *= d; + } + // 2) Compute cx', cy' + s = 0.0f; + sa = nsvg__sqr(rx)*nsvg__sqr(ry) - nsvg__sqr(rx)*nsvg__sqr(y1p) - nsvg__sqr(ry)*nsvg__sqr(x1p); + sb = nsvg__sqr(rx)*nsvg__sqr(y1p) + nsvg__sqr(ry)*nsvg__sqr(x1p); + if (sa < 0.0f) sa = 0.0f; + if (sb > 0.0f) + s = sqrtf(sa / sb); + if (fa == fs) + s = -s; + cxp = s * rx * y1p / ry; + cyp = s * -ry * x1p / rx; + + // 3) Compute cx,cy from cx',cy' + cx = (x1 + x2)/2.0f + cosrx*cxp - sinrx*cyp; + cy = (y1 + y2)/2.0f + sinrx*cxp + cosrx*cyp; + + // 4) Calculate theta1, and delta theta. + ux = (x1p - cxp) / rx; + uy = (y1p - cyp) / ry; + vx = (-x1p - cxp) / rx; + vy = (-y1p - cyp) / ry; + a1 = nsvg__vecang(1.0f,0.0f, ux,uy); // Initial angle + da = nsvg__vecang(ux,uy, vx,vy); // Delta angle + +// if (vecrat(ux,uy,vx,vy) <= -1.0f) da = NSVG_PI; +// if (vecrat(ux,uy,vx,vy) >= 1.0f) da = 0; + + if (fs == 0 && da > 0) + da -= 2 * NSVG_PI; + else if (fs == 1 && da < 0) + da += 2 * NSVG_PI; + + // Approximate the arc using cubic spline segments. + t[0] = cosrx; t[1] = sinrx; + t[2] = -sinrx; t[3] = cosrx; + t[4] = cx; t[5] = cy; + + // Split arc into max 90 degree segments. + // The loop assumes an iteration per end point (including start and end), this +1. + ndivs = (int)(fabsf(da) / (NSVG_PI*0.5f) + 1.0f); + hda = (da / (float)ndivs) / 2.0f; + // Fix for ticket #179: division by 0: avoid cotangens around 0 (infinite) + if ((hda < 1e-3f) && (hda > -1e-3f)) + hda *= 0.5f; + else + hda = (1.0f - cosf(hda)) / sinf(hda); + kappa = fabsf(4.0f / 3.0f * hda); + if (da < 0.0f) + kappa = -kappa; + + for (i = 0; i <= ndivs; i++) { + a = a1 + da * ((float)i/(float)ndivs); + dx = cosf(a); + dy = sinf(a); + nsvg__xformPoint(&x, &y, dx*rx, dy*ry, t); // position + nsvg__xformVec(&tanx, &tany, -dy*rx * kappa, dx*ry * kappa, t); // tangent + if (i > 0) + nsvg__cubicBezTo(p, px+ptanx,py+ptany, x-tanx, y-tany, x, y); + px = x; + py = y; + ptanx = tanx; + ptany = tany; + } + + *cpx = x2; + *cpy = y2; +} + +static void nsvg__parsePath(NSVGparser* p, const char** attr) +{ + const char* s = NULL; + char cmd = '\0'; + float args[10]; + int nargs; + int rargs = 0; + char initPoint; + float cpx, cpy, cpx2, cpy2; + const char* tmp[4]; + char closedFlag; + int i; + char item[64]; + + for (i = 0; attr[i]; i += 2) { + if (strcmp(attr[i], "d") == 0) { + s = attr[i + 1]; + } else { + tmp[0] = attr[i]; + tmp[1] = attr[i + 1]; + tmp[2] = 0; + tmp[3] = 0; + nsvg__parseAttribs(p, tmp); + } + } + + if (s) { + nsvg__resetPath(p); + cpx = 0; cpy = 0; + cpx2 = 0; cpy2 = 0; + initPoint = 0; + closedFlag = 0; + nargs = 0; + + while (*s) { + item[0] = '\0'; + if ((cmd == 'A' || cmd == 'a') && (nargs == 3 || nargs == 4)) + s = nsvg__getNextPathItemWhenArcFlag(s, item); + if (!*item) + s = nsvg__getNextPathItem(s, item); + if (!*item) break; + if (cmd != '\0' && nsvg__isCoordinate(item)) { + if (nargs < 10) + args[nargs++] = (float)nsvg__atof(item); + if (nargs >= rargs) { + switch (cmd) { + case 'm': + case 'M': + nsvg__pathMoveTo(p, &cpx, &cpy, args, cmd == 'm' ? 1 : 0); + // Moveto can be followed by multiple coordinate pairs, + // which should be treated as linetos. + cmd = (cmd == 'm') ? 'l' : 'L'; + rargs = nsvg__getArgsPerElement(cmd); + cpx2 = cpx; cpy2 = cpy; + initPoint = 1; + break; + case 'l': + case 'L': + nsvg__pathLineTo(p, &cpx, &cpy, args, cmd == 'l' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'H': + case 'h': + nsvg__pathHLineTo(p, &cpx, &cpy, args, cmd == 'h' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'V': + case 'v': + nsvg__pathVLineTo(p, &cpx, &cpy, args, cmd == 'v' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'C': + case 'c': + nsvg__pathCubicBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'c' ? 1 : 0); + break; + case 'S': + case 's': + nsvg__pathCubicBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 's' ? 1 : 0); + break; + case 'Q': + case 'q': + nsvg__pathQuadBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'q' ? 1 : 0); + break; + case 'T': + case 't': + nsvg__pathQuadBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 't' ? 1 : 0); + break; + case 'A': + case 'a': + nsvg__pathArcTo(p, &cpx, &cpy, args, cmd == 'a' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + default: + if (nargs >= 2) { + cpx = args[nargs-2]; + cpy = args[nargs-1]; + cpx2 = cpx; cpy2 = cpy; + } + break; + } + nargs = 0; + } + } else { + cmd = item[0]; + if (cmd == 'M' || cmd == 'm') { + // Commit path. + if (p->npts > 0) + nsvg__addPath(p, closedFlag); + // Start new subpath. + nsvg__resetPath(p); + closedFlag = 0; + nargs = 0; + } else if (initPoint == 0) { + // Do not allow other commands until initial point has been set (moveTo called once). + cmd = '\0'; + } + if (cmd == 'Z' || cmd == 'z') { + closedFlag = 1; + // Commit path. + if (p->npts > 0) { + // Move current point to first point + cpx = p->pts[0]; + cpy = p->pts[1]; + cpx2 = cpx; cpy2 = cpy; + nsvg__addPath(p, closedFlag); + } + // Start new subpath. + nsvg__resetPath(p); + nsvg__moveTo(p, cpx, cpy); + closedFlag = 0; + nargs = 0; + } + rargs = nsvg__getArgsPerElement(cmd); + if (rargs == -1) { + // Command not recognized + cmd = '\0'; + rargs = 0; + } + } + } + // Commit path. + if (p->npts) + nsvg__addPath(p, closedFlag); + } + + nsvg__addShape(p); +} + +static void nsvg__parseRect(NSVGparser* p, const char** attr) +{ + float x = 0.0f; + float y = 0.0f; + float w = 0.0f; + float h = 0.0f; + float rx = -1.0f; // marks not set + float ry = -1.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "x") == 0) x = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y") == 0) y = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "width") == 0) w = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p)); + if (strcmp(attr[i], "height") == 0) h = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p)); + if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); + if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); + } + } + + if (rx < 0.0f && ry > 0.0f) rx = ry; + if (ry < 0.0f && rx > 0.0f) ry = rx; + if (rx < 0.0f) rx = 0.0f; + if (ry < 0.0f) ry = 0.0f; + if (rx > w/2.0f) rx = w/2.0f; + if (ry > h/2.0f) ry = h/2.0f; + + if (w != 0.0f && h != 0.0f) { + nsvg__resetPath(p); + + if (rx < 0.00001f || ry < 0.0001f) { + nsvg__moveTo(p, x, y); + nsvg__lineTo(p, x+w, y); + nsvg__lineTo(p, x+w, y+h); + nsvg__lineTo(p, x, y+h); + } else { + // Rounded rectangle + nsvg__moveTo(p, x+rx, y); + nsvg__lineTo(p, x+w-rx, y); + nsvg__cubicBezTo(p, x+w-rx*(1-NSVG_KAPPA90), y, x+w, y+ry*(1-NSVG_KAPPA90), x+w, y+ry); + nsvg__lineTo(p, x+w, y+h-ry); + nsvg__cubicBezTo(p, x+w, y+h-ry*(1-NSVG_KAPPA90), x+w-rx*(1-NSVG_KAPPA90), y+h, x+w-rx, y+h); + nsvg__lineTo(p, x+rx, y+h); + nsvg__cubicBezTo(p, x+rx*(1-NSVG_KAPPA90), y+h, x, y+h-ry*(1-NSVG_KAPPA90), x, y+h-ry); + nsvg__lineTo(p, x, y+ry); + nsvg__cubicBezTo(p, x, y+ry*(1-NSVG_KAPPA90), x+rx*(1-NSVG_KAPPA90), y, x+rx, y); + } + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseCircle(NSVGparser* p, const char** attr) +{ + float cx = 0.0f; + float cy = 0.0f; + float r = 0.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "r") == 0) r = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualLength(p))); + } + } + + if (r > 0.0f) { + nsvg__resetPath(p); + + nsvg__moveTo(p, cx+r, cy); + nsvg__cubicBezTo(p, cx+r, cy+r*NSVG_KAPPA90, cx+r*NSVG_KAPPA90, cy+r, cx, cy+r); + nsvg__cubicBezTo(p, cx-r*NSVG_KAPPA90, cy+r, cx-r, cy+r*NSVG_KAPPA90, cx-r, cy); + nsvg__cubicBezTo(p, cx-r, cy-r*NSVG_KAPPA90, cx-r*NSVG_KAPPA90, cy-r, cx, cy-r); + nsvg__cubicBezTo(p, cx+r*NSVG_KAPPA90, cy-r, cx+r, cy-r*NSVG_KAPPA90, cx+r, cy); + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseEllipse(NSVGparser* p, const char** attr) +{ + float cx = 0.0f; + float cy = 0.0f; + float rx = 0.0f; + float ry = 0.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); + if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); + } + } + + if (rx > 0.0f && ry > 0.0f) { + + nsvg__resetPath(p); + + nsvg__moveTo(p, cx+rx, cy); + nsvg__cubicBezTo(p, cx+rx, cy+ry*NSVG_KAPPA90, cx+rx*NSVG_KAPPA90, cy+ry, cx, cy+ry); + nsvg__cubicBezTo(p, cx-rx*NSVG_KAPPA90, cy+ry, cx-rx, cy+ry*NSVG_KAPPA90, cx-rx, cy); + nsvg__cubicBezTo(p, cx-rx, cy-ry*NSVG_KAPPA90, cx-rx*NSVG_KAPPA90, cy-ry, cx, cy-ry); + nsvg__cubicBezTo(p, cx+rx*NSVG_KAPPA90, cy-ry, cx+rx, cy-ry*NSVG_KAPPA90, cx+rx, cy); + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseLine(NSVGparser* p, const char** attr) +{ + float x1 = 0.0; + float y1 = 0.0; + float x2 = 0.0; + float y2 = 0.0; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "x1") == 0) x1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y1") == 0) y1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "x2") == 0) x2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y2") == 0) y2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + } + } + + nsvg__resetPath(p); + + nsvg__moveTo(p, x1, y1); + nsvg__lineTo(p, x2, y2); + + nsvg__addPath(p, 0); + + nsvg__addShape(p); +} + +static void nsvg__parsePoly(NSVGparser* p, const char** attr, int closeFlag) +{ + int i; + const char* s; + float args[2]; + int nargs, npts = 0; + char item[64]; + + nsvg__resetPath(p); + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "points") == 0) { + s = attr[i + 1]; + nargs = 0; + while (*s) { + s = nsvg__getNextPathItem(s, item); + args[nargs++] = (float)nsvg__atof(item); + if (nargs >= 2) { + if (npts == 0) + nsvg__moveTo(p, args[0], args[1]); + else + nsvg__lineTo(p, args[0], args[1]); + nargs = 0; + npts++; + } + } + } + } + } + + nsvg__addPath(p, (char)closeFlag); + + nsvg__addShape(p); +} + +static void nsvg__parseSVG(NSVGparser* p, const char** attr) +{ + int i; + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "width") == 0) { + p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); + } else if (strcmp(attr[i], "height") == 0) { + p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); + } else if (strcmp(attr[i], "viewBox") == 0) { + const char *s = attr[i + 1]; + char buf[64]; + s = nsvg__parseNumber(s, buf, 64); + p->viewMinx = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewMiny = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewWidth = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewHeight = nsvg__atof(buf); + } else if (strcmp(attr[i], "preserveAspectRatio") == 0) { + if (strstr(attr[i + 1], "none") != 0) { + // No uniform scaling + p->alignType = NSVG_ALIGN_NONE; + } else { + // Parse X align + if (strstr(attr[i + 1], "xMin") != 0) + p->alignX = NSVG_ALIGN_MIN; + else if (strstr(attr[i + 1], "xMid") != 0) + p->alignX = NSVG_ALIGN_MID; + else if (strstr(attr[i + 1], "xMax") != 0) + p->alignX = NSVG_ALIGN_MAX; + // Parse X align + if (strstr(attr[i + 1], "yMin") != 0) + p->alignY = NSVG_ALIGN_MIN; + else if (strstr(attr[i + 1], "yMid") != 0) + p->alignY = NSVG_ALIGN_MID; + else if (strstr(attr[i + 1], "yMax") != 0) + p->alignY = NSVG_ALIGN_MAX; + // Parse meet/slice + p->alignType = NSVG_ALIGN_MEET; + if (strstr(attr[i + 1], "slice") != 0) + p->alignType = NSVG_ALIGN_SLICE; + } + } + } + } +} + +static void nsvg__parseGradient(NSVGparser* p, const char** attr, signed char type) +{ + int i; + NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); + if (grad == NULL) return; + memset(grad, 0, sizeof(NSVGgradientData)); + grad->units = NSVG_OBJECT_SPACE; + grad->type = type; + if (grad->type == NSVG_PAINT_LINEAR_GRADIENT) { + grad->linear.x1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + grad->linear.y1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + grad->linear.x2 = nsvg__coord(100.0f, NSVG_UNITS_PERCENT); + grad->linear.y2 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + } else if (grad->type == NSVG_PAINT_RADIAL_GRADIENT) { + grad->radial.cx = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + grad->radial.cy = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + grad->radial.r = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + } + + nsvg__xformIdentity(grad->xform); + + for (i = 0; attr[i]; i += 2) { + if (strcmp(attr[i], "id") == 0) { + strncpy(grad->id, attr[i+1], 63); + grad->id[63] = '\0'; + } else if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "gradientUnits") == 0) { + if (strcmp(attr[i+1], "objectBoundingBox") == 0) + grad->units = NSVG_OBJECT_SPACE; + else + grad->units = NSVG_USER_SPACE; + } else if (strcmp(attr[i], "gradientTransform") == 0) { + nsvg__parseTransform(grad->xform, attr[i + 1]); + } else if (strcmp(attr[i], "cx") == 0) { + grad->radial.cx = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "cy") == 0) { + grad->radial.cy = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "r") == 0) { + grad->radial.r = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "fx") == 0) { + grad->radial.fx = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "fy") == 0) { + grad->radial.fy = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "x1") == 0) { + grad->linear.x1 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "y1") == 0) { + grad->linear.y1 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "x2") == 0) { + grad->linear.x2 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "y2") == 0) { + grad->linear.y2 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "spreadMethod") == 0) { + if (strcmp(attr[i+1], "pad") == 0) + grad->spread = NSVG_SPREAD_PAD; + else if (strcmp(attr[i+1], "reflect") == 0) + grad->spread = NSVG_SPREAD_REFLECT; + else if (strcmp(attr[i+1], "repeat") == 0) + grad->spread = NSVG_SPREAD_REPEAT; + } else if (strcmp(attr[i], "xlink:href") == 0) { + const char *href = attr[i+1]; + strncpy(grad->ref, href+1, 62); + grad->ref[62] = '\0'; + } + } + } + + grad->next = p->gradients; + p->gradients = grad; +} + +static void nsvg__parseGradientStop(NSVGparser* p, const char** attr) +{ + NSVGattrib* curAttr = nsvg__getAttr(p); + NSVGgradientData* grad; + NSVGgradientStop* stop; + int i, idx; + + curAttr->stopOffset = 0; + curAttr->stopColor = 0; + curAttr->stopOpacity = 1.0f; + + for (i = 0; attr[i]; i += 2) { + nsvg__parseAttr(p, attr[i], attr[i + 1]); + } + + // Add stop to the last gradient. + grad = p->gradients; + if (grad == NULL) return; + + grad->nstops++; + grad->stops = (NSVGgradientStop*)realloc(grad->stops, sizeof(NSVGgradientStop)*grad->nstops); + if (grad->stops == NULL) return; + + // Insert + idx = grad->nstops-1; + for (i = 0; i < grad->nstops-1; i++) { + if (curAttr->stopOffset < grad->stops[i].offset) { + idx = i; + break; + } + } + if (idx != grad->nstops-1) { + for (i = grad->nstops-1; i > idx; i--) + grad->stops[i] = grad->stops[i-1]; + } + + stop = &grad->stops[idx]; + stop->color = curAttr->stopColor; + stop->color |= (unsigned int)(curAttr->stopOpacity*255) << 24; + stop->offset = curAttr->stopOffset; +} + +static void nsvg__startElement(void* ud, const char* el, const char** attr) +{ + NSVGparser* p = (NSVGparser*)ud; + + if (p->defsFlag) { + // Skip everything but gradients in defs + if (strcmp(el, "linearGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); + } else if (strcmp(el, "radialGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); + } else if (strcmp(el, "stop") == 0) { + nsvg__parseGradientStop(p, attr); + } + return; + } + + if (strcmp(el, "g") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + } else if (strcmp(el, "path") == 0) { + if (p->pathFlag) // Do not allow nested paths. + return; + nsvg__pushAttr(p); + nsvg__parsePath(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "rect") == 0) { + nsvg__pushAttr(p); + nsvg__parseRect(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "circle") == 0) { + nsvg__pushAttr(p); + nsvg__parseCircle(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "ellipse") == 0) { + nsvg__pushAttr(p); + nsvg__parseEllipse(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "line") == 0) { + nsvg__pushAttr(p); + nsvg__parseLine(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "polyline") == 0) { + nsvg__pushAttr(p); + nsvg__parsePoly(p, attr, 0); + nsvg__popAttr(p); + } else if (strcmp(el, "polygon") == 0) { + nsvg__pushAttr(p); + nsvg__parsePoly(p, attr, 1); + nsvg__popAttr(p); + } else if (strcmp(el, "linearGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); + } else if (strcmp(el, "radialGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); + } else if (strcmp(el, "stop") == 0) { + nsvg__parseGradientStop(p, attr); + } else if (strcmp(el, "defs") == 0) { + p->defsFlag = 1; + } else if (strcmp(el, "svg") == 0) { + nsvg__parseSVG(p, attr); + } +} + +static void nsvg__endElement(void* ud, const char* el) +{ + NSVGparser* p = (NSVGparser*)ud; + + if (strcmp(el, "g") == 0) { + nsvg__popAttr(p); + } else if (strcmp(el, "path") == 0) { + p->pathFlag = 0; + } else if (strcmp(el, "defs") == 0) { + p->defsFlag = 0; + } +} + +static void nsvg__content(void* ud, const char* s) +{ + NSVG_NOTUSED(ud); + NSVG_NOTUSED(s); + // empty +} + +static void nsvg__imageBounds(NSVGparser* p, float* bounds) +{ + NSVGshape* shape; + shape = p->image->shapes; + if (shape == NULL) { + bounds[0] = bounds[1] = bounds[2] = bounds[3] = 0.0; + return; + } + bounds[0] = shape->bounds[0]; + bounds[1] = shape->bounds[1]; + bounds[2] = shape->bounds[2]; + bounds[3] = shape->bounds[3]; + for (shape = shape->next; shape != NULL; shape = shape->next) { + bounds[0] = nsvg__minf(bounds[0], shape->bounds[0]); + bounds[1] = nsvg__minf(bounds[1], shape->bounds[1]); + bounds[2] = nsvg__maxf(bounds[2], shape->bounds[2]); + bounds[3] = nsvg__maxf(bounds[3], shape->bounds[3]); + } +} + +static float nsvg__viewAlign(float content, float container, int type) +{ + if (type == NSVG_ALIGN_MIN) + return 0; + else if (type == NSVG_ALIGN_MAX) + return container - content; + // mid + return (container - content) * 0.5f; +} + +static void nsvg__scaleGradient(NSVGgradient* grad, float tx, float ty, float sx, float sy) +{ + float t[6]; + nsvg__xformSetTranslation(t, tx, ty); + nsvg__xformMultiply (grad->xform, t); + + nsvg__xformSetScale(t, sx, sy); + nsvg__xformMultiply (grad->xform, t); +} + +static void nsvg__scaleToViewbox(NSVGparser* p, const char* units) +{ + NSVGshape* shape; + NSVGpath* path; + float tx, ty, sx, sy, us, bounds[4], t[6], avgs; + int i; + float* pt; + + // Guess image size if not set completely. + nsvg__imageBounds(p, bounds); + + if (p->viewWidth == 0) { + if (p->image->width > 0) { + p->viewWidth = p->image->width; + } else { + p->viewMinx = bounds[0]; + p->viewWidth = bounds[2] - bounds[0]; + } + } + if (p->viewHeight == 0) { + if (p->image->height > 0) { + p->viewHeight = p->image->height; + } else { + p->viewMiny = bounds[1]; + p->viewHeight = bounds[3] - bounds[1]; + } + } + if (p->image->width == 0) + p->image->width = p->viewWidth; + if (p->image->height == 0) + p->image->height = p->viewHeight; + + tx = -p->viewMinx; + ty = -p->viewMiny; + sx = p->viewWidth > 0 ? p->image->width / p->viewWidth : 0; + sy = p->viewHeight > 0 ? p->image->height / p->viewHeight : 0; + // Unit scaling + us = 1.0f / nsvg__convertToPixels(p, nsvg__coord(1.0f, nsvg__parseUnits(units)), 0.0f, 1.0f); + + // Fix aspect ratio + if (p->alignType == NSVG_ALIGN_MEET) { + // fit whole image into viewbox + sx = sy = nsvg__minf(sx, sy); + tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; + ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; + } else if (p->alignType == NSVG_ALIGN_SLICE) { + // fill whole viewbox with image + sx = sy = nsvg__maxf(sx, sy); + tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; + ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; + } + + // Transform + sx *= us; + sy *= us; + avgs = (sx+sy) / 2.0f; + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + shape->bounds[0] = (shape->bounds[0] + tx) * sx; + shape->bounds[1] = (shape->bounds[1] + ty) * sy; + shape->bounds[2] = (shape->bounds[2] + tx) * sx; + shape->bounds[3] = (shape->bounds[3] + ty) * sy; + for (path = shape->paths; path != NULL; path = path->next) { + path->bounds[0] = (path->bounds[0] + tx) * sx; + path->bounds[1] = (path->bounds[1] + ty) * sy; + path->bounds[2] = (path->bounds[2] + tx) * sx; + path->bounds[3] = (path->bounds[3] + ty) * sy; + for (i =0; i < path->npts; i++) { + pt = &path->pts[i*2]; + pt[0] = (pt[0] + tx) * sx; + pt[1] = (pt[1] + ty) * sy; + } + } + + if (shape->fill.type == NSVG_PAINT_LINEAR_GRADIENT || shape->fill.type == NSVG_PAINT_RADIAL_GRADIENT) { + nsvg__scaleGradient(shape->fill.gradient, tx,ty, sx,sy); + memcpy(t, shape->fill.gradient->xform, sizeof(float)*6); + nsvg__xformInverse(shape->fill.gradient->xform, t); + } + if (shape->stroke.type == NSVG_PAINT_LINEAR_GRADIENT || shape->stroke.type == NSVG_PAINT_RADIAL_GRADIENT) { + nsvg__scaleGradient(shape->stroke.gradient, tx,ty, sx,sy); + memcpy(t, shape->stroke.gradient->xform, sizeof(float)*6); + nsvg__xformInverse(shape->stroke.gradient->xform, t); + } + + shape->strokeWidth *= avgs; + shape->strokeDashOffset *= avgs; + for (i = 0; i < shape->strokeDashCount; i++) + shape->strokeDashArray[i] *= avgs; + } +} + +static void nsvg__createGradients(NSVGparser* p) +{ + NSVGshape* shape; + + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + if (shape->fill.type == NSVG_PAINT_UNDEF) { + if (shape->fillGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->fill.gradient = nsvg__createGradient(p, shape->fillGradient, localBounds, shape->xform, &shape->fill.type); + } + if (shape->fill.type == NSVG_PAINT_UNDEF) { + shape->fill.type = NSVG_PAINT_NONE; + } + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + if (shape->strokeGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->stroke.gradient = nsvg__createGradient(p, shape->strokeGradient, localBounds, shape->xform, &shape->stroke.type); + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + shape->stroke.type = NSVG_PAINT_NONE; + } + } + } +} + +NSVGimage* nsvgParse(char* input, const char* units, float dpi) +{ + NSVGparser* p; + NSVGimage* ret = 0; + + p = nsvg__createParser(); + if (p == NULL) { + return NULL; + } + p->dpi = dpi; + + nsvg__parseXML(input, nsvg__startElement, nsvg__endElement, nsvg__content, p); + + // Create gradients after all definitions have been parsed + nsvg__createGradients(p); + + // Scale to viewBox + nsvg__scaleToViewbox(p, units); + + ret = p->image; + p->image = NULL; + + nsvg__deleteParser(p); + + return ret; +} + +NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) +{ + FILE* fp = NULL; + size_t size; + char* data = NULL; + NSVGimage* image = NULL; + + fp = fopen(filename, "rb"); + if (!fp) goto error; + fseek(fp, 0, SEEK_END); + size = ftell(fp); + fseek(fp, 0, SEEK_SET); + data = (char*)malloc(size+1); + if (data == NULL) goto error; + if (fread(data, 1, size, fp) != size) goto error; + data[size] = '\0'; // Must be null terminated. + fclose(fp); + image = nsvgParse(data, units, dpi); + free(data); + + return image; + +error: + if (fp) fclose(fp); + if (data) free(data); + if (image) nsvgDelete(image); + return NULL; +} + +NSVGpath* nsvgDuplicatePath(NSVGpath* p) +{ + NSVGpath* res = NULL; + + if (p == NULL) + return NULL; + + res = (NSVGpath*)malloc(sizeof(NSVGpath)); + if (res == NULL) goto error; + memset(res, 0, sizeof(NSVGpath)); + + res->pts = (float*)malloc(p->npts*2*sizeof(float)); + if (res->pts == NULL) goto error; + memcpy(res->pts, p->pts, p->npts * sizeof(float) * 2); + res->npts = p->npts; + + memcpy(res->bounds, p->bounds, sizeof(p->bounds)); + + res->closed = p->closed; + + return res; + +error: + if (res != NULL) { + free(res->pts); + free(res); + } + return NULL; +} + +void nsvgDelete(NSVGimage* image) +{ + NSVGshape *snext, *shape; + if (image == NULL) return; + shape = image->shapes; + while (shape != NULL) { + snext = shape->next; + nsvg__deletePaths(shape->paths); + nsvg__deletePaint(&shape->fill); + nsvg__deletePaint(&shape->stroke); + free(shape); + shape = snext; + } + free(image); +} + +#endif // NANOSVG_IMPLEMENTATION + +#endif // NANOSVG_H diff --git a/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h b/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h new file mode 100644 index 0000000000..89fae3834b --- /dev/null +++ b/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h @@ -0,0 +1,1459 @@ +/* + * Copyright (c) 2013-14 Mikko Mononen memon@inside.org + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * The polygon rasterization is heavily based on stb_truetype rasterizer + * by Sean Barrett - http://nothings.org/ + * + */ + +#ifndef NANOSVGRAST_H +#define NANOSVGRAST_H + +#include "nanosvg.h" + +#ifndef NANOSVGRAST_CPLUSPLUS +#ifdef __cplusplus +extern "C" { +#endif +#endif + +typedef struct NSVGrasterizer NSVGrasterizer; + +/* Example Usage: + // Load SVG + NSVGimage* image; + image = nsvgParseFromFile("test.svg", "px", 96); + + // Create rasterizer (can be used to render multiple images). + struct NSVGrasterizer* rast = nsvgCreateRasterizer(); + // Allocate memory for image + unsigned char* img = malloc(w*h*4); + // Rasterize + nsvgRasterize(rast, image, 0,0,1, img, w, h, w*4); +*/ + +// Allocated rasterizer context. +NSVGrasterizer* nsvgCreateRasterizer(void); + +// Rasterizes SVG image, returns RGBA image (non-premultiplied alpha) +// r - pointer to rasterizer context +// image - pointer to image to rasterize +// tx,ty - image offset (applied after scaling) +// scale - image scale +// dst - pointer to destination image data, 4 bytes per pixel (RGBA) +// w - width of the image to render +// h - height of the image to render +// stride - number of bytes per scaleline in the destination buffer +void nsvgRasterize(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride); + +// Deletes rasterizer context. +void nsvgDeleteRasterizer(NSVGrasterizer*); + + +#ifndef NANOSVGRAST_CPLUSPLUS +#ifdef __cplusplus +} +#endif +#endif + +#ifdef NANOSVGRAST_IMPLEMENTATION + +#include +#include +#include + +#define NSVG__SUBSAMPLES 5 +#define NSVG__FIXSHIFT 10 +#define NSVG__FIX (1 << NSVG__FIXSHIFT) +#define NSVG__FIXMASK (NSVG__FIX-1) +#define NSVG__MEMPAGE_SIZE 1024 + +typedef struct NSVGedge { + float x0,y0, x1,y1; + int dir; + struct NSVGedge* next; +} NSVGedge; + +typedef struct NSVGpoint { + float x, y; + float dx, dy; + float len; + float dmx, dmy; + unsigned char flags; +} NSVGpoint; + +typedef struct NSVGactiveEdge { + int x,dx; + float ey; + int dir; + struct NSVGactiveEdge *next; +} NSVGactiveEdge; + +typedef struct NSVGmemPage { + unsigned char mem[NSVG__MEMPAGE_SIZE]; + int size; + struct NSVGmemPage* next; +} NSVGmemPage; + +typedef struct NSVGcachedPaint { + signed char type; + char spread; + float xform[6]; + unsigned int colors[256]; +} NSVGcachedPaint; + +struct NSVGrasterizer +{ + float px, py; + + float tessTol; + float distTol; + + NSVGedge* edges; + int nedges; + int cedges; + + NSVGpoint* points; + int npoints; + int cpoints; + + NSVGpoint* points2; + int npoints2; + int cpoints2; + + NSVGactiveEdge* freelist; + NSVGmemPage* pages; + NSVGmemPage* curpage; + + unsigned char* scanline; + int cscanline; + + unsigned char* bitmap; + int width, height, stride; +}; + +NSVGrasterizer* nsvgCreateRasterizer(void) +{ + NSVGrasterizer* r = (NSVGrasterizer*)malloc(sizeof(NSVGrasterizer)); + if (r == NULL) goto error; + memset(r, 0, sizeof(NSVGrasterizer)); + + r->tessTol = 0.25f; + r->distTol = 0.01f; + + return r; + +error: + nsvgDeleteRasterizer(r); + return NULL; +} + +void nsvgDeleteRasterizer(NSVGrasterizer* r) +{ + NSVGmemPage* p; + + if (r == NULL) return; + + p = r->pages; + while (p != NULL) { + NSVGmemPage* next = p->next; + free(p); + p = next; + } + + if (r->edges) free(r->edges); + if (r->points) free(r->points); + if (r->points2) free(r->points2); + if (r->scanline) free(r->scanline); + + free(r); +} + +static NSVGmemPage* nsvg__nextPage(NSVGrasterizer* r, NSVGmemPage* cur) +{ + NSVGmemPage *newp; + + // If using existing chain, return the next page in chain + if (cur != NULL && cur->next != NULL) { + return cur->next; + } + + // Alloc new page + newp = (NSVGmemPage*)malloc(sizeof(NSVGmemPage)); + if (newp == NULL) return NULL; + memset(newp, 0, sizeof(NSVGmemPage)); + + // Add to linked list + if (cur != NULL) + cur->next = newp; + else + r->pages = newp; + + return newp; +} + +static void nsvg__resetPool(NSVGrasterizer* r) +{ + NSVGmemPage* p = r->pages; + while (p != NULL) { + p->size = 0; + p = p->next; + } + r->curpage = r->pages; +} + +static unsigned char* nsvg__alloc(NSVGrasterizer* r, int size) +{ + unsigned char* buf; + if (size > NSVG__MEMPAGE_SIZE) return NULL; + if (r->curpage == NULL || r->curpage->size+size > NSVG__MEMPAGE_SIZE) { + r->curpage = nsvg__nextPage(r, r->curpage); + } + buf = &r->curpage->mem[r->curpage->size]; + r->curpage->size += size; + return buf; +} + +static int nsvg__ptEquals(float x1, float y1, float x2, float y2, float tol) +{ + float dx = x2 - x1; + float dy = y2 - y1; + return dx*dx + dy*dy < tol*tol; +} + +static void nsvg__addPathPoint(NSVGrasterizer* r, float x, float y, int flags) +{ + NSVGpoint* pt; + + if (r->npoints > 0) { + pt = &r->points[r->npoints-1]; + if (nsvg__ptEquals(pt->x,pt->y, x,y, r->distTol)) { + pt->flags = (unsigned char)(pt->flags | flags); + return; + } + } + + if (r->npoints+1 > r->cpoints) { + r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; + r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); + if (r->points == NULL) return; + } + + pt = &r->points[r->npoints]; + pt->x = x; + pt->y = y; + pt->flags = (unsigned char)flags; + r->npoints++; +} + +static void nsvg__appendPathPoint(NSVGrasterizer* r, NSVGpoint pt) +{ + if (r->npoints+1 > r->cpoints) { + r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; + r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); + if (r->points == NULL) return; + } + r->points[r->npoints] = pt; + r->npoints++; +} + +static void nsvg__duplicatePoints(NSVGrasterizer* r) +{ + if (r->npoints > r->cpoints2) { + r->cpoints2 = r->npoints; + r->points2 = (NSVGpoint*)realloc(r->points2, sizeof(NSVGpoint) * r->cpoints2); + if (r->points2 == NULL) return; + } + + memcpy(r->points2, r->points, sizeof(NSVGpoint) * r->npoints); + r->npoints2 = r->npoints; +} + +static void nsvg__addEdge(NSVGrasterizer* r, float x0, float y0, float x1, float y1) +{ + NSVGedge* e; + + // Skip horizontal edges + if (y0 == y1) + return; + + if (r->nedges+1 > r->cedges) { + r->cedges = r->cedges > 0 ? r->cedges * 2 : 64; + r->edges = (NSVGedge*)realloc(r->edges, sizeof(NSVGedge) * r->cedges); + if (r->edges == NULL) return; + } + + e = &r->edges[r->nedges]; + r->nedges++; + + if (y0 < y1) { + e->x0 = x0; + e->y0 = y0; + e->x1 = x1; + e->y1 = y1; + e->dir = 1; + } else { + e->x0 = x1; + e->y0 = y1; + e->x1 = x0; + e->y1 = y0; + e->dir = -1; + } +} + +static float nsvg__normalize(float *x, float* y) +{ + float d = sqrtf((*x)*(*x) + (*y)*(*y)); + if (d > 1e-6f) { + float id = 1.0f / d; + *x *= id; + *y *= id; + } + return d; +} + +static float nsvg__absf(float x) { return x < 0 ? -x : x; } +static float nsvg__roundf(float x) { return (x >= 0) ? floorf(x + 0.5) : ceilf(x - 0.5); } + +static void nsvg__flattenCubicBez(NSVGrasterizer* r, + float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4, + int level, int type) +{ + float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; + float dx,dy,d2,d3; + + if (level > 10) return; + + x12 = (x1+x2)*0.5f; + y12 = (y1+y2)*0.5f; + x23 = (x2+x3)*0.5f; + y23 = (y2+y3)*0.5f; + x34 = (x3+x4)*0.5f; + y34 = (y3+y4)*0.5f; + x123 = (x12+x23)*0.5f; + y123 = (y12+y23)*0.5f; + + dx = x4 - x1; + dy = y4 - y1; + d2 = nsvg__absf(((x2 - x4) * dy - (y2 - y4) * dx)); + d3 = nsvg__absf(((x3 - x4) * dy - (y3 - y4) * dx)); + + if ((d2 + d3)*(d2 + d3) < r->tessTol * (dx*dx + dy*dy)) { + nsvg__addPathPoint(r, x4, y4, type); + return; + } + + x234 = (x23+x34)*0.5f; + y234 = (y23+y34)*0.5f; + x1234 = (x123+x234)*0.5f; + y1234 = (y123+y234)*0.5f; + + nsvg__flattenCubicBez(r, x1,y1, x12,y12, x123,y123, x1234,y1234, level+1, 0); + nsvg__flattenCubicBez(r, x1234,y1234, x234,y234, x34,y34, x4,y4, level+1, type); +} + +static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float scale) +{ + int i, j; + NSVGpath* path; + + for (path = shape->paths; path != NULL; path = path->next) { + r->npoints = 0; + // Flatten path + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); + for (i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, 0); + } + // Close path + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); + // Build edges + for (i = 0, j = r->npoints-1; i < r->npoints; j = i++) + nsvg__addEdge(r, r->points[j].x, r->points[j].y, r->points[i].x, r->points[i].y); + } +} + +enum NSVGpointFlags +{ + NSVG_PT_CORNER = 0x01, + NSVG_PT_BEVEL = 0x02, + NSVG_PT_LEFT = 0x04 +}; + +static void nsvg__initClosed(NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + float len = nsvg__normalize(&dx, &dy); + float px = p0->x + dx*len*0.5f, py = p0->y + dy*len*0.5f; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__buttCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) +{ + float w = lineWidth * 0.5f; + float px = p->x, py = p->y; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + + nsvg__addEdge(r, lx, ly, rx, ry); + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__squareCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) +{ + float w = lineWidth * 0.5f; + float px = p->x - dx*w, py = p->y - dy*w; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + + nsvg__addEdge(r, lx, ly, rx, ry); + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +#ifndef NSVG_PI +#define NSVG_PI (3.14159265358979323846264338327f) +#endif + +static void nsvg__roundCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int ncap, int connect) +{ + int i; + float w = lineWidth * 0.5f; + float px = p->x, py = p->y; + float dlx = dy, dly = -dx; + float lx = 0, ly = 0, rx = 0, ry = 0, prevx = 0, prevy = 0; + + for (i = 0; i < ncap; i++) { + float a = (float)i/(float)(ncap-1)*NSVG_PI; + float ax = cosf(a) * w, ay = sinf(a) * w; + float x = px - dlx*ax - dx*ay; + float y = py - dly*ax - dy*ay; + + if (i > 0) + nsvg__addEdge(r, prevx, prevy, x, y); + + prevx = x; + prevy = y; + + if (i == 0) { + lx = x; ly = y; + } else if (i == ncap-1) { + rx = x; ry = y; + } + } + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__bevelJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float lx0 = p1->x - (dlx0 * w), ly0 = p1->y - (dly0 * w); + float rx0 = p1->x + (dlx0 * w), ry0 = p1->y + (dly0 * w); + float lx1 = p1->x - (dlx1 * w), ly1 = p1->y - (dly1 * w); + float rx1 = p1->x + (dlx1 * w), ry1 = p1->y + (dly1 * w); + + nsvg__addEdge(r, lx0, ly0, left->x, left->y); + nsvg__addEdge(r, lx1, ly1, lx0, ly0); + + nsvg__addEdge(r, right->x, right->y, rx0, ry0); + nsvg__addEdge(r, rx0, ry0, rx1, ry1); + + left->x = lx1; left->y = ly1; + right->x = rx1; right->y = ry1; +} + +static void nsvg__miterJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float lx0, rx0, lx1, rx1; + float ly0, ry0, ly1, ry1; + + if (p1->flags & NSVG_PT_LEFT) { + lx0 = lx1 = p1->x - p1->dmx * w; + ly0 = ly1 = p1->y - p1->dmy * w; + nsvg__addEdge(r, lx1, ly1, left->x, left->y); + + rx0 = p1->x + (dlx0 * w); + ry0 = p1->y + (dly0 * w); + rx1 = p1->x + (dlx1 * w); + ry1 = p1->y + (dly1 * w); + nsvg__addEdge(r, right->x, right->y, rx0, ry0); + nsvg__addEdge(r, rx0, ry0, rx1, ry1); + } else { + lx0 = p1->x - (dlx0 * w); + ly0 = p1->y - (dly0 * w); + lx1 = p1->x - (dlx1 * w); + ly1 = p1->y - (dly1 * w); + nsvg__addEdge(r, lx0, ly0, left->x, left->y); + nsvg__addEdge(r, lx1, ly1, lx0, ly0); + + rx0 = rx1 = p1->x + p1->dmx * w; + ry0 = ry1 = p1->y + p1->dmy * w; + nsvg__addEdge(r, right->x, right->y, rx1, ry1); + } + + left->x = lx1; left->y = ly1; + right->x = rx1; right->y = ry1; +} + +static void nsvg__roundJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth, int ncap) +{ + int i, n; + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float a0 = atan2f(dly0, dlx0); + float a1 = atan2f(dly1, dlx1); + float da = a1 - a0; + float lx, ly, rx, ry; + + if (da < NSVG_PI) da += NSVG_PI*2; + if (da > NSVG_PI) da -= NSVG_PI*2; + + n = (int)ceilf((nsvg__absf(da) / NSVG_PI) * (float)ncap); + if (n < 2) n = 2; + if (n > ncap) n = ncap; + + lx = left->x; + ly = left->y; + rx = right->x; + ry = right->y; + + for (i = 0; i < n; i++) { + float u = (float)i/(float)(n-1); + float a = a0 + u*da; + float ax = cosf(a) * w, ay = sinf(a) * w; + float lx1 = p1->x - ax, ly1 = p1->y - ay; + float rx1 = p1->x + ax, ry1 = p1->y + ay; + + nsvg__addEdge(r, lx1, ly1, lx, ly); + nsvg__addEdge(r, rx, ry, rx1, ry1); + + lx = lx1; ly = ly1; + rx = rx1; ry = ry1; + } + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__straightJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float lx = p1->x - (p1->dmx * w), ly = p1->y - (p1->dmy * w); + float rx = p1->x + (p1->dmx * w), ry = p1->y + (p1->dmy * w); + + nsvg__addEdge(r, lx, ly, left->x, left->y); + nsvg__addEdge(r, right->x, right->y, rx, ry); + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static int nsvg__curveDivs(float r, float arc, float tol) +{ + float da = acosf(r / (r + tol)) * 2.0f; + int divs = (int)ceilf(arc / da); + if (divs < 2) divs = 2; + return divs; +} + +static void nsvg__expandStroke(NSVGrasterizer* r, NSVGpoint* points, int npoints, int closed, int lineJoin, int lineCap, float lineWidth) +{ + int ncap = nsvg__curveDivs(lineWidth*0.5f, NSVG_PI, r->tessTol); // Calculate divisions per half circle. + NSVGpoint left = {0,0,0,0,0,0,0,0}, right = {0,0,0,0,0,0,0,0}, firstLeft = {0,0,0,0,0,0,0,0}, firstRight = {0,0,0,0,0,0,0,0}; + NSVGpoint* p0, *p1; + int j, s, e; + + // Build stroke edges + if (closed) { + // Looping + p0 = &points[npoints-1]; + p1 = &points[0]; + s = 0; + e = npoints; + } else { + // Add cap + p0 = &points[0]; + p1 = &points[1]; + s = 1; + e = npoints-1; + } + + if (closed) { + nsvg__initClosed(&left, &right, p0, p1, lineWidth); + firstLeft = left; + firstRight = right; + } else { + // Add cap + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + nsvg__normalize(&dx, &dy); + if (lineCap == NSVG_CAP_BUTT) + nsvg__buttCap(r, &left, &right, p0, dx, dy, lineWidth, 0); + else if (lineCap == NSVG_CAP_SQUARE) + nsvg__squareCap(r, &left, &right, p0, dx, dy, lineWidth, 0); + else if (lineCap == NSVG_CAP_ROUND) + nsvg__roundCap(r, &left, &right, p0, dx, dy, lineWidth, ncap, 0); + } + + for (j = s; j < e; ++j) { + if (p1->flags & NSVG_PT_CORNER) { + if (lineJoin == NSVG_JOIN_ROUND) + nsvg__roundJoin(r, &left, &right, p0, p1, lineWidth, ncap); + else if (lineJoin == NSVG_JOIN_BEVEL || (p1->flags & NSVG_PT_BEVEL)) + nsvg__bevelJoin(r, &left, &right, p0, p1, lineWidth); + else + nsvg__miterJoin(r, &left, &right, p0, p1, lineWidth); + } else { + nsvg__straightJoin(r, &left, &right, p1, lineWidth); + } + p0 = p1++; + } + + if (closed) { + // Loop it + nsvg__addEdge(r, firstLeft.x, firstLeft.y, left.x, left.y); + nsvg__addEdge(r, right.x, right.y, firstRight.x, firstRight.y); + } else { + // Add cap + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + nsvg__normalize(&dx, &dy); + if (lineCap == NSVG_CAP_BUTT) + nsvg__buttCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); + else if (lineCap == NSVG_CAP_SQUARE) + nsvg__squareCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); + else if (lineCap == NSVG_CAP_ROUND) + nsvg__roundCap(r, &right, &left, p1, -dx, -dy, lineWidth, ncap, 1); + } +} + +static void nsvg__prepareStroke(NSVGrasterizer* r, float miterLimit, int lineJoin) +{ + int i, j; + NSVGpoint* p0, *p1; + + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + for (i = 0; i < r->npoints; i++) { + // Calculate segment direction and length + p0->dx = p1->x - p0->x; + p0->dy = p1->y - p0->y; + p0->len = nsvg__normalize(&p0->dx, &p0->dy); + // Advance + p0 = p1++; + } + + // calculate joins + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + for (j = 0; j < r->npoints; j++) { + float dlx0, dly0, dlx1, dly1, dmr2, cross; + dlx0 = p0->dy; + dly0 = -p0->dx; + dlx1 = p1->dy; + dly1 = -p1->dx; + // Calculate extrusions + p1->dmx = (dlx0 + dlx1) * 0.5f; + p1->dmy = (dly0 + dly1) * 0.5f; + dmr2 = p1->dmx*p1->dmx + p1->dmy*p1->dmy; + if (dmr2 > 0.000001f) { + float s2 = 1.0f / dmr2; + if (s2 > 600.0f) { + s2 = 600.0f; + } + p1->dmx *= s2; + p1->dmy *= s2; + } + + // Clear flags, but keep the corner. + p1->flags = (p1->flags & NSVG_PT_CORNER) ? NSVG_PT_CORNER : 0; + + // Keep track of left turns. + cross = p1->dx * p0->dy - p0->dx * p1->dy; + if (cross > 0.0f) + p1->flags |= NSVG_PT_LEFT; + + // Check to see if the corner needs to be beveled. + if (p1->flags & NSVG_PT_CORNER) { + if ((dmr2 * miterLimit*miterLimit) < 1.0f || lineJoin == NSVG_JOIN_BEVEL || lineJoin == NSVG_JOIN_ROUND) { + p1->flags |= NSVG_PT_BEVEL; + } + } + + p0 = p1++; + } +} + +static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float scale) +{ + int i, j, closed; + NSVGpath* path; + NSVGpoint* p0, *p1; + float miterLimit = shape->miterLimit; + int lineJoin = shape->strokeLineJoin; + int lineCap = shape->strokeLineCap; + float lineWidth = shape->strokeWidth * scale; + + for (path = shape->paths; path != NULL; path = path->next) { + // Flatten path + r->npoints = 0; + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, NSVG_PT_CORNER); + for (i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, NSVG_PT_CORNER); + } + if (r->npoints < 2) + continue; + + closed = path->closed; + + // If the first and last points are the same, remove the last, mark as closed path. + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + if (nsvg__ptEquals(p0->x,p0->y, p1->x,p1->y, r->distTol)) { + r->npoints--; + p0 = &r->points[r->npoints-1]; + closed = 1; + } + + if (shape->strokeDashCount > 0) { + int idash = 0, dashState = 1; + float totalDist = 0, dashLen, allDashLen, dashOffset; + NSVGpoint cur; + + if (closed) + nsvg__appendPathPoint(r, r->points[0]); + + // Duplicate points -> points2. + nsvg__duplicatePoints(r); + + r->npoints = 0; + cur = r->points2[0]; + nsvg__appendPathPoint(r, cur); + + // Figure out dash offset. + allDashLen = 0; + for (j = 0; j < shape->strokeDashCount; j++) + allDashLen += shape->strokeDashArray[j]; + if (shape->strokeDashCount & 1) + allDashLen *= 2.0f; + // Find location inside pattern + dashOffset = fmodf(shape->strokeDashOffset, allDashLen); + if (dashOffset < 0.0f) + dashOffset += allDashLen; + + while (dashOffset > shape->strokeDashArray[idash]) { + dashOffset -= shape->strokeDashArray[idash]; + idash = (idash + 1) % shape->strokeDashCount; + } + dashLen = (shape->strokeDashArray[idash] - dashOffset) * scale; + + for (j = 1; j < r->npoints2; ) { + float dx = r->points2[j].x - cur.x; + float dy = r->points2[j].y - cur.y; + float dist = sqrtf(dx*dx + dy*dy); + + if ((totalDist + dist) > dashLen) { + // Calculate intermediate point + float d = (dashLen - totalDist) / dist; + float x = cur.x + dx * d; + float y = cur.y + dy * d; + nsvg__addPathPoint(r, x, y, NSVG_PT_CORNER); + + // Stroke + if (r->npoints > 1 && dashState) { + nsvg__prepareStroke(r, miterLimit, lineJoin); + nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); + } + // Advance dash pattern + dashState = !dashState; + idash = (idash+1) % shape->strokeDashCount; + dashLen = shape->strokeDashArray[idash] * scale; + // Restart + cur.x = x; + cur.y = y; + cur.flags = NSVG_PT_CORNER; + totalDist = 0.0f; + r->npoints = 0; + nsvg__appendPathPoint(r, cur); + } else { + totalDist += dist; + cur = r->points2[j]; + nsvg__appendPathPoint(r, cur); + j++; + } + } + // Stroke any leftover path + if (r->npoints > 1 && dashState) + nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); + } else { + nsvg__prepareStroke(r, miterLimit, lineJoin); + nsvg__expandStroke(r, r->points, r->npoints, closed, lineJoin, lineCap, lineWidth); + } + } +} + +static int nsvg__cmpEdge(const void *p, const void *q) +{ + const NSVGedge* a = (const NSVGedge*)p; + const NSVGedge* b = (const NSVGedge*)q; + + if (a->y0 < b->y0) return -1; + if (a->y0 > b->y0) return 1; + return 0; +} + + +static NSVGactiveEdge* nsvg__addActive(NSVGrasterizer* r, NSVGedge* e, float startPoint) +{ + NSVGactiveEdge* z; + + if (r->freelist != NULL) { + // Restore from freelist. + z = r->freelist; + r->freelist = z->next; + } else { + // Alloc new edge. + z = (NSVGactiveEdge*)nsvg__alloc(r, sizeof(NSVGactiveEdge)); + if (z == NULL) return NULL; + } + + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); +// STBTT_assert(e->y0 <= start_point); + // round dx down to avoid going too far + if (dxdy < 0) + z->dx = (int)(-nsvg__roundf(NSVG__FIX * -dxdy)); + else + z->dx = (int)nsvg__roundf(NSVG__FIX * dxdy); + z->x = (int)nsvg__roundf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); +// z->x -= off_x * FIX; + z->ey = e->y1; + z->next = 0; + z->dir = e->dir; + + return z; +} + +static void nsvg__freeActive(NSVGrasterizer* r, NSVGactiveEdge* z) +{ + z->next = r->freelist; + r->freelist = z; +} + +static void nsvg__fillScanline(unsigned char* scanline, int len, int x0, int x1, int maxWeight, int* xmin, int* xmax) +{ + int i = x0 >> NSVG__FIXSHIFT; + int j = x1 >> NSVG__FIXSHIFT; + if (i < *xmin) *xmin = i; + if (j > *xmax) *xmax = j; + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = (unsigned char)(scanline[i] + ((x1 - x0) * maxWeight >> NSVG__FIXSHIFT)); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = (unsigned char)(scanline[i] + (((NSVG__FIX - (x0 & NSVG__FIXMASK)) * maxWeight) >> NSVG__FIXSHIFT)); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = (unsigned char)(scanline[j] + (((x1 & NSVG__FIXMASK) * maxWeight) >> NSVG__FIXSHIFT)); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = (unsigned char)(scanline[i] + maxWeight); + } + } +} + +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void nsvg__fillActiveEdges(unsigned char* scanline, int len, NSVGactiveEdge* e, int maxWeight, int* xmin, int* xmax, char fillRule) +{ + // non-zero winding fill + int x0 = 0, w = 0; + + if (fillRule == NSVG_FILLRULE_NONZERO) { + // Non-zero + while (e != NULL) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->dir; + } else { + int x1 = e->x; w += e->dir; + // if we went to zero, we need to draw + if (w == 0) + nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); + } + e = e->next; + } + } else if (fillRule == NSVG_FILLRULE_EVENODD) { + // Even-odd + while (e != NULL) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w = 1; + } else { + int x1 = e->x; w = 0; + nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); + } + e = e->next; + } + } +} + +static float nsvg__clampf(float a, float mn, float mx) { return a < mn ? mn : (a > mx ? mx : a); } + +static unsigned int nsvg__RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) +{ + return ((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16) | ((unsigned int)a << 24); +} + +static unsigned int nsvg__lerpRGBA(unsigned int c0, unsigned int c1, float u) +{ + int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); + int r = (((c0) & 0xff)*(256-iu) + (((c1) & 0xff)*iu)) >> 8; + int g = (((c0>>8) & 0xff)*(256-iu) + (((c1>>8) & 0xff)*iu)) >> 8; + int b = (((c0>>16) & 0xff)*(256-iu) + (((c1>>16) & 0xff)*iu)) >> 8; + int a = (((c0>>24) & 0xff)*(256-iu) + (((c1>>24) & 0xff)*iu)) >> 8; + return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); +} + +static unsigned int nsvg__applyOpacity(unsigned int c, float u) +{ + int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); + int r = (c) & 0xff; + int g = (c>>8) & 0xff; + int b = (c>>16) & 0xff; + int a = (((c>>24) & 0xff)*iu) >> 8; + return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); +} + +static inline int nsvg__div255(int x) +{ + return ((x+1) * 257) >> 16; +} + +static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* cover, int x, int y, + float tx, float ty, float scale, NSVGcachedPaint* cache) +{ + + if (cache->type == NSVG_PAINT_COLOR) { + int i, cr, cg, cb, ca; + cr = cache->colors[0] & 0xff; + cg = (cache->colors[0] >> 8) & 0xff; + cb = (cache->colors[0] >> 16) & 0xff; + ca = (cache->colors[0] >> 24) & 0xff; + + for (i = 0; i < count; i++) { + int r,g,b; + int a = nsvg__div255((int)cover[0] * ca); + int ia = 255 - a; + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + } + } else if (cache->type == NSVG_PAINT_LINEAR_GRADIENT) { + // TODO: spread modes. + // TODO: plenty of opportunities to optimize. + float fx, fy, dx, gy; + float* t = cache->xform; + int i, cr, cg, cb, ca; + unsigned int c; + + fx = ((float)x - tx) / scale; + fy = ((float)y - ty) / scale; + dx = 1.0f / scale; + + for (i = 0; i < count; i++) { + int r,g,b,a,ia; + gy = fx*t[1] + fy*t[3] + t[5]; + c = cache->colors[(int)nsvg__clampf(gy*255.0f, 0, 255.0f)]; + cr = (c) & 0xff; + cg = (c >> 8) & 0xff; + cb = (c >> 16) & 0xff; + ca = (c >> 24) & 0xff; + + a = nsvg__div255((int)cover[0] * ca); + ia = 255 - a; + + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + fx += dx; + } + } else if (cache->type == NSVG_PAINT_RADIAL_GRADIENT) { + // TODO: spread modes. + // TODO: plenty of opportunities to optimize. + // TODO: focus (fx,fy) + float fx, fy, dx, gx, gy, gd; + float* t = cache->xform; + int i, cr, cg, cb, ca; + unsigned int c; + + fx = ((float)x - tx) / scale; + fy = ((float)y - ty) / scale; + dx = 1.0f / scale; + + for (i = 0; i < count; i++) { + int r,g,b,a,ia; + gx = fx*t[0] + fy*t[2] + t[4]; + gy = fx*t[1] + fy*t[3] + t[5]; + gd = sqrtf(gx*gx + gy*gy); + c = cache->colors[(int)nsvg__clampf(gd*255.0f, 0, 255.0f)]; + cr = (c) & 0xff; + cg = (c >> 8) & 0xff; + cb = (c >> 16) & 0xff; + ca = (c >> 24) & 0xff; + + a = nsvg__div255((int)cover[0] * ca); + ia = 255 - a; + + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + fx += dx; + } + } +} + +static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, float scale, NSVGcachedPaint* cache, char fillRule) +{ + NSVGactiveEdge *active = NULL; + int y, s; + int e = 0; + int maxWeight = (255 / NSVG__SUBSAMPLES); // weight per vertical scanline + int xmin, xmax; + + for (y = 0; y < r->height; y++) { + memset(r->scanline, 0, r->width); + xmin = r->width; + xmax = 0; + for (s = 0; s < NSVG__SUBSAMPLES; ++s) { + // find center of pixel for this scanline + float scany = (float)(y*NSVG__SUBSAMPLES + s) + 0.5f; + NSVGactiveEdge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + NSVGactiveEdge *z = *step; + if (z->ey <= scany) { + *step = z->next; // delete from list +// NSVG__assert(z->valid); + nsvg__freeActive(r, z); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for (;;) { + int changed = 0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + NSVGactiveEdge* t = *step; + NSVGactiveEdge* q = t->next; + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e < r->nedges && r->edges[e].y0 <= scany) { + if (r->edges[e].y1 > scany) { + NSVGactiveEdge* z = nsvg__addActive(r, &r->edges[e], scany); + if (z == NULL) break; + // find insertion point + if (active == NULL) { + active = z; + } else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + NSVGactiveEdge* p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + e++; + } + + // now process all active edges in non-zero fashion + if (active != NULL) + nsvg__fillActiveEdges(r->scanline, r->width, active, maxWeight, &xmin, &xmax, fillRule); + } + // Blit + if (xmin < 0) xmin = 0; + if (xmax > r->width-1) xmax = r->width-1; + if (xmin <= xmax) { + nsvg__scanlineSolid(&r->bitmap[y * r->stride] + xmin*4, xmax-xmin+1, &r->scanline[xmin], xmin, y, tx,ty, scale, cache); + } + } + +} + +static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride) +{ + int x,y; + + // Unpremultiply + for (y = 0; y < h; y++) { + unsigned char *row = &image[y*stride]; + for (x = 0; x < w; x++) { + int r = row[0], g = row[1], b = row[2], a = row[3]; + if (a != 0) { + row[0] = (unsigned char)(r*255/a); + row[1] = (unsigned char)(g*255/a); + row[2] = (unsigned char)(b*255/a); + } + row += 4; + } + } + + // Defringe + for (y = 0; y < h; y++) { + unsigned char *row = &image[y*stride]; + for (x = 0; x < w; x++) { + int r = 0, g = 0, b = 0, a = row[3], n = 0; + if (a == 0) { + if (x-1 > 0 && row[-1] != 0) { + r += row[-4]; + g += row[-3]; + b += row[-2]; + n++; + } + if (x+1 < w && row[7] != 0) { + r += row[4]; + g += row[5]; + b += row[6]; + n++; + } + if (y-1 > 0 && row[-stride+3] != 0) { + r += row[-stride]; + g += row[-stride+1]; + b += row[-stride+2]; + n++; + } + if (y+1 < h && row[stride+3] != 0) { + r += row[stride]; + g += row[stride+1]; + b += row[stride+2]; + n++; + } + if (n > 0) { + row[0] = (unsigned char)(r/n); + row[1] = (unsigned char)(g/n); + row[2] = (unsigned char)(b/n); + } + } + row += 4; + } + } +} + + +static void nsvg__initPaint(NSVGcachedPaint* cache, NSVGpaint* paint, float opacity) +{ + int i, j; + NSVGgradient* grad; + + cache->type = paint->type; + + if (paint->type == NSVG_PAINT_COLOR) { + cache->colors[0] = nsvg__applyOpacity(paint->color, opacity); + return; + } + + grad = paint->gradient; + + cache->spread = grad->spread; + memcpy(cache->xform, grad->xform, sizeof(float)*6); + + if (grad->nstops == 0) { + for (i = 0; i < 256; i++) + cache->colors[i] = 0; + } else if (grad->nstops == 1) { + for (i = 0; i < 256; i++) + cache->colors[i] = nsvg__applyOpacity(grad->stops[i].color, opacity); + } else { + unsigned int ca, cb = 0; + float ua, ub, du, u; + int ia, ib, count; + + ca = nsvg__applyOpacity(grad->stops[0].color, opacity); + ua = nsvg__clampf(grad->stops[0].offset, 0, 1); + ub = nsvg__clampf(grad->stops[grad->nstops-1].offset, ua, 1); + ia = (int)(ua * 255.0f); + ib = (int)(ub * 255.0f); + for (i = 0; i < ia; i++) { + cache->colors[i] = ca; + } + + for (i = 0; i < grad->nstops-1; i++) { + ca = nsvg__applyOpacity(grad->stops[i].color, opacity); + cb = nsvg__applyOpacity(grad->stops[i+1].color, opacity); + ua = nsvg__clampf(grad->stops[i].offset, 0, 1); + ub = nsvg__clampf(grad->stops[i+1].offset, 0, 1); + ia = (int)(ua * 255.0f); + ib = (int)(ub * 255.0f); + count = ib - ia; + if (count <= 0) continue; + u = 0; + du = 1.0f / (float)count; + for (j = 0; j < count; j++) { + cache->colors[ia+j] = nsvg__lerpRGBA(ca,cb,u); + u += du; + } + } + + for (i = ib; i < 256; i++) + cache->colors[i] = cb; + } + +} + +/* +static void dumpEdges(NSVGrasterizer* r, const char* name) +{ + float xmin = 0, xmax = 0, ymin = 0, ymax = 0; + NSVGedge *e = NULL; + int i; + if (r->nedges == 0) return; + FILE* fp = fopen(name, "w"); + if (fp == NULL) return; + + xmin = xmax = r->edges[0].x0; + ymin = ymax = r->edges[0].y0; + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + xmin = nsvg__minf(xmin, e->x0); + xmin = nsvg__minf(xmin, e->x1); + xmax = nsvg__maxf(xmax, e->x0); + xmax = nsvg__maxf(xmax, e->x1); + ymin = nsvg__minf(ymin, e->y0); + ymin = nsvg__minf(ymin, e->y1); + ymax = nsvg__maxf(ymax, e->y0); + ymax = nsvg__maxf(ymax, e->y1); + } + + fprintf(fp, "", xmin, ymin, (xmax - xmin), (ymax - ymin)); + + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + fprintf(fp ,"", e->x0,e->y0, e->x1,e->y1); + } + + for (i = 0; i < r->npoints; i++) { + if (i+1 < r->npoints) + fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i+1].x, r->points[i+1].y); + fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i].flags == 0 ? "#f00" : "#0f0"); + } + + fprintf(fp, ""); + fclose(fp); +} +*/ + +void nsvgRasterize(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride) +{ + NSVGshape *shape = NULL; + NSVGedge *e = NULL; + NSVGcachedPaint cache; + int i; + + r->bitmap = dst; + r->width = w; + r->height = h; + r->stride = stride; + + if (w > r->cscanline) { + r->cscanline = w; + r->scanline = (unsigned char*)realloc(r->scanline, w); + if (r->scanline == NULL) return; + } + + for (i = 0; i < h; i++) + memset(&dst[i*stride], 0, w*4); + + for (shape = image->shapes; shape != NULL; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) + continue; + + if (shape->fill.type != NSVG_PAINT_NONE) { + nsvg__resetPool(r); + r->freelist = NULL; + r->nedges = 0; + + nsvg__flattenShape(r, shape, scale); + + // Scale and translate edges + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + e->x0 = tx + e->x0; + e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; + e->x1 = tx + e->x1; + e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; + } + + // Rasterize edges + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + + // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule + nsvg__initPaint(&cache, &shape->fill, shape->opacity); + + nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, shape->fillRule); + } + if (shape->stroke.type != NSVG_PAINT_NONE && (shape->strokeWidth * scale) > 0.01f) { + nsvg__resetPool(r); + r->freelist = NULL; + r->nedges = 0; + + nsvg__flattenShapeStroke(r, shape, scale); + +// dumpEdges(r, "edge.svg"); + + // Scale and translate edges + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + e->x0 = tx + e->x0; + e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; + e->x1 = tx + e->x1; + e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; + } + + // Rasterize edges + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + + // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule + nsvg__initPaint(&cache, &shape->stroke, shape->opacity); + + nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, NSVG_FILLRULE_NONZERO); + } + } + + nsvg__unpremultiplyAlpha(dst, w, h, stride); + + r->bitmap = NULL; + r->width = 0; + r->height = 0; + r->stride = 0; +} + +#endif // NANOSVGRAST_IMPLEMENTATION + +#endif // NANOSVGRAST_H diff --git a/libraries/ZWidget/src/core/picopng/LICENSE.txt b/libraries/ZWidget/src/core/picopng/LICENSE.txt new file mode 100644 index 0000000000..a1f522f51b --- /dev/null +++ b/libraries/ZWidget/src/core/picopng/LICENSE.txt @@ -0,0 +1,18 @@ + // picoPNG version 20101224 + // Copyright (c) 2005-2010 Lode Vandevenne + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. diff --git a/libraries/ZWidget/src/core/picopng/picopng.cpp b/libraries/ZWidget/src/core/picopng/picopng.cpp new file mode 100644 index 0000000000..97eb3b500f --- /dev/null +++ b/libraries/ZWidget/src/core/picopng/picopng.cpp @@ -0,0 +1,595 @@ +#include +#include +#include + +/* +decodePNG: The picoPNG function, decodes a PNG file buffer in memory, into a raw pixel buffer. +out_image: output parameter, this will contain the raw pixels after decoding. + By default the output is 32-bit RGBA color. + The std::vector is automatically resized to the correct size. +image_width: output_parameter, this will contain the width of the image in pixels. +image_height: output_parameter, this will contain the height of the image in pixels. +in_png: pointer to the buffer of the PNG file in memory. To get it from a file on + disk, load it and store it in a memory buffer yourself first. +in_size: size of the input PNG file in bytes. +convert_to_rgba32: optional parameter, true by default. + Set to true to get the output in RGBA 32-bit (8 bit per channel) color format + no matter what color type the original PNG image had. This gives predictable, + useable data from any random input PNG. + Set to false to do no color conversion at all. The result then has the same data + type as the PNG image, which can range from 1 bit to 64 bits per pixel. + Information about the color type or palette colors are not provided. You need + to know this information yourself to be able to use the data so this only + works for trusted PNG files. Use LodePNG instead of picoPNG if you need this information. +return: 0 if success, not 0 if some error occured. +*/ +int decodePNG(std::vector& out_image, unsigned long& image_width, unsigned long& image_height, const unsigned char* in_png, size_t in_size, bool convert_to_rgba32 = true) +{ + // picoPNG version 20101224 + // Copyright (c) 2005-2010 Lode Vandevenne + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + // picoPNG is a PNG decoder in one C++ function of around 500 lines. Use picoPNG for + // programs that need only 1 .cpp file. Since it's a single function, it's very limited, + // it can convert a PNG to raw pixel data either converted to 32-bit RGBA color or + // with no color conversion at all. For anything more complex, another tiny library + // is available: LodePNG (lodepng.c(pp)), which is a single source and header file. + // Apologies for the compact code style, it's to make this tiny. + + static const unsigned long LENBASE[29] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258}; + static const unsigned long LENEXTRA[29] = {0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; + static const unsigned long DISTBASE[30] = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577}; + static const unsigned long DISTEXTRA[30] = {0,0,0,0,1,1,2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; + static const unsigned long CLCL[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; //code length code lengths + struct Zlib //nested functions for zlib decompression + { + static unsigned long readBitFromStream(size_t& bitp, const unsigned char* bits) { unsigned long result = (bits[bitp >> 3] >> (bitp & 0x7)) & 1; bitp++; return result;} + static unsigned long readBitsFromStream(size_t& bitp, const unsigned char* bits, size_t nbits) + { + unsigned long result = 0; + for(size_t i = 0; i < nbits; i++) result += (readBitFromStream(bitp, bits)) << i; + return result; + } + struct HuffmanTree + { + int makeFromLengths(const std::vector& bitlen, unsigned long maxbitlen) + { //make tree given the lengths + unsigned long numcodes = (unsigned long)(bitlen.size()), treepos = 0, nodefilled = 0; + std::vector tree1d(numcodes), blcount(maxbitlen + 1, 0), nextcode(maxbitlen + 1, 0); + for(unsigned long bits = 0; bits < numcodes; bits++) blcount[bitlen[bits]]++; //count number of instances of each code length + for(unsigned long bits = 1; bits <= maxbitlen; bits++) nextcode[bits] = (nextcode[bits - 1] + blcount[bits - 1]) << 1; + for(unsigned long n = 0; n < numcodes; n++) if(bitlen[n] != 0) tree1d[n] = nextcode[bitlen[n]]++; //generate all the codes + tree2d.clear(); tree2d.resize(numcodes * 2, 32767); //32767 here means the tree2d isn't filled there yet + for(unsigned long n = 0; n < numcodes; n++) //the codes + for(unsigned long i = 0; i < bitlen[n]; i++) //the bits for this code + { + unsigned long bit = (tree1d[n] >> (bitlen[n] - i - 1)) & 1; + if(treepos > numcodes - 2) return 55; + if(tree2d[2 * treepos + bit] == 32767) //not yet filled in + { + if(i + 1 == bitlen[n]) { tree2d[2 * treepos + bit] = n; treepos = 0; } //last bit + else { tree2d[2 * treepos + bit] = ++nodefilled + numcodes; treepos = nodefilled; } //addresses are encoded as values > numcodes + } + else treepos = tree2d[2 * treepos + bit] - numcodes; //subtract numcodes from address to get address value + } + return 0; + } + int decode(bool& decoded, unsigned long& result, size_t& treepos, unsigned long bit) const + { //Decodes a symbol from the tree + unsigned long numcodes = (unsigned long)tree2d.size() / 2; + if(treepos >= numcodes) return 11; //error: you appeared outside the codetree + result = tree2d[2 * treepos + bit]; + decoded = (result < numcodes); + treepos = decoded ? 0 : result - numcodes; + return 0; + } + std::vector tree2d; //2D representation of a huffman tree: The one dimension is "0" or "1", the other contains all nodes and leaves of the tree. + }; + struct Inflator + { + int error; + void inflate(std::vector& out, const std::vector& in, size_t inpos = 0) + { + size_t bp = 0, pos = 0; //bit pointer and byte pointer + error = 0; + unsigned long BFINAL = 0; + while(!BFINAL && !error) + { + if(bp >> 3 >= in.size()) { error = 52; return; } //error, bit pointer will jump past memory + BFINAL = readBitFromStream(bp, &in[inpos]); + unsigned long BTYPE = readBitFromStream(bp, &in[inpos]); BTYPE += 2 * readBitFromStream(bp, &in[inpos]); + if(BTYPE == 3) { error = 20; return; } //error: invalid BTYPE + else if(BTYPE == 0) inflateNoCompression(out, &in[inpos], bp, pos, in.size()); + else inflateHuffmanBlock(out, &in[inpos], bp, pos, in.size(), BTYPE); + } + if(!error) out.resize(pos); //Only now we know the true size of out, resize it to that + } + void generateFixedTrees(HuffmanTree& tree, HuffmanTree& treeD) //get the tree of a deflated block with fixed tree + { + std::vector bitlen(288, 8), bitlenD(32, 5);; + for(size_t i = 144; i <= 255; i++) bitlen[i] = 9; + for(size_t i = 256; i <= 279; i++) bitlen[i] = 7; + tree.makeFromLengths(bitlen, 15); + treeD.makeFromLengths(bitlenD, 15); + } + HuffmanTree codetree, codetreeD, codelengthcodetree; //the code tree for Huffman codes, dist codes, and code length codes + unsigned long huffmanDecodeSymbol(const unsigned char* in, size_t& bp, const HuffmanTree& codetree, size_t inlength) + { //decode a single symbol from given list of bits with given code tree. return value is the symbol + bool decoded; unsigned long ct; + for(size_t treepos = 0;;) + { + if((bp & 0x07) == 0 && (bp >> 3) > inlength) { error = 10; return 0; } //error: end reached without endcode + error = codetree.decode(decoded, ct, treepos, readBitFromStream(bp, in)); if(error) return 0; //stop, an error happened + if(decoded) return ct; + } + } + void getTreeInflateDynamic(HuffmanTree& tree, HuffmanTree& treeD, const unsigned char* in, size_t& bp, size_t inlength) + { //get the tree of a deflated block with dynamic tree, the tree itself is also Huffman compressed with a known tree + std::vector bitlen(288, 0), bitlenD(32, 0); + if(bp >> 3 >= inlength - 2) { error = 49; return; } //the bit pointer is or will go past the memory + size_t HLIT = readBitsFromStream(bp, in, 5) + 257; //number of literal/length codes + 257 + size_t HDIST = readBitsFromStream(bp, in, 5) + 1; //number of dist codes + 1 + size_t HCLEN = readBitsFromStream(bp, in, 4) + 4; //number of code length codes + 4 + std::vector codelengthcode(19); //lengths of tree to decode the lengths of the dynamic tree + for(size_t i = 0; i < 19; i++) codelengthcode[CLCL[i]] = (i < HCLEN) ? readBitsFromStream(bp, in, 3) : 0; + error = codelengthcodetree.makeFromLengths(codelengthcode, 7); if(error) return; + size_t i = 0, replength; + while(i < HLIT + HDIST) + { + unsigned long code = huffmanDecodeSymbol(in, bp, codelengthcodetree, inlength); if(error) return; + if(code <= 15) { if(i < HLIT) bitlen[i++] = code; else bitlenD[i++ - HLIT] = code; } //a length code + else if(code == 16) //repeat previous + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 3 + readBitsFromStream(bp, in, 2); + unsigned long value; //set value to the previous code + if((i - 1) < HLIT) value = bitlen[i - 1]; + else value = bitlenD[i - HLIT - 1]; + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 13; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = value; else bitlenD[i++ - HLIT] = value; + } + } + else if(code == 17) //repeat "0" 3-10 times + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 3 + readBitsFromStream(bp, in, 3); + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 14; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = 0; else bitlenD[i++ - HLIT] = 0; + } + } + else if(code == 18) //repeat "0" 11-138 times + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 11 + readBitsFromStream(bp, in, 7); + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 15; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = 0; else bitlenD[i++ - HLIT] = 0; + } + } + else { error = 16; return; } //error: somehow an unexisting code appeared. This can never happen. + } + if(bitlen[256] == 0) { error = 64; return; } //the length of the end code 256 must be larger than 0 + error = tree.makeFromLengths(bitlen, 15); if(error) return; //now we've finally got HLIT and HDIST, so generate the code trees, and the function is done + error = treeD.makeFromLengths(bitlenD, 15); if(error) return; + } + void inflateHuffmanBlock(std::vector& out, const unsigned char* in, size_t& bp, size_t& pos, size_t inlength, unsigned long btype) + { + if(btype == 1) { generateFixedTrees(codetree, codetreeD); } + else if(btype == 2) { getTreeInflateDynamic(codetree, codetreeD, in, bp, inlength); if(error) return; } + for(;;) + { + unsigned long code = huffmanDecodeSymbol(in, bp, codetree, inlength); if(error) return; + if(code == 256) return; //end code + else if(code <= 255) //literal symbol + { + if(pos >= out.size()) out.resize((pos + 1) * 2); //reserve more room + out[pos++] = (unsigned char)(code); + } + else if(code >= 257 && code <= 285) //length code + { + size_t length = LENBASE[code - 257], numextrabits = LENEXTRA[code - 257]; + if((bp >> 3) >= inlength) { error = 51; return; } //error, bit pointer will jump past memory + length += readBitsFromStream(bp, in, numextrabits); + unsigned long codeD = huffmanDecodeSymbol(in, bp, codetreeD, inlength); if(error) return; + if(codeD > 29) { error = 18; return; } //error: invalid dist code (30-31 are never used) + unsigned long dist = DISTBASE[codeD], numextrabitsD = DISTEXTRA[codeD]; + if((bp >> 3) >= inlength) { error = 51; return; } //error, bit pointer will jump past memory + dist += readBitsFromStream(bp, in, numextrabitsD); + size_t start = pos, back = start - dist; //backwards + if(pos + length >= out.size()) out.resize((pos + length) * 2); //reserve more room + for(size_t i = 0; i < length; i++) { out[pos++] = out[back++]; if(back >= start) back = start - dist; } + } + } + } + void inflateNoCompression(std::vector& out, const unsigned char* in, size_t& bp, size_t& pos, size_t inlength) + { + while((bp & 0x7) != 0) bp++; //go to first boundary of byte + size_t p = bp / 8; + if(p >= inlength - 4) { error = 52; return; } //error, bit pointer will jump past memory + unsigned long LEN = in[p] + 256 * in[p + 1], NLEN = in[p + 2] + 256 * in[p + 3]; p += 4; + if(LEN + NLEN != 65535) { error = 21; return; } //error: NLEN is not one's complement of LEN + if(pos + LEN >= out.size()) out.resize(pos + LEN); + if(p + LEN > inlength) { error = 23; return; } //error: reading outside of in buffer + for(unsigned long n = 0; n < LEN; n++) out[pos++] = in[p++]; //read LEN bytes of literal data + bp = p * 8; + } + }; + int decompress(std::vector& out, const std::vector& in) //returns error value + { + Inflator inflator; + if(in.size() < 2) { return 53; } //error, size of zlib data too small + if((in[0] * 256 + in[1]) % 31 != 0) { return 24; } //error: 256 * in[0] + in[1] must be a multiple of 31, the FCHECK value is supposed to be made that way + unsigned long CM = in[0] & 15, CINFO = (in[0] >> 4) & 15, FDICT = (in[1] >> 5) & 1; + if(CM != 8 || CINFO > 7) { return 25; } //error: only compression method 8: inflate with sliding window of 32k is supported by the PNG spec + if(FDICT != 0) { return 26; } //error: the specification of PNG says about the zlib stream: "The additional flags shall not specify a preset dictionary." + inflator.inflate(out, in, 2); + return inflator.error; //note: adler32 checksum was skipped and ignored + } + }; + struct PNG //nested functions for PNG decoding + { + struct Info + { + unsigned long width, height, colorType, bitDepth, compressionMethod, filterMethod, interlaceMethod, key_r, key_g, key_b; + bool key_defined; //is a transparent color key given? + std::vector palette; + } info; + int error; + void decode(std::vector& out, const unsigned char* in, size_t size, bool convert_to_rgba32) + { + error = 0; + if(size == 0 || in == 0) { error = 48; return; } //the given data is empty + readPngHeader(&in[0], size); if(error) return; + size_t pos = 33; //first byte of the first chunk after the header + std::vector idat; //the data from idat chunks + bool IEND = false, known_type = true; + info.key_defined = false; + while(!IEND) //loop through the chunks, ignoring unknown chunks and stopping at IEND chunk. IDAT data is put at the start of the in buffer + { + if(pos + 8 >= size) { error = 30; return; } //error: size of the in buffer too small to contain next chunk + size_t chunkLength = read32bitInt(&in[pos]); pos += 4; + if(chunkLength > 2147483647) { error = 63; return; } + if(pos + chunkLength >= size) { error = 35; return; } //error: size of the in buffer too small to contain next chunk + if(in[pos + 0] == 'I' && in[pos + 1] == 'D' && in[pos + 2] == 'A' && in[pos + 3] == 'T') //IDAT chunk, containing compressed image data + { + idat.insert(idat.end(), &in[pos + 4], &in[pos + 4 + chunkLength]); + pos += (4 + chunkLength); + } + else if(in[pos + 0] == 'I' && in[pos + 1] == 'E' && in[pos + 2] == 'N' && in[pos + 3] == 'D') { pos += 4; IEND = true; } + else if(in[pos + 0] == 'P' && in[pos + 1] == 'L' && in[pos + 2] == 'T' && in[pos + 3] == 'E') //palette chunk (PLTE) + { + pos += 4; //go after the 4 letters + info.palette.resize(4 * (chunkLength / 3)); + if(info.palette.size() > (4 * 256)) { error = 38; return; } //error: palette too big + for(size_t i = 0; i < info.palette.size(); i += 4) + { + for(size_t j = 0; j < 3; j++) info.palette[i + j] = in[pos++]; //RGB + info.palette[i + 3] = 255; //alpha + } + } + else if(in[pos + 0] == 't' && in[pos + 1] == 'R' && in[pos + 2] == 'N' && in[pos + 3] == 'S') //palette transparency chunk (tRNS) + { + pos += 4; //go after the 4 letters + if(info.colorType == 3) + { + if(4 * chunkLength > info.palette.size()) { error = 39; return; } //error: more alpha values given than there are palette entries + for(size_t i = 0; i < chunkLength; i++) info.palette[4 * i + 3] = in[pos++]; + } + else if(info.colorType == 0) + { + if(chunkLength != 2) { error = 40; return; } //error: this chunk must be 2 bytes for greyscale image + info.key_defined = 1; info.key_r = info.key_g = info.key_b = 256 * in[pos] + in[pos + 1]; pos += 2; + } + else if(info.colorType == 2) + { + if(chunkLength != 6) { error = 41; return; } //error: this chunk must be 6 bytes for RGB image + info.key_defined = 1; + info.key_r = 256 * in[pos] + in[pos + 1]; pos += 2; + info.key_g = 256 * in[pos] + in[pos + 1]; pos += 2; + info.key_b = 256 * in[pos] + in[pos + 1]; pos += 2; + } + else { error = 42; return; } //error: tRNS chunk not allowed for other color models + } + else //it's not an implemented chunk type, so ignore it: skip over the data + { + if(!(in[pos + 0] & 32)) { error = 69; return; } //error: unknown critical chunk (5th bit of first byte of chunk type is 0) + pos += (chunkLength + 4); //skip 4 letters and uninterpreted data of unimplemented chunk + known_type = false; + } + pos += 4; //step over CRC (which is ignored) + } + unsigned long bpp = getBpp(info); + std::vector scanlines(((info.width * (info.height * bpp + 7)) / 8) + info.height); //now the out buffer will be filled + Zlib zlib; //decompress with the Zlib decompressor + error = zlib.decompress(scanlines, idat); if(error) return; //stop if the zlib decompressor returned an error + size_t bytewidth = (bpp + 7) / 8, outlength = (info.height * info.width * bpp + 7) / 8; + out.resize(outlength); //time to fill the out buffer + unsigned char* out_ = outlength ? &out[0] : 0; //use a regular pointer to the std::vector for faster code if compiled without optimization + if(info.interlaceMethod == 0) //no interlace, just filter + { + size_t linestart = 0, linelength = (info.width * bpp + 7) / 8; //length in bytes of a scanline, excluding the filtertype byte + if(bpp >= 8) //byte per byte + for(unsigned long y = 0; y < info.height; y++) + { + unsigned long filterType = scanlines[linestart]; + const unsigned char* prevline = (y == 0) ? 0 : &out_[(y - 1) * info.width * bytewidth]; + unFilterScanline(&out_[linestart - y], &scanlines[linestart + 1], prevline, bytewidth, filterType, linelength); if(error) return; + linestart += (1 + linelength); //go to start of next scanline + } + else //less than 8 bits per pixel, so fill it up bit per bit + { + std::vector templine((info.width * bpp + 7) >> 3); //only used if bpp < 8 + for(size_t y = 0, obp = 0; y < info.height; y++) + { + unsigned long filterType = scanlines[linestart]; + const unsigned char* prevline = (y == 0) ? 0 : &out_[(y - 1) * info.width * bytewidth]; + unFilterScanline(&templine[0], &scanlines[linestart + 1], prevline, bytewidth, filterType, linelength); if(error) return; + for(size_t bp = 0; bp < info.width * bpp;) setBitOfReversedStream(obp, out_, readBitFromReversedStream(bp, &templine[0])); + linestart += (1 + linelength); //go to start of next scanline + } + } + } + else //interlaceMethod is 1 (Adam7) + { + size_t passw[7] = { (info.width + 7) / 8, (info.width + 3) / 8, (info.width + 3) / 4, (info.width + 1) / 4, (info.width + 1) / 2, (info.width + 0) / 2, (info.width + 0) / 1 }; + size_t passh[7] = { (info.height + 7) / 8, (info.height + 7) / 8, (info.height + 3) / 8, (info.height + 3) / 4, (info.height + 1) / 4, (info.height + 1) / 2, (info.height + 0) / 2 }; + size_t passstart[7] = {0}; + size_t pattern[28] = {0,4,0,2,0,1,0,0,0,4,0,2,0,1,8,8,4,4,2,2,1,8,8,8,4,4,2,2}; //values for the adam7 passes + for(int i = 0; i < 6; i++) passstart[i + 1] = passstart[i] + passh[i] * ((passw[i] ? 1 : 0) + (passw[i] * bpp + 7) / 8); + std::vector scanlineo((info.width * bpp + 7) / 8), scanlinen((info.width * bpp + 7) / 8); //"old" and "new" scanline + for(int i = 0; i < 7; i++) + adam7Pass(&out_[0], &scanlinen[0], &scanlineo[0], &scanlines[passstart[i]], info.width, pattern[i], pattern[i + 7], pattern[i + 14], pattern[i + 21], passw[i], passh[i], bpp); + } + if(convert_to_rgba32 && (info.colorType != 6 || info.bitDepth != 8)) //conversion needed + { + std::vector data = out; + error = convert(out, &data[0], info, info.width, info.height); + } + } + void readPngHeader(const unsigned char* in, size_t inlength) //read the information from the header and store it in the Info + { + if(inlength < 29) { error = 27; return; } //error: the data length is smaller than the length of the header + if(in[0] != 137 || in[1] != 80 || in[2] != 78 || in[3] != 71 || in[4] != 13 || in[5] != 10 || in[6] != 26 || in[7] != 10) { error = 28; return; } //no PNG signature + if(in[12] != 'I' || in[13] != 'H' || in[14] != 'D' || in[15] != 'R') { error = 29; return; } //error: it doesn't start with a IHDR chunk! + info.width = read32bitInt(&in[16]); info.height = read32bitInt(&in[20]); + info.bitDepth = in[24]; info.colorType = in[25]; + info.compressionMethod = in[26]; if(in[26] != 0) { error = 32; return; } //error: only compression method 0 is allowed in the specification + info.filterMethod = in[27]; if(in[27] != 0) { error = 33; return; } //error: only filter method 0 is allowed in the specification + info.interlaceMethod = in[28]; if(in[28] > 1) { error = 34; return; } //error: only interlace methods 0 and 1 exist in the specification + error = checkColorValidity(info.colorType, info.bitDepth); + } + void unFilterScanline(unsigned char* recon, const unsigned char* scanline, const unsigned char* precon, size_t bytewidth, unsigned long filterType, size_t length) + { + switch(filterType) + { + case 0: for(size_t i = 0; i < length; i++) recon[i] = scanline[i]; break; + case 1: + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + recon[i - bytewidth]; + break; + case 2: + if(precon) for(size_t i = 0; i < length; i++) recon[i] = scanline[i] + precon[i]; + else for(size_t i = 0; i < length; i++) recon[i] = scanline[i]; + break; + case 3: + if(precon) + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i] + precon[i] / 2; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + ((recon[i - bytewidth] + precon[i]) / 2); + } + else + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + recon[i - bytewidth] / 2; + } + break; + case 4: + if(precon) + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i] + paethPredictor(0, precon[i], 0); + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + paethPredictor(recon[i - bytewidth], precon[i], precon[i - bytewidth]); + } + else + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + paethPredictor(recon[i - bytewidth], 0, 0); + } + break; + default: error = 36; return; //error: unexisting filter type given + } + } + void adam7Pass(unsigned char* out, unsigned char* linen, unsigned char* lineo, const unsigned char* in, unsigned long w, size_t passleft, size_t passtop, size_t spacex, size_t spacey, size_t passw, size_t passh, unsigned long bpp) + { //filter and reposition the pixels into the output when the image is Adam7 interlaced. This function can only do it after the full image is already decoded. The out buffer must have the correct allocated memory size already. + if(passw == 0) return; + size_t bytewidth = (bpp + 7) / 8, linelength = 1 + ((bpp * passw + 7) / 8); + for(unsigned long y = 0; y < passh; y++) + { + unsigned char filterType = in[y * linelength], *prevline = (y == 0) ? 0 : lineo; + unFilterScanline(linen, &in[y * linelength + 1], prevline, bytewidth, filterType, (w * bpp + 7) / 8); if(error) return; + if(bpp >= 8) for(size_t i = 0; i < passw; i++) for(size_t b = 0; b < bytewidth; b++) //b = current byte of this pixel + out[bytewidth * w * (passtop + spacey * y) + bytewidth * (passleft + spacex * i) + b] = linen[bytewidth * i + b]; + else for(size_t i = 0; i < passw; i++) + { + size_t obp = bpp * w * (passtop + spacey * y) + bpp * (passleft + spacex * i), bp = i * bpp; + for(size_t b = 0; b < bpp; b++) setBitOfReversedStream(obp, out, readBitFromReversedStream(bp, &linen[0])); + } + unsigned char* temp = linen; linen = lineo; lineo = temp; //swap the two buffer pointers "line old" and "line new" + } + } + static unsigned long readBitFromReversedStream(size_t& bitp, const unsigned char* bits) { unsigned long result = (bits[bitp >> 3] >> (7 - (bitp & 0x7))) & 1; bitp++; return result;} + static unsigned long readBitsFromReversedStream(size_t& bitp, const unsigned char* bits, unsigned long nbits) + { + unsigned long result = 0; + for(size_t i = nbits - 1; i < nbits; i--) result += ((readBitFromReversedStream(bitp, bits)) << i); + return result; + } + void setBitOfReversedStream(size_t& bitp, unsigned char* bits, unsigned long bit) { bits[bitp >> 3] |= (bit << (7 - (bitp & 0x7))); bitp++; } + unsigned long read32bitInt(const unsigned char* buffer) { return (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]; } + int checkColorValidity(unsigned long colorType, unsigned long bd) //return type is a LodePNG error code + { + if((colorType == 2 || colorType == 4 || colorType == 6)) { if(!(bd == 8 || bd == 16)) return 37; else return 0; } + else if(colorType == 0) { if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 || bd == 16)) return 37; else return 0; } + else if(colorType == 3) { if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 )) return 37; else return 0; } + else return 31; //unexisting color type + } + unsigned long getBpp(const Info& info) + { + if(info.colorType == 2) return (3 * info.bitDepth); + else if(info.colorType >= 4) return (info.colorType - 2) * info.bitDepth; + else return info.bitDepth; + } + int convert(std::vector& out, const unsigned char* in, Info& infoIn, unsigned long w, unsigned long h) + { //converts from any color type to 32-bit. return value = LodePNG error code + size_t numpixels = w * h, bp = 0; + out.resize(numpixels * 4); + unsigned char* out_ = out.empty() ? 0 : &out[0]; //faster if compiled without optimization + if(infoIn.bitDepth == 8 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[i]; + out_[4 * i + 3] = (infoIn.key_defined && in[i] == infoIn.key_r) ? 0 : 255; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 2) //RGB color + for(size_t i = 0; i < numpixels; i++) + { + for(size_t c = 0; c < 3; c++) out_[4 * i + c] = in[3 * i + c]; + out_[4 * i + 3] = (infoIn.key_defined == 1 && in[3 * i + 0] == infoIn.key_r && in[3 * i + 1] == infoIn.key_g && in[3 * i + 2] == infoIn.key_b) ? 0 : 255; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 3) //indexed color (palette) + for(size_t i = 0; i < numpixels; i++) + { + if(4U * in[i] >= infoIn.palette.size()) return 46; + for(size_t c = 0; c < 4; c++) out_[4 * i + c] = infoIn.palette[4 * in[i] + c]; //get rgb colors from the palette + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 4) //greyscale with alpha + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[2 * i + 0]; + out_[4 * i + 3] = in[2 * i + 1]; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 6) for(size_t i = 0; i < numpixels; i++) for(size_t c = 0; c < 4; c++) out_[4 * i + c] = in[4 * i + c]; //RGB with alpha + else if(infoIn.bitDepth == 16 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[2 * i]; + out_[4 * i + 3] = (infoIn.key_defined && 256U * in[i] + in[i + 1] == infoIn.key_r) ? 0 : 255; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 2) //RGB color + for(size_t i = 0; i < numpixels; i++) + { + for(size_t c = 0; c < 3; c++) out_[4 * i + c] = in[6 * i + 2 * c]; + out_[4 * i + 3] = (infoIn.key_defined && 256U*in[6*i+0]+in[6*i+1] == infoIn.key_r && 256U*in[6*i+2]+in[6*i+3] == infoIn.key_g && 256U*in[6*i+4]+in[6*i+5] == infoIn.key_b) ? 0 : 255; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 4) //greyscale with alpha + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[4 * i]; //most significant byte + out_[4 * i + 3] = in[4 * i + 2]; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 6) for(size_t i = 0; i < numpixels; i++) for(size_t c = 0; c < 4; c++) out_[4 * i + c] = in[8 * i + 2 * c]; //RGB with alpha + else if(infoIn.bitDepth < 8 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + unsigned long value = (readBitsFromReversedStream(bp, in, infoIn.bitDepth) * 255) / ((1 << infoIn.bitDepth) - 1); //scale value from 0 to 255 + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = (unsigned char)(value); + out_[4 * i + 3] = (infoIn.key_defined && value && ((1U << infoIn.bitDepth) - 1U) == infoIn.key_r && ((1U << infoIn.bitDepth) - 1U)) ? 0 : 255; + } + else if(infoIn.bitDepth < 8 && infoIn.colorType == 3) //palette + for(size_t i = 0; i < numpixels; i++) + { + unsigned long value = readBitsFromReversedStream(bp, in, infoIn.bitDepth); + if(4 * value >= infoIn.palette.size()) return 47; + for(size_t c = 0; c < 4; c++) out_[4 * i + c] = infoIn.palette[4 * value + c]; //get rgb colors from the palette + } + return 0; + } + unsigned char paethPredictor(short a, short b, short c) //Paeth predicter, used by PNG filter type 4 + { + short p = a + b - c, pa = p > a ? (p - a) : (a - p), pb = p > b ? (p - b) : (b - p), pc = p > c ? (p - c) : (c - p); + return (unsigned char)((pa <= pb && pa <= pc) ? a : pb <= pc ? b : c); + } + }; + PNG decoder; decoder.decode(out_image, in_png, in_size, convert_to_rgba32); + image_width = decoder.info.width; image_height = decoder.info.height; + return decoder.error; +} + +#if 0 + + + +//an example using the PNG loading function: + +#include +#include + +void loadFile(std::vector& buffer, const std::string& filename) //designed for loading files from hard disk in an std::vector +{ + std::ifstream file(filename.c_str(), std::ios::in|std::ios::binary|std::ios::ate); + + //get filesize + std::streamsize size = 0; + if(file.seekg(0, std::ios::end).good()) size = file.tellg(); + if(file.seekg(0, std::ios::beg).good()) size -= file.tellg(); + + //read contents of the file into the vector + if(size > 0) + { + buffer.resize((size_t)size); + file.read((char*)(&buffer[0]), size); + } + else buffer.clear(); +} + +int main(int argc, char *argv[]) +{ + const char* filename = argc > 1 ? argv[1] : "test.png"; + + //load and decode + std::vector buffer, image; + loadFile(buffer, filename); + unsigned long w, h; + int error = decodePNG(image, w, h, buffer.empty() ? 0 : &buffer[0], (unsigned long)buffer.size()); + + //if there's an error, display it + if(error != 0) std::cout << "error: " << error << std::endl; + + //the pixels are now in the vector "image", use it as texture, draw it, ... + + if(image.size() > 4) std::cout << "width: " << w << " height: " << h << " first pixel: " << std::hex << int(image[0]) << int(image[1]) << int(image[2]) << int(image[3]) << std::endl; +} + +/* + //this is test code, it displays the pixels of a 1 bit PNG. To use it, set the flag convert_to_rgba32 to false and load a 1-bit PNG image with a small size (so that its ASCII representation can fit in a console window) + for(int y = 0; y < h; y++) + { + for(int x = 0; x < w; x++) + { + int i = y * h + x; + std::cout << (((image[i/8] >> (7-i%8)) & 1) ? '.' : '#'); + } + std::cout << std::endl; + } +*/ + +#endif diff --git a/libraries/ZWidget/src/core/picopng/picopng.h b/libraries/ZWidget/src/core/picopng/picopng.h new file mode 100644 index 0000000000..87dc961bea --- /dev/null +++ b/libraries/ZWidget/src/core/picopng/picopng.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +int decodePNG(std::vector& out_image, unsigned long& image_width, unsigned long& image_height, const unsigned char* in_png, size_t in_size, bool convert_to_rgba32 = true); diff --git a/libraries/ZWidget/src/core/schrift/LICENSE.txt b/libraries/ZWidget/src/core/schrift/LICENSE.txt new file mode 100644 index 0000000000..6bb186fa97 --- /dev/null +++ b/libraries/ZWidget/src/core/schrift/LICENSE.txt @@ -0,0 +1,16 @@ +ISC License + +© 2019-2022 Thomas Oltmann and contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + diff --git a/libraries/ZWidget/src/core/schrift/schrift.cpp b/libraries/ZWidget/src/core/schrift/schrift.cpp new file mode 100644 index 0000000000..0febb86c00 --- /dev/null +++ b/libraries/ZWidget/src/core/schrift/schrift.cpp @@ -0,0 +1,1576 @@ +/* This file is part of libschrift. + * + * © 2019-2022 Thomas Oltmann and contributors + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ + +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +# define restrict __restrict +#endif + +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN 1 +# include +#else +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 1 +#endif +# include +# include +# include +# include +#endif + +#include "schrift.h" + +#define SCHRIFT_VERSION "0.10.2" + +#define FILE_MAGIC_ONE 0x00010000 +#define FILE_MAGIC_TWO 0x74727565 + +#define HORIZONTAL_KERNING 0x01 +#define MINIMUM_KERNING 0x02 +#define CROSS_STREAM_KERNING 0x04 +#define OVERRIDE_KERNING 0x08 + +#define POINT_IS_ON_CURVE 0x01 +#define X_CHANGE_IS_SMALL 0x02 +#define Y_CHANGE_IS_SMALL 0x04 +#define REPEAT_FLAG 0x08 +#define X_CHANGE_IS_ZERO 0x10 +#define X_CHANGE_IS_POSITIVE 0x10 +#define Y_CHANGE_IS_ZERO 0x20 +#define Y_CHANGE_IS_POSITIVE 0x20 + +#define OFFSETS_ARE_LARGE 0x001 +#define ACTUAL_XY_OFFSETS 0x002 +#define GOT_A_SINGLE_SCALE 0x008 +#define THERE_ARE_MORE_COMPONENTS 0x020 +#define GOT_AN_X_AND_Y_SCALE 0x040 +#define GOT_A_SCALE_MATRIX 0x080 + +/* macros */ +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define SIGN(x) (((x) > 0) - ((x) < 0)) +/* Allocate values on the stack if they are small enough, else spill to heap. */ +#define STACK_ALLOC(var, type, thresh, count) \ + type var##_stack_[thresh]; \ + var = (type*)( (count) <= (thresh) ? var##_stack_ : calloc(sizeof(type), count) ); +#define STACK_FREE(var) \ + if (var != var##_stack_) free(var); + +enum { SrcMapping, SrcUser }; + +/* structs */ +typedef struct Point Point; +typedef struct Line Line; +typedef struct Curve Curve; +typedef struct Cell Cell; +typedef struct Outline Outline; +typedef struct Raster Raster; + +struct Point { double x, y; }; +struct Line { uint_least16_t beg, end; }; +struct Curve { uint_least16_t beg, end, ctrl; }; +struct Cell { double area, cover; }; + +struct Outline +{ + Point *points; + Curve *curves; + Line *lines; + uint_least16_t numPoints; + uint_least16_t capPoints; + uint_least16_t numCurves; + uint_least16_t capCurves; + uint_least16_t numLines; + uint_least16_t capLines; +}; + +struct Raster +{ + Cell *cells; + int width; + int height; +}; + +struct SFT_Font +{ + const uint8_t *memory; + uint_fast32_t size; +#if defined(_WIN32) + HANDLE mapping; +#endif + int source; + + uint_least16_t unitsPerEm; + int_least16_t locaFormat; + uint_least16_t numLongHmtx; +}; + +/* function declarations */ +/* generic utility functions */ +static void *reallocarray2(void *optr, size_t nmemb, size_t size); +static inline int fast_floor(double x); +static inline int fast_ceil (double x); +/* file loading */ +static int map_file (SFT_Font *font, const char *filename); +static void unmap_file(SFT_Font *font); +static int init_font (SFT_Font *font); +/* simple mathematical operations */ +static Point midpoint(Point a, Point b); +static void transform_points(unsigned int numPts, Point *points, double trf[6]); +static void clip_points(unsigned int numPts, Point *points, int width, int height); +/* 'outline' data structure management */ +static int init_outline(Outline *outl); +static void free_outline(Outline *outl); +static int grow_points (Outline *outl); +static int grow_curves (Outline *outl); +static int grow_lines (Outline *outl); +/* TTF parsing utilities */ +static inline int is_safe_offset(SFT_Font *font, uint_fast32_t offset, uint_fast32_t margin); +static void *csearch(const void *key, const void *base, + size_t nmemb, size_t size, int (*compar)(const void *, const void *)); +static int cmpu16(const void *a, const void *b); +static int cmpu32(const void *a, const void *b); +static inline uint_least8_t getu8 (SFT_Font *font, uint_fast32_t offset); +static inline int_least8_t geti8 (SFT_Font *font, uint_fast32_t offset); +static inline uint_least16_t getu16(SFT_Font *font, uint_fast32_t offset); +static inline int_least16_t geti16(SFT_Font *font, uint_fast32_t offset); +static inline uint_least32_t getu32(SFT_Font *font, uint_fast32_t offset); +static int gettable(SFT_Font *font, const char* tag, uint_fast32_t *offset); +/* codepoint to glyph id translation */ +static int cmap_fmt4(SFT_Font *font, uint_fast32_t table, SFT_UChar charCode, uint_fast32_t *glyph); +static int cmap_fmt6(SFT_Font *font, uint_fast32_t table, SFT_UChar charCode, uint_fast32_t *glyph); +static int glyph_id(SFT_Font *font, SFT_UChar charCode, uint_fast32_t *glyph); +/* glyph metrics lookup */ +static int hor_metrics(SFT_Font *font, uint_fast32_t glyph, int *advanceWidth, int *leftSideBearing); +static int glyph_bbox(const SFT *sft, uint_fast32_t outline, int box[4]); +/* decoding outlines */ +static int outline_offset(SFT_Font *font, uint_fast32_t glyph, uint_fast32_t *offset); +static int simple_flags(SFT_Font *font, uint_fast32_t *offset, uint_fast16_t numPts, uint8_t *flags); +static int simple_points(SFT_Font *font, uint_fast32_t offset, uint_fast16_t numPts, uint8_t *flags, Point *points); +static int decode_contour(uint8_t *flags, uint_fast16_t basePoint, uint_fast16_t count, Outline *outl); +static int simple_outline(SFT_Font *font, uint_fast32_t offset, unsigned int numContours, Outline *outl); +static int compound_outline(SFT_Font *font, uint_fast32_t offset, int recDepth, Outline *outl); +static int decode_outline(SFT_Font *font, uint_fast32_t offset, int recDepth, Outline *outl); +/* tesselation */ +static int is_flat(Outline *outl, Curve curve); +static int tesselate_curve(Curve curve, Outline *outl); +static int tesselate_curves(Outline *outl); +/* silhouette rasterization */ +static void draw_line(Raster buf, Point origin, Point goal); +static void draw_lines(Outline *outl, Raster buf); +/* post-processing */ +static void post_process(Raster buf, uint8_t *image); +/* glyph rendering */ +static int render_outline(Outline *outl, double transform[6], SFT_Image image); + +/* function implementations */ + +const char * +sft_version(void) +{ + return SCHRIFT_VERSION; +} + +/* Loads a font from a user-supplied memory range. */ +SFT_Font * +sft_loadmem(const void *mem, size_t size) +{ + SFT_Font *font; + if (size > UINT32_MAX) { + return NULL; + } + if (!(font = (SFT_Font*)calloc(1, sizeof *font))) { + return NULL; + } + font->memory = (const uint8_t*)mem; + font->size = (uint_fast32_t) size; + font->source = SrcUser; + if (init_font(font) < 0) { + sft_freefont(font); + return NULL; + } + return font; +} + +/* Loads a font from the file system. To do so, it has to map the entire font into memory. */ +SFT_Font * +sft_loadfile(char const *filename) +{ + SFT_Font *font; + if (!(font = (SFT_Font*)calloc(1, sizeof *font))) { + return NULL; + } + if (map_file(font, filename) < 0) { + free(font); + return NULL; + } + if (init_font(font) < 0) { + sft_freefont(font); + return NULL; + } + return font; +} + +void +sft_freefont(SFT_Font *font) +{ + if (!font) return; + /* Only unmap if we mapped it ourselves. */ + if (font->source == SrcMapping) + unmap_file(font); + free(font); +} + +int +sft_lmetrics(const SFT *sft, SFT_LMetrics *metrics) +{ + double factor; + uint_fast32_t hhea; + memset(metrics, 0, sizeof *metrics); + if (gettable(sft->font, "hhea", &hhea) < 0) + return -1; + if (!is_safe_offset(sft->font, hhea, 36)) + return -1; + factor = sft->yScale / sft->font->unitsPerEm; + metrics->ascender = geti16(sft->font, hhea + 4) * factor; + metrics->descender = geti16(sft->font, hhea + 6) * factor; + metrics->lineGap = geti16(sft->font, hhea + 8) * factor; + return 0; +} + +int +sft_lookup(const SFT *sft, SFT_UChar codepoint, SFT_Glyph *glyph) +{ + return glyph_id(sft->font, codepoint, glyph); +} + +int +sft_gmetrics(const SFT *sft, SFT_Glyph glyph, SFT_GMetrics *metrics) +{ + int adv, lsb; + double xScale = sft->xScale / sft->font->unitsPerEm; + uint_fast32_t outline; + int bbox[4]; + + memset(metrics, 0, sizeof *metrics); + + if (hor_metrics(sft->font, glyph, &adv, &lsb) < 0) + return -1; + metrics->advanceWidth = adv * xScale; + metrics->leftSideBearing = lsb * xScale + sft->xOffset; + + if (outline_offset(sft->font, glyph, &outline) < 0) + return -1; + if (!outline) + return 0; + if (glyph_bbox(sft, outline, bbox) < 0) + return -1; + metrics->minWidth = bbox[2] - bbox[0] + 1; + metrics->minHeight = bbox[3] - bbox[1] + 1; + metrics->yOffset = sft->flags & SFT_DOWNWARD_Y ? -bbox[3] : bbox[1]; + + return 0; +} + +int +sft_kerning(const SFT *sft, SFT_Glyph leftGlyph, SFT_Glyph rightGlyph, + SFT_Kerning *kerning) +{ + void *match; + uint_fast32_t offset; + unsigned int numTables, numPairs, length, format, flags; + int value; + uint8_t key[4]; + + memset(kerning, 0, sizeof *kerning); + + if (gettable(sft->font, "kern", &offset) < 0) + return 0; + + /* Read kern table header. */ + if (!is_safe_offset(sft->font, offset, 4)) + return -1; + if (getu16(sft->font, offset) != 0) + return 0; + numTables = getu16(sft->font, offset + 2); + offset += 4; + + while (numTables > 0) { + /* Read subtable header. */ + if (!is_safe_offset(sft->font, offset, 6)) + return -1; + length = getu16(sft->font, offset + 2); + format = getu8 (sft->font, offset + 4); + flags = getu8 (sft->font, offset + 5); + offset += 6; + + if (format == 0 && (flags & HORIZONTAL_KERNING) && !(flags & MINIMUM_KERNING)) { + /* Read format 0 header. */ + if (!is_safe_offset(sft->font, offset, 8)) + return -1; + numPairs = getu16(sft->font, offset); + offset += 8; + /* Look up character code pair via binary search. */ + key[0] = (leftGlyph >> 8) & 0xFF; + key[1] = leftGlyph & 0xFF; + key[2] = (rightGlyph >> 8) & 0xFF; + key[3] = rightGlyph & 0xFF; + if ((match = bsearch(key, sft->font->memory + offset, + numPairs, 6, cmpu32)) != NULL) { + + value = geti16(sft->font, (uint_fast32_t) ((uint8_t *) match - sft->font->memory + 4)); + if (flags & CROSS_STREAM_KERNING) { + kerning->yShift += value; + } else { + kerning->xShift += value; + } + } + + } + + offset += length; + --numTables; + } + + kerning->xShift = kerning->xShift / sft->font->unitsPerEm * sft->xScale; + kerning->yShift = kerning->yShift / sft->font->unitsPerEm * sft->yScale; + + return 0; +} + +int +sft_render(const SFT *sft, SFT_Glyph glyph, SFT_Image image) +{ + uint_fast32_t outline; + double transform[6]; + int bbox[4]; + Outline outl; + + if (outline_offset(sft->font, glyph, &outline) < 0) + return -1; + if (!outline) + return 0; + if (glyph_bbox(sft, outline, bbox) < 0) + return -1; + /* Set up the transformation matrix such that + * the transformed bounding boxes min corner lines + * up with the (0, 0) point. */ + transform[0] = sft->xScale / sft->font->unitsPerEm; + transform[1] = 0.0; + transform[2] = 0.0; + transform[4] = sft->xOffset - bbox[0]; + if (sft->flags & SFT_DOWNWARD_Y) { + transform[3] = -sft->yScale / sft->font->unitsPerEm; + transform[5] = bbox[3] - sft->yOffset; + } else { + transform[3] = +sft->yScale / sft->font->unitsPerEm; + transform[5] = sft->yOffset - bbox[1]; + } + + memset(&outl, 0, sizeof outl); + if (init_outline(&outl) < 0) + goto failure; + + if (decode_outline(sft->font, outline, 0, &outl) < 0) + goto failure; + if (render_outline(&outl, transform, image) < 0) + goto failure; + + free_outline(&outl); + return 0; + +failure: + free_outline(&outl); + return -1; +} + +/* This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX + * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW */ +#define MUL_NO_OVERFLOW ((size_t)1 << (sizeof(size_t) * 4)) + +/* OpenBSD's reallocarray() standard libary function. + * A wrapper for realloc() that takes two size args like calloc(). + * Useful because it eliminates common integer overflow bugs. */ +static void * +reallocarray2(void *optr, size_t nmemb, size_t size) +{ + if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && + nmemb > 0 && SIZE_MAX / nmemb < size) { + errno = ENOMEM; + return NULL; + } + return realloc(optr, size * nmemb); +} + +/* TODO maybe we should use long here instead of int. */ +static inline int +fast_floor(double x) +{ + int i = (int) x; + return i - (i > x); +} + +static inline int +fast_ceil(double x) +{ + int i = (int) x; + return i + (i < x); +} + +#if defined(_WIN32) + +static int +map_file(SFT_Font *font, const char *filename) +{ + HANDLE file; + DWORD high, low; + + font->mapping = NULL; + font->memory = NULL; + + file = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (file == INVALID_HANDLE_VALUE) { + return -1; + } + + low = GetFileSize(file, &high); + if (low == INVALID_FILE_SIZE) { + CloseHandle(file); + return -1; + } + + font->size = (size_t)high << (8 * sizeof(DWORD)) | low; + + font->mapping = CreateFileMapping(file, NULL, PAGE_READONLY, high, low, NULL); + if (!font->mapping) { + CloseHandle(file); + return -1; + } + + CloseHandle(file); + + font->memory = (const uint8_t*)MapViewOfFile(font->mapping, FILE_MAP_READ, 0, 0, 0); + if (!font->memory) { + CloseHandle(font->mapping); + font->mapping = NULL; + return -1; + } + + return 0; +} + +static void +unmap_file(SFT_Font *font) +{ + if (font->memory) { + UnmapViewOfFile(font->memory); + font->memory = NULL; + } + if (font->mapping) { + CloseHandle(font->mapping); + font->mapping = NULL; + } +} + +#else + +static int +map_file(SFT_Font *font, const char *filename) +{ + struct stat info; + int fd; + font->memory = (const uint8_t*)MAP_FAILED; + font->size = 0; + font->source = SrcMapping; + if ((fd = open(filename, O_RDONLY)) < 0) { + return -1; + } + if (fstat(fd, &info) < 0) { + close(fd); + return -1; + } + /* FIXME do some basic validation on info.st_size maybe - it is signed for example, so it *could* be negative .. */ + font->memory = (const uint8_t*)mmap(NULL, (size_t) info.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + font->size = (uint_fast32_t) info.st_size; + close(fd); + return font->memory == MAP_FAILED ? -1 : 0; +} + +static void +unmap_file(SFT_Font *font) +{ + assert(font->memory != MAP_FAILED); + munmap((void *) font->memory, font->size); +} + +#endif + +static int +init_font(SFT_Font *font) +{ + uint_fast32_t scalerType, head, hhea; + + if (!is_safe_offset(font, 0, 12)) + return -1; + /* Check for a compatible scalerType (magic number). */ + scalerType = getu32(font, 0); + if (scalerType != FILE_MAGIC_ONE && scalerType != FILE_MAGIC_TWO) + return -1; + + if (gettable(font, "head", &head) < 0) + return -1; + if (!is_safe_offset(font, head, 54)) + return -1; + font->unitsPerEm = getu16(font, head + 18); + font->locaFormat = geti16(font, head + 50); + + if (gettable(font, "hhea", &hhea) < 0) + return -1; + if (!is_safe_offset(font, hhea, 36)) + return -1; + font->numLongHmtx = getu16(font, hhea + 34); + + return 0; +} + +static Point +midpoint(Point a, Point b) +{ + return { + 0.5 * (a.x + b.x), + 0.5 * (a.y + b.y) + }; +} + +/* Applies an affine linear transformation matrix to a set of points. */ +static void +transform_points(unsigned int numPts, Point *points, double trf[6]) +{ + Point pt; + unsigned int i; + for (i = 0; i < numPts; ++i) { + pt = points[i]; + points[i] = { + pt.x * trf[0] + pt.y * trf[2] + trf[4], + pt.x * trf[1] + pt.y * trf[3] + trf[5] + }; + } +} + +static void +clip_points(unsigned int numPts, Point *points, int width, int height) +{ + Point pt; + unsigned int i; + + for (i = 0; i < numPts; ++i) { + pt = points[i]; + + if (pt.x < 0.0) { + points[i].x = 0.0; + } + if (pt.x >= width) { + points[i].x = nextafter(width, 0.0); + } + if (pt.y < 0.0) { + points[i].y = 0.0; + } + if (pt.y >= height) { + points[i].y = nextafter(height, 0.0); + } + } +} + +static int +init_outline(Outline *outl) +{ + /* TODO Smaller initial allocations */ + outl->numPoints = 0; + outl->capPoints = 64; + if (!(outl->points = (Point*)malloc(outl->capPoints * sizeof *outl->points))) + return -1; + outl->numCurves = 0; + outl->capCurves = 64; + if (!(outl->curves = (Curve*)malloc(outl->capCurves * sizeof *outl->curves))) + return -1; + outl->numLines = 0; + outl->capLines = 64; + if (!(outl->lines = (Line*)malloc(outl->capLines * sizeof *outl->lines))) + return -1; + return 0; +} + +static void +free_outline(Outline *outl) +{ + free(outl->points); + free(outl->curves); + free(outl->lines); +} + +static int +grow_points(Outline *outl) +{ + void *mem; + uint_fast16_t cap; + assert(outl->capPoints); + /* Since we use uint_fast16_t for capacities, we have to be extra careful not to trigger integer overflow. */ + if (outl->capPoints > UINT16_MAX / 2) + return -1; + cap = (uint_fast16_t) (2U * outl->capPoints); + if (!(mem = reallocarray2(outl->points, cap, sizeof *outl->points))) + return -1; + outl->capPoints = (uint_least16_t) cap; + outl->points = (Point*)mem; + return 0; +} + +static int +grow_curves(Outline *outl) +{ + void *mem; + uint_fast16_t cap; + assert(outl->capCurves); + if (outl->capCurves > UINT16_MAX / 2) + return -1; + cap = (uint_fast16_t) (2U * outl->capCurves); + if (!(mem = reallocarray2(outl->curves, cap, sizeof *outl->curves))) + return -1; + outl->capCurves = (uint_least16_t) cap; + outl->curves = (Curve*)mem; + return 0; +} + +static int +grow_lines(Outline *outl) +{ + void *mem; + uint_fast16_t cap; + assert(outl->capLines); + if (outl->capLines > UINT16_MAX / 2) + return -1; + cap = (uint_fast16_t) (2U * outl->capLines); + if (!(mem = reallocarray2(outl->lines, cap, sizeof *outl->lines))) + return -1; + outl->capLines = (uint_least16_t) cap; + outl->lines = (Line*)mem; + return 0; +} + +static inline int +is_safe_offset(SFT_Font *font, uint_fast32_t offset, uint_fast32_t margin) +{ + if (offset > font->size) return 0; + if (font->size - offset < margin) return 0; + return 1; +} + +/* Like bsearch(), but returns the next highest element if key could not be found. */ +static void * +csearch(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar)(const void *, const void *)) +{ + const uint8_t *bytes = (const uint8_t*)base, *sample; + size_t low = 0, high = nmemb - 1, mid; + if (!nmemb) return NULL; + while (low != high) { + mid = low + (high - low) / 2; + sample = bytes + mid * size; + if (compar(key, sample) > 0) { + low = mid + 1; + } else { + high = mid; + } + } + return (uint8_t *) bytes + low * size; +} + +/* Used as a comparison function for [bc]search(). */ +static int +cmpu16(const void *a, const void *b) +{ + return memcmp(a, b, 2); +} + +/* Used as a comparison function for [bc]search(). */ +static int +cmpu32(const void *a, const void *b) +{ + return memcmp(a, b, 4); +} + +static inline uint_least8_t +getu8(SFT_Font *font, uint_fast32_t offset) +{ + assert(offset + 1 <= font->size); + return *(font->memory + offset); +} + +static inline int_least8_t +geti8(SFT_Font *font, uint_fast32_t offset) +{ + return (int_least8_t) getu8(font, offset); +} + +static inline uint_least16_t +getu16(SFT_Font *font, uint_fast32_t offset) +{ + assert(offset + 2 <= font->size); + const uint8_t *base = font->memory + offset; + uint_least16_t b1 = base[0], b0 = base[1]; + return (uint_least16_t) (b1 << 8 | b0); +} + +static inline int16_t +geti16(SFT_Font *font, uint_fast32_t offset) +{ + return (int_least16_t) getu16(font, offset); +} + +static inline uint32_t +getu32(SFT_Font *font, uint_fast32_t offset) +{ + assert(offset + 4 <= font->size); + const uint8_t *base = font->memory + offset; + uint_least32_t b3 = base[0], b2 = base[1], b1 = base[2], b0 = base[3]; + return (uint_least32_t) (b3 << 24 | b2 << 16 | b1 << 8 | b0); +} + +static int +gettable(SFT_Font *font, const char* tag, uint_fast32_t *offset) +{ + void *match; + unsigned int numTables; + /* No need to bounds-check access to the first 12 bytes - this gets already checked by init_font(). */ + numTables = getu16(font, 4); + if (!is_safe_offset(font, 12, (uint_fast32_t) numTables * 16)) + return -1; + if (!(match = bsearch(tag, font->memory + 12, numTables, 16, cmpu32))) + return -1; + *offset = getu32(font, (uint_fast32_t) ((uint8_t *) match - font->memory + 8)); + return 0; +} + +static int +cmap_fmt4(SFT_Font *font, uint_fast32_t table, SFT_UChar charCode, SFT_Glyph *glyph) +{ + const uint8_t *segPtr; + uint_fast32_t segIdxX2; + uint_fast32_t endCodes, startCodes, idDeltas, idRangeOffsets, idOffset; + uint_fast16_t segCountX2, idRangeOffset, startCode, shortCode, idDelta, id; + uint8_t key[2] = { (uint8_t) (charCode >> 8), (uint8_t) charCode }; + /* cmap format 4 only supports the Unicode BMP. */ + if (charCode > 0xFFFF) { + *glyph = 0; + return 0; + } + shortCode = (uint_fast16_t) charCode; + if (!is_safe_offset(font, table, 8)) + return -1; + segCountX2 = getu16(font, table); + if ((segCountX2 & 1) || !segCountX2) + return -1; + /* Find starting positions of the relevant arrays. */ + endCodes = table + 8; + startCodes = endCodes + segCountX2 + 2; + idDeltas = startCodes + segCountX2; + idRangeOffsets = idDeltas + segCountX2; + if (!is_safe_offset(font, idRangeOffsets, segCountX2)) + return -1; + /* Find the segment that contains shortCode by binary searching over + * the highest codes in the segments. */ + segPtr = (const uint8_t*)csearch(key, font->memory + endCodes, segCountX2 / 2, 2, cmpu16); + segIdxX2 = (uint_fast32_t) (segPtr - (font->memory + endCodes)); + /* Look up segment info from the arrays & short circuit if the spec requires. */ + if ((startCode = getu16(font, startCodes + segIdxX2)) > shortCode) + return 0; + idDelta = getu16(font, idDeltas + segIdxX2); + if (!(idRangeOffset = getu16(font, idRangeOffsets + segIdxX2))) { + /* Intentional integer under- and overflow. */ + *glyph = (shortCode + idDelta) & 0xFFFF; + return 0; + } + /* Calculate offset into glyph array and determine ultimate value. */ + idOffset = idRangeOffsets + segIdxX2 + idRangeOffset + 2U * (unsigned int) (shortCode - startCode); + if (!is_safe_offset(font, idOffset, 2)) + return -1; + id = getu16(font, idOffset); + /* Intentional integer under- and overflow. */ + *glyph = id ? (id + idDelta) & 0xFFFF : 0; + return 0; +} + +static int +cmap_fmt6(SFT_Font *font, uint_fast32_t table, SFT_UChar charCode, SFT_Glyph *glyph) +{ + unsigned int firstCode, entryCount; + /* cmap format 6 only supports the Unicode BMP. */ + if (charCode > 0xFFFF) { + *glyph = 0; + return 0; + } + if (!is_safe_offset(font, table, 4)) + return -1; + firstCode = getu16(font, table); + entryCount = getu16(font, table + 2); + if (!is_safe_offset(font, table, 4 + 2 * entryCount)) + return -1; + if (charCode < firstCode) + return -1; + charCode -= firstCode; + if (!(charCode < entryCount)) + return -1; + *glyph = getu16(font, table + 4 + 2 * charCode); + return 0; +} + +static int +cmap_fmt12_13(SFT_Font *font, uint_fast32_t table, SFT_UChar charCode, SFT_Glyph *glyph, int which) +{ + uint32_t len, numEntries; + uint_fast32_t i; + + *glyph = 0; + + /* check that the entire header is present */ + if (!is_safe_offset(font, table, 16)) + return -1; + + len = getu32(font, table + 4); + + /* A minimal header is 16 bytes */ + if (len < 16) + return -1; + + if (!is_safe_offset(font, table, len)) + return -1; + + numEntries = getu32(font, table + 12); + + for (i = 0; i < numEntries; ++i) { + uint32_t firstCode, lastCode, glyphOffset; + firstCode = getu32(font, table + (i * 12) + 16); + lastCode = getu32(font, table + (i * 12) + 16 + 4); + if (charCode < firstCode || charCode > lastCode) + continue; + glyphOffset = getu32(font, table + (i * 12) + 16 + 8); + if (which == 12) + *glyph = (charCode-firstCode) + glyphOffset; + else + *glyph = glyphOffset; + return 0; + } + + return 0; +} + +/* Maps Unicode code points to glyph indices. */ +static int +glyph_id(SFT_Font *font, SFT_UChar charCode, SFT_Glyph *glyph) +{ + uint_fast32_t cmap, entry, table; + unsigned int idx, numEntries; + int type, format; + + *glyph = 0; + + if (gettable(font, "cmap", &cmap) < 0) + return -1; + + if (!is_safe_offset(font, cmap, 4)) + return -1; + numEntries = getu16(font, cmap + 2); + + if (!is_safe_offset(font, cmap, 4 + numEntries * 8)) + return -1; + + /* First look for a 'full repertoire'/non-BMP map. */ + for (idx = 0; idx < numEntries; ++idx) { + entry = cmap + 4 + idx * 8; + type = getu16(font, entry) * 0100 + getu16(font, entry + 2); + /* Complete unicode map */ + if (type == 0004 || type == 0312) { + table = cmap + getu32(font, entry + 4); + if (!is_safe_offset(font, table, 8)) + return -1; + /* Dispatch based on cmap format. */ + format = getu16(font, table); + switch (format) { + case 12: + return cmap_fmt12_13(font, table, charCode, glyph, 12); + default: + return -1; + } + } + } + + /* If no 'full repertoire' cmap was found, try looking for a BMP map. */ + for (idx = 0; idx < numEntries; ++idx) { + entry = cmap + 4 + idx * 8; + type = getu16(font, entry) * 0100 + getu16(font, entry + 2); + /* Unicode BMP */ + if (type == 0003 || type == 0301) { + table = cmap + getu32(font, entry + 4); + if (!is_safe_offset(font, table, 6)) + return -1; + /* Dispatch based on cmap format. */ + switch (getu16(font, table)) { + case 4: + return cmap_fmt4(font, table + 6, charCode, glyph); + case 6: + return cmap_fmt6(font, table + 6, charCode, glyph); + default: + return -1; + } + } + } + + return -1; +} + +static int +hor_metrics(SFT_Font *font, SFT_Glyph glyph, int *advanceWidth, int *leftSideBearing) +{ + uint_fast32_t hmtx, offset, boundary; + if (gettable(font, "hmtx", &hmtx) < 0) + return -1; + if (glyph < font->numLongHmtx) { + /* glyph is inside long metrics segment. */ + offset = hmtx + 4 * glyph; + if (!is_safe_offset(font, offset, 4)) + return -1; + *advanceWidth = getu16(font, offset); + *leftSideBearing = geti16(font, offset + 2); + return 0; + } else { + /* glyph is inside short metrics segment. */ + boundary = hmtx + 4U * (uint_fast32_t) font->numLongHmtx; + if (boundary < 4) + return -1; + + offset = boundary - 4; + if (!is_safe_offset(font, offset, 4)) + return -1; + *advanceWidth = getu16(font, offset); + + offset = boundary + 2 * (glyph - font->numLongHmtx); + if (!is_safe_offset(font, offset, 2)) + return -1; + *leftSideBearing = geti16(font, offset); + return 0; + } +} + +static int +glyph_bbox(const SFT *sft, uint_fast32_t outline, int box[4]) +{ + double xScale, yScale; + /* Read the bounding box from the font file verbatim. */ + if (!is_safe_offset(sft->font, outline, 10)) + return -1; + box[0] = geti16(sft->font, outline + 2); + box[1] = geti16(sft->font, outline + 4); + box[2] = geti16(sft->font, outline + 6); + box[3] = geti16(sft->font, outline + 8); + if (box[2] <= box[0] || box[3] <= box[1]) + return -1; + /* Transform the bounding box into SFT coordinate space. */ + xScale = sft->xScale / sft->font->unitsPerEm; + yScale = sft->yScale / sft->font->unitsPerEm; + box[0] = (int) floor(box[0] * xScale + sft->xOffset); + box[1] = (int) floor(box[1] * yScale + sft->yOffset); + box[2] = (int) ceil (box[2] * xScale + sft->xOffset); + box[3] = (int) ceil (box[3] * yScale + sft->yOffset); + return 0; +} + +/* Returns the offset into the font that the glyph's outline is stored at. */ +static int +outline_offset(SFT_Font *font, SFT_Glyph glyph, uint_fast32_t *offset) +{ + uint_fast32_t loca, glyf; + uint_fast32_t base, this_, next; + + if (gettable(font, "loca", &loca) < 0) + return -1; + if (gettable(font, "glyf", &glyf) < 0) + return -1; + + if (font->locaFormat == 0) { + base = loca + 2 * glyph; + + if (!is_safe_offset(font, base, 4)) + return -1; + + this_ = 2U * (uint_fast32_t) getu16(font, base); + next = 2U * (uint_fast32_t) getu16(font, base + 2); + } else { + base = loca + 4 * glyph; + + if (!is_safe_offset(font, base, 8)) + return -1; + + this_ = getu32(font, base); + next = getu32(font, base + 4); + } + + *offset = this_ == next ? 0 : glyf + this_; + return 0; +} + +/* For a 'simple' outline, determines each point of the outline with a set of flags. */ +static int +simple_flags(SFT_Font *font, uint_fast32_t *offset, uint_fast16_t numPts, uint8_t *flags) +{ + uint_fast32_t off = *offset; + uint_fast16_t i; + uint8_t value = 0, repeat = 0; + for (i = 0; i < numPts; ++i) { + if (repeat) { + --repeat; + } else { + if (!is_safe_offset(font, off, 1)) + return -1; + value = getu8(font, off++); + if (value & REPEAT_FLAG) { + if (!is_safe_offset(font, off, 1)) + return -1; + repeat = getu8(font, off++); + } + } + flags[i] = value; + } + *offset = off; + return 0; +} + +/* For a 'simple' outline, decodes both X and Y coordinates for each point of the outline. */ +static int +simple_points(SFT_Font *font, uint_fast32_t offset, uint_fast16_t numPts, uint8_t *flags, Point *points) +{ + long accum, value, bit; + uint_fast16_t i; + + accum = 0L; + for (i = 0; i < numPts; ++i) { + if (flags[i] & X_CHANGE_IS_SMALL) { + if (!is_safe_offset(font, offset, 1)) + return -1; + value = (long) getu8(font, offset++); + bit = !!(flags[i] & X_CHANGE_IS_POSITIVE); + accum -= (value ^ -bit) + bit; + } else if (!(flags[i] & X_CHANGE_IS_ZERO)) { + if (!is_safe_offset(font, offset, 2)) + return -1; + accum += geti16(font, offset); + offset += 2; + } + points[i].x = (double) accum; + } + + accum = 0L; + for (i = 0; i < numPts; ++i) { + if (flags[i] & Y_CHANGE_IS_SMALL) { + if (!is_safe_offset(font, offset, 1)) + return -1; + value = (long) getu8(font, offset++); + bit = !!(flags[i] & Y_CHANGE_IS_POSITIVE); + accum -= (value ^ -bit) + bit; + } else if (!(flags[i] & Y_CHANGE_IS_ZERO)) { + if (!is_safe_offset(font, offset, 2)) + return -1; + accum += geti16(font, offset); + offset += 2; + } + points[i].y = (double) accum; + } + + return 0; +} + +static int +decode_contour(uint8_t *flags, uint_fast16_t basePoint, uint_fast16_t count, Outline *outl) +{ + uint_fast16_t i; + uint_least16_t looseEnd, beg, ctrl, center, cur; + unsigned int gotCtrl; + + /* Skip contours with less than two points, since the following algorithm can't handle them and + * they should appear invisible either way (because they don't have any area). */ + if (count < 2) return 0; + + assert(basePoint <= UINT16_MAX - count); + + if (flags[0] & POINT_IS_ON_CURVE) { + looseEnd = (uint_least16_t) basePoint++; + ++flags; + --count; + } else if (flags[count - 1] & POINT_IS_ON_CURVE) { + looseEnd = (uint_least16_t) (basePoint + --count); + } else { + if (outl->numPoints >= outl->capPoints && grow_points(outl) < 0) + return -1; + + looseEnd = outl->numPoints; + outl->points[outl->numPoints++] = midpoint( + outl->points[basePoint], + outl->points[basePoint + count - 1]); + } + beg = looseEnd; + gotCtrl = 0; + for (i = 0; i < count; ++i) { + /* cur can't overflow because we ensure that basePoint + count < 0xFFFF before calling decode_contour(). */ + cur = (uint_least16_t) (basePoint + i); + /* NOTE clang-analyzer will often flag this and another piece of code because it thinks that flags and + * outl->points + basePoint don't always get properly initialized -- even when you explicitly loop over both + * and set every element to zero (but not when you use memset). This is a known clang-analyzer bug: + * http://clang-developers.42468.n3.nabble.com/StaticAnalyzer-False-positive-with-loop-handling-td4053875.html */ + if (flags[i] & POINT_IS_ON_CURVE) { + if (gotCtrl) { + if (outl->numCurves >= outl->capCurves && grow_curves(outl) < 0) + return -1; + outl->curves[outl->numCurves++] = { beg, cur, ctrl }; + } else { + if (outl->numLines >= outl->capLines && grow_lines(outl) < 0) + return -1; + outl->lines[outl->numLines++] = { beg, cur }; + } + beg = cur; + gotCtrl = 0; + } else { + if (gotCtrl) { + center = outl->numPoints; + if (outl->numPoints >= outl->capPoints && grow_points(outl) < 0) + return -1; + outl->points[center] = midpoint(outl->points[ctrl], outl->points[cur]); + ++outl->numPoints; + + if (outl->numCurves >= outl->capCurves && grow_curves(outl) < 0) + return -1; + outl->curves[outl->numCurves++] = { beg, center, ctrl }; + + beg = center; + } + ctrl = cur; + gotCtrl = 1; + } + } + if (gotCtrl) { + if (outl->numCurves >= outl->capCurves && grow_curves(outl) < 0) + return -1; + outl->curves[outl->numCurves++] = { beg, looseEnd, ctrl }; + } else { + if (outl->numLines >= outl->capLines && grow_lines(outl) < 0) + return -1; + outl->lines[outl->numLines++] = { beg, looseEnd }; + } + + return 0; +} + +static int +simple_outline(SFT_Font *font, uint_fast32_t offset, unsigned int numContours, Outline *outl) +{ + uint_fast16_t *endPts = NULL; + uint8_t *flags = NULL; + uint_fast16_t numPts; + unsigned int i; + uint_fast16_t beg; + + assert(numContours > 0); + + uint_fast16_t basePoint = outl->numPoints; + + if (!is_safe_offset(font, offset, numContours * 2 + 2)) + goto failure; + numPts = getu16(font, offset + (numContours - 1) * 2); + if (numPts >= UINT16_MAX) + goto failure; + numPts++; + if (outl->numPoints > UINT16_MAX - numPts) + goto failure; + + while (outl->capPoints < basePoint + numPts) { + if (grow_points(outl) < 0) + goto failure; + } + + STACK_ALLOC(endPts, uint_fast16_t, 16, numContours); + if (endPts == NULL) + goto failure; + STACK_ALLOC(flags, uint8_t, 128, numPts); + if (flags == NULL) + goto failure; + + for (i = 0; i < numContours; ++i) { + endPts[i] = getu16(font, offset); + offset += 2; + } + /* Ensure that endPts are never falling. + * Falling endPts have no sensible interpretation and most likely only occur in malicious input. + * Therefore, we bail, should we ever encounter such input. */ + for (i = 0; i < numContours - 1; ++i) { + if (endPts[i + 1] < endPts[i] + 1) + goto failure; + } + offset += 2U + getu16(font, offset); + + if (simple_flags(font, &offset, numPts, flags) < 0) + goto failure; + if (simple_points(font, offset, numPts, flags, outl->points + basePoint) < 0) + goto failure; + outl->numPoints = (uint_least16_t) (outl->numPoints + numPts); + + beg = 0; + for (i = 0; i < numContours; ++i) { + uint_fast16_t count = endPts[i] - beg + 1; + if (decode_contour(flags + beg, basePoint + beg, count, outl) < 0) + goto failure; + beg = endPts[i] + 1; + } + + STACK_FREE(endPts); + STACK_FREE(flags); + return 0; +failure: + STACK_FREE(endPts); + STACK_FREE(flags); + return -1; +} + +static int +compound_outline(SFT_Font *font, uint_fast32_t offset, int recDepth, Outline *outl) +{ + double local[6]; + uint_fast32_t outline; + unsigned int flags, glyph, basePoint; + /* Guard against infinite recursion (compound glyphs that have themselves as component). */ + if (recDepth >= 4) + return -1; + do { + memset(local, 0, sizeof local); + if (!is_safe_offset(font, offset, 4)) + return -1; + flags = getu16(font, offset); + glyph = getu16(font, offset + 2); + offset += 4; + /* We don't implement point matching, and neither does stb_truetype for that matter. */ + if (!(flags & ACTUAL_XY_OFFSETS)) + return -1; + /* Read additional X and Y offsets (in FUnits) of this component. */ + if (flags & OFFSETS_ARE_LARGE) { + if (!is_safe_offset(font, offset, 4)) + return -1; + local[4] = geti16(font, offset); + local[5] = geti16(font, offset + 2); + offset += 4; + } else { + if (!is_safe_offset(font, offset, 2)) + return -1; + local[4] = geti8(font, offset); + local[5] = geti8(font, offset + 1); + offset += 2; + } + if (flags & GOT_A_SINGLE_SCALE) { + if (!is_safe_offset(font, offset, 2)) + return -1; + local[0] = geti16(font, offset) / 16384.0; + local[3] = local[0]; + offset += 2; + } else if (flags & GOT_AN_X_AND_Y_SCALE) { + if (!is_safe_offset(font, offset, 4)) + return -1; + local[0] = geti16(font, offset + 0) / 16384.0; + local[3] = geti16(font, offset + 2) / 16384.0; + offset += 4; + } else if (flags & GOT_A_SCALE_MATRIX) { + if (!is_safe_offset(font, offset, 8)) + return -1; + local[0] = geti16(font, offset + 0) / 16384.0; + local[1] = geti16(font, offset + 2) / 16384.0; + local[2] = geti16(font, offset + 4) / 16384.0; + local[3] = geti16(font, offset + 6) / 16384.0; + offset += 8; + } else { + local[0] = 1.0; + local[3] = 1.0; + } + /* At this point, Apple's spec more or less tells you to scale the matrix by its own L1 norm. + * But stb_truetype scales by the L2 norm. And FreeType2 doesn't scale at all. + * Furthermore, Microsoft's spec doesn't even mention anything like this. + * It's almost as if nobody ever uses this feature anyway. */ + if (outline_offset(font, glyph, &outline) < 0) + return -1; + if (outline) { + basePoint = outl->numPoints; + if (decode_outline(font, outline, recDepth + 1, outl) < 0) + return -1; + transform_points(outl->numPoints - basePoint, outl->points + basePoint, local); + } + } while (flags & THERE_ARE_MORE_COMPONENTS); + + return 0; +} + +static int +decode_outline(SFT_Font *font, uint_fast32_t offset, int recDepth, Outline *outl) +{ + int numContours; + if (!is_safe_offset(font, offset, 10)) + return -1; + numContours = geti16(font, offset); + if (numContours > 0) { + /* Glyph has a 'simple' outline consisting of a number of contours. */ + return simple_outline(font, offset + 10, (unsigned int) numContours, outl); + } else if (numContours < 0) { + /* Glyph has a compound outline combined from mutiple other outlines. */ + return compound_outline(font, offset + 10, recDepth, outl); + } else { + return 0; + } +} + +/* A heuristic to tell whether a given curve can be approximated closely enough by a line. */ +static int +is_flat(Outline *outl, Curve curve) +{ + const double maxArea2 = 2.0; + Point a = outl->points[curve.beg]; + Point b = outl->points[curve.ctrl]; + Point c = outl->points[curve.end]; + Point g = { b.x-a.x, b.y-a.y }; + Point h = { c.x-a.x, c.y-a.y }; + double area2 = fabs(g.x*h.y-h.x*g.y); + return area2 <= maxArea2; +} + +static int +tesselate_curve(Curve curve, Outline *outl) +{ + /* From my tests I can conclude that this stack barely reaches a top height + * of 4 elements even for the largest font sizes I'm willing to support. And + * as space requirements should only grow logarithmically, I think 10 is + * more than enough. */ +#define STACK_SIZE 10 + Curve stack[STACK_SIZE]; + unsigned int top = 0; + for (;;) { + if (is_flat(outl, curve) || top >= STACK_SIZE) { + if (outl->numLines >= outl->capLines && grow_lines(outl) < 0) + return -1; + outl->lines[outl->numLines++] = { curve.beg, curve.end }; + if (top == 0) break; + curve = stack[--top]; + } else { + uint_least16_t ctrl0 = outl->numPoints; + if (outl->numPoints >= outl->capPoints && grow_points(outl) < 0) + return -1; + outl->points[ctrl0] = midpoint(outl->points[curve.beg], outl->points[curve.ctrl]); + ++outl->numPoints; + + uint_least16_t ctrl1 = outl->numPoints; + if (outl->numPoints >= outl->capPoints && grow_points(outl) < 0) + return -1; + outl->points[ctrl1] = midpoint(outl->points[curve.ctrl], outl->points[curve.end]); + ++outl->numPoints; + + uint_least16_t pivot = outl->numPoints; + if (outl->numPoints >= outl->capPoints && grow_points(outl) < 0) + return -1; + outl->points[pivot] = midpoint(outl->points[ctrl0], outl->points[ctrl1]); + ++outl->numPoints; + + stack[top++] = { curve.beg, pivot, ctrl0 }; + curve = { pivot, curve.end, ctrl1 }; + } + } + return 0; +#undef STACK_SIZE +} + +static int +tesselate_curves(Outline *outl) +{ + unsigned int i; + for (i = 0; i < outl->numCurves; ++i) { + if (tesselate_curve(outl->curves[i], outl) < 0) + return -1; + } + return 0; +} + +/* Draws a line into the buffer. Uses a custom 2D raycasting algorithm to do so. */ +static void +draw_line(Raster buf, Point origin, Point goal) +{ + Point delta; + Point nextCrossing; + Point crossingIncr; + double halfDeltaX; + double prevDistance = 0.0, nextDistance; + double xAverage, yDifference; + struct { int x, y; } pixel; + struct { int x, y; } dir; + int step, numSteps = 0; +#ifdef _MSC_VER + Cell *restrict cptr, cell; +#else + Cell *cptr, cell; +#endif + + delta.x = goal.x - origin.x; + delta.y = goal.y - origin.y; + dir.x = SIGN(delta.x); + dir.y = SIGN(delta.y); + + if (!dir.y) { + return; + } + + crossingIncr.x = dir.x ? fabs(1.0 / delta.x) : 1.0; + crossingIncr.y = fabs(1.0 / delta.y); + + if (!dir.x) { + pixel.x = fast_floor(origin.x); + nextCrossing.x = 100.0; + } else { + if (dir.x > 0) { + pixel.x = fast_floor(origin.x); + nextCrossing.x = (origin.x - pixel.x) * crossingIncr.x; + nextCrossing.x = crossingIncr.x - nextCrossing.x; + numSteps += fast_ceil(goal.x) - fast_floor(origin.x) - 1; + } else { + pixel.x = fast_ceil(origin.x) - 1; + nextCrossing.x = (origin.x - pixel.x) * crossingIncr.x; + numSteps += fast_ceil(origin.x) - fast_floor(goal.x) - 1; + } + } + + if (dir.y > 0) { + pixel.y = fast_floor(origin.y); + nextCrossing.y = (origin.y - pixel.y) * crossingIncr.y; + nextCrossing.y = crossingIncr.y - nextCrossing.y; + numSteps += fast_ceil(goal.y) - fast_floor(origin.y) - 1; + } else { + pixel.y = fast_ceil(origin.y) - 1; + nextCrossing.y = (origin.y - pixel.y) * crossingIncr.y; + numSteps += fast_ceil(origin.y) - fast_floor(goal.y) - 1; + } + + nextDistance = MIN(nextCrossing.x, nextCrossing.y); + halfDeltaX = 0.5 * delta.x; + + for (step = 0; step < numSteps; ++step) { + xAverage = origin.x + (prevDistance + nextDistance) * halfDeltaX; + yDifference = (nextDistance - prevDistance) * delta.y; + cptr = &buf.cells[pixel.y * buf.width + pixel.x]; + cell = *cptr; + cell.cover += yDifference; + xAverage -= (double) pixel.x; + cell.area += (1.0 - xAverage) * yDifference; + *cptr = cell; + prevDistance = nextDistance; + int alongX = nextCrossing.x < nextCrossing.y; + pixel.x += alongX ? dir.x : 0; + pixel.y += alongX ? 0 : dir.y; + nextCrossing.x += alongX ? crossingIncr.x : 0.0; + nextCrossing.y += alongX ? 0.0 : crossingIncr.y; + nextDistance = MIN(nextCrossing.x, nextCrossing.y); + } + + xAverage = origin.x + (prevDistance + 1.0) * halfDeltaX; + yDifference = (1.0 - prevDistance) * delta.y; + cptr = &buf.cells[pixel.y * buf.width + pixel.x]; + cell = *cptr; + cell.cover += yDifference; + xAverage -= (double) pixel.x; + cell.area += (1.0 - xAverage) * yDifference; + *cptr = cell; +} + +static void +draw_lines(Outline *outl, Raster buf) +{ + unsigned int i; + for (i = 0; i < outl->numLines; ++i) { + Line line = outl->lines[i]; + Point origin = outl->points[line.beg]; + Point goal = outl->points[line.end]; + draw_line(buf, origin, goal); + } +} + +/* Integrate the values in the buffer to arrive at the final grayscale image. */ +static void +post_process(Raster buf, uint8_t *image) +{ + Cell cell; + double accum = 0.0, value; + unsigned int i, num; + num = (unsigned int) buf.width * (unsigned int) buf.height; + for (i = 0; i < num; ++i) { + cell = buf.cells[i]; + value = fabs(accum + cell.area); + value = MIN(value, 1.0); + value = value * 255.0 + 0.5; + image[i] = (uint8_t) value; + accum += cell.cover; + } +} + +static int +render_outline(Outline *outl, double transform[6], SFT_Image image) +{ + Cell *cells = NULL; + Raster buf; + unsigned int numPixels; + + numPixels = (unsigned int) image.width * (unsigned int) image.height; + + STACK_ALLOC(cells, Cell, 128 * 128, numPixels); + if (!cells) { + return -1; + } + memset(cells, 0, numPixels * sizeof *cells); + buf.cells = cells; + buf.width = image.width; + buf.height = image.height; + + transform_points(outl->numPoints, outl->points, transform); + + clip_points(outl->numPoints, outl->points, image.width, image.height); + + if (tesselate_curves(outl) < 0) { + STACK_FREE(cells); + return -1; + } + + draw_lines(outl, buf); + + post_process(buf, (uint8_t*)image.pixels); + + STACK_FREE(cells); + return 0; +} + diff --git a/libraries/ZWidget/src/core/schrift/schrift.h b/libraries/ZWidget/src/core/schrift/schrift.h new file mode 100644 index 0000000000..93d20516ae --- /dev/null +++ b/libraries/ZWidget/src/core/schrift/schrift.h @@ -0,0 +1,95 @@ +/* This file is part of libschrift. + * + * © 2019-2022 Thomas Oltmann and contributors + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ + +#ifndef SCHRIFT_H +#define SCHRIFT_H 1 + +#include /* size_t */ +#include /* uint_fast32_t, uint_least32_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +#define SFT_DOWNWARD_Y 0x01 + +typedef struct SFT SFT; +typedef struct SFT_Font SFT_Font; +typedef uint_least32_t SFT_UChar; /* Guaranteed to be compatible with char32_t. */ +typedef uint_fast32_t SFT_Glyph; +typedef struct SFT_LMetrics SFT_LMetrics; +typedef struct SFT_GMetrics SFT_GMetrics; +typedef struct SFT_Kerning SFT_Kerning; +typedef struct SFT_Image SFT_Image; + +struct SFT +{ + SFT_Font *font; + double xScale; + double yScale; + double xOffset; + double yOffset; + int flags; +}; + +struct SFT_LMetrics +{ + double ascender; + double descender; + double lineGap; +}; + +struct SFT_GMetrics +{ + double advanceWidth; + double leftSideBearing; + int yOffset; + int minWidth; + int minHeight; +}; + +struct SFT_Kerning +{ + double xShift; + double yShift; +}; + +struct SFT_Image +{ + void *pixels; + int width; + int height; +}; + +const char *sft_version(void); + +SFT_Font *sft_loadmem (const void *mem, size_t size); +SFT_Font *sft_loadfile(const char *filename); +void sft_freefont(SFT_Font *font); + +int sft_lmetrics(const SFT *sft, SFT_LMetrics *metrics); +int sft_lookup (const SFT *sft, SFT_UChar codepoint, SFT_Glyph *glyph); +int sft_gmetrics(const SFT *sft, SFT_Glyph glyph, SFT_GMetrics *metrics); +int sft_kerning (const SFT *sft, SFT_Glyph leftGlyph, SFT_Glyph rightGlyph, + SFT_Kerning *kerning); +int sft_render (const SFT *sft, SFT_Glyph glyph, SFT_Image image); + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/libraries/ZWidget/src/core/span_layout.cpp b/libraries/ZWidget/src/core/span_layout.cpp new file mode 100644 index 0000000000..1b68f6ef73 --- /dev/null +++ b/libraries/ZWidget/src/core/span_layout.cpp @@ -0,0 +1,883 @@ + +#include "core/span_layout.h" +#include "core/canvas.h" +#include "core/widget.h" +#include "core/font.h" +#include "core/image.h" + +SpanLayout::SpanLayout() +{ +} + +SpanLayout::~SpanLayout() +{ +} + +void SpanLayout::Clear() +{ + objects.clear(); + text.clear(); + lines.clear(); +} + +std::vector SpanLayout::GetRectById(int id) const +{ + std::vector segment_rects; + + double x = position.x; + double y = position.y; + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + const Line& line = lines[line_index]; + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + const LineSegment& segment = line.segments[segment_index]; + if (segment.id == id) + { + segment_rects.push_back(Rect(x + segment.x_position, y, segment.width, y + line.height)); + } + } + y += line.height; + } + + return segment_rects; +} + +void SpanLayout::DrawLayout(Canvas* canvas) +{ + double x = position.x; + double y = position.y; + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + Line& line = lines[line_index]; + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + switch (segment.type) + { + case object_text: + DrawLayoutText(canvas, line, segment, x, y); + break; + case object_image: + DrawLayoutImage(canvas, line, segment, x, y); + break; + case object_component: + break; + } + + } + + if (line_index + 1 == lines.size() && !line.segments.empty()) + { + LineSegment& segment = line.segments.back(); + if (cursor_visible && segment.end <= cursor_pos) + { + switch (segment.type) + { + case object_text: + { + double cursor_x = x + segment.x_position + canvas->measureText(segment.font, text.substr(segment.start, segment.end - segment.start)).width; + double cursor_width = 1; + canvas->fillRect(Rect::ltrb(cursor_x, y + line.ascender - segment.ascender, cursor_width, y + line.ascender + segment.descender), cursor_color); + } + break; + } + } + } + + y += line.height; + } +} + +void SpanLayout::DrawLayoutEllipsis(Canvas* canvas, const Rect& content_rect) +{ + is_ellipsis_draw = true; + ellipsis_content_rect = content_rect; + try + { + is_ellipsis_draw = false; + DrawLayout(canvas); + } + catch (...) + { + is_ellipsis_draw = false; + throw; + } +} + +void SpanLayout::DrawLayoutImage(Canvas* canvas, Line& line, LineSegment& segment, double x, double y) +{ + canvas->drawImage(segment.image, Point(x + segment.x_position, y + line.ascender - segment.ascender)); +} + +void SpanLayout::DrawLayoutText(Canvas* canvas, Line& line, LineSegment& segment, double x, double y) +{ + std::string segment_text = text.substr(segment.start, segment.end - segment.start); + + int length = (int)segment_text.length(); + int s1 = clamp((int)sel_start - (int)segment.start, 0, length); + int s2 = clamp((int)sel_end - (int)segment.start, 0, length); + + if (s1 != s2) + { + double xx = x + segment.x_position; + double xx0 = xx + canvas->measureText(segment.font, segment_text.substr(0, s1)).width; + double xx1 = xx0 + canvas->measureText(segment.font, segment_text.substr(s1, s2 - s1)).width; + double sel_width = canvas->measureText(segment.font, segment_text.substr(s1, s2 - s1)).width; + + canvas->fillRect(Rect::ltrb(xx0, y + line.ascender - segment.ascender, xx1, y + line.ascender + segment.descender), sel_background); + + if (cursor_visible && cursor_pos >= segment.start && cursor_pos < segment.end) + { + double cursor_x = x + segment.x_position + canvas->measureText(segment.font, text.substr(segment.start, cursor_pos - segment.start)).width; + double cursor_width = cursor_overwrite_mode ? canvas->measureText(segment.font, text.substr(cursor_pos, 1)).width : 1; + canvas->fillRect(Rect::ltrb(cursor_x, y + line.ascender - segment.ascender, cursor_x + cursor_width, y + line.ascender + segment.descender), cursor_color); + } + + if (s1 > 0) + { + if (is_ellipsis_draw) + canvas->drawTextEllipsis(segment.font, Point(xx, y + line.ascender), ellipsis_content_rect, segment_text.substr(0, s1), segment.color); + else + canvas->drawText(segment.font, Point(xx, y + line.ascender), segment_text.substr(0, s1), segment.color); + } + if (is_ellipsis_draw) + canvas->drawTextEllipsis(segment.font, Point(xx0, y + line.ascender), ellipsis_content_rect, segment_text.substr(s1, s2 - s1), sel_foreground); + else + canvas->drawText(segment.font, Point(xx0, y + line.ascender), segment_text.substr(s1, s2 - s1), sel_foreground); + xx += sel_width; + if (s2 < length) + { + if (is_ellipsis_draw) + canvas->drawTextEllipsis(segment.font, Point(xx1, y + line.ascender), ellipsis_content_rect, segment_text.substr(s2), segment.color); + else + canvas->drawText(segment.font, Point(xx1, y + line.ascender), segment_text.substr(s2), segment.color); + } + } + else + { + if (cursor_visible && cursor_pos >= segment.start && cursor_pos < segment.end) + { + double cursor_x = x + segment.x_position + canvas->measureText(segment.font, text.substr(segment.start, cursor_pos - segment.start)).width; + double cursor_width = cursor_overwrite_mode ? canvas->measureText(segment.font, text.substr(cursor_pos, 1)).width : 1; + canvas->fillRect(Rect::ltrb(cursor_x, y + line.ascender - segment.ascender, cursor_x + cursor_width, y + line.ascender + segment.descender), cursor_color); + } + + if (is_ellipsis_draw) + canvas->drawTextEllipsis(segment.font, Point(x + segment.x_position, y + line.ascender), ellipsis_content_rect, segment_text, segment.color); + else + canvas->drawText(segment.font, Point(x + segment.x_position, y + line.ascender), segment_text, segment.color); + } +} + +SpanLayout::HitTestResult SpanLayout::HitTest(Canvas* canvas, const Point& pos) +{ + SpanLayout::HitTestResult result; + + if (lines.empty()) + { + result.type = SpanLayout::HitTestResult::no_objects_available; + return result; + } + + double x = position.x; + double y = position.y; + + // Check if we are outside to the top + if (pos.y < y) + { + result.type = SpanLayout::HitTestResult::outside_top; + result.object_id = lines[0].segments[0].id; + result.offset = 0; + return result; + } + + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + Line& line = lines[line_index]; + + // Check if we found current line + if (pos.y >= y && pos.y <= y + line.height) + { + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + + // Check if we are outside to the left + if (segment_index == 0 && pos.x < x) + { + result.type = SpanLayout::HitTestResult::outside_left; + result.object_id = segment.id; + result.offset = segment.start; + return result; + } + + // Check if we are inside a segment + if (pos.x >= x + segment.x_position && pos.x <= x + segment.x_position + segment.width) + { + std::string segment_text = text.substr(segment.start, segment.end - segment.start); + Point hit_point(pos.x - x - segment.x_position, 0); + size_t offset = segment.start + canvas->getCharacterIndex(segment.font, segment_text, hit_point); + + result.type = SpanLayout::HitTestResult::inside; + result.object_id = segment.id; + result.offset = offset; + return result; + } + + // Check if we are outside to the right + if (segment_index == line.segments.size() - 1 && pos.x > x + segment.x_position + segment.width) + { + result.type = SpanLayout::HitTestResult::outside_right; + result.object_id = segment.id; + result.offset = segment.end; + return result; + } + } + } + + y += line.height; + } + + // We are outside to the bottom + const Line& last_line = lines[lines.size() - 1]; + const LineSegment& last_segment = last_line.segments[last_line.segments.size() - 1]; + + result.type = SpanLayout::HitTestResult::outside_bottom; + result.object_id = last_segment.id; + result.offset = last_segment.end; + return result; +} + +Size SpanLayout::GetSize() const +{ + return GetRect().size(); +} + +Rect SpanLayout::GetRect() const +{ + double x = position.x; + double y = position.y; + + const double max_value = 0x70000000; + double left = max_value; + double top = max_value; + double right = -max_value; + double bottom = -max_value; + + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + const Line& line = lines[line_index]; + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + const LineSegment& segment = line.segments[segment_index]; + Rect area(Point(x + segment.x_position, y), Size(segment.width, line.height)); + + left = std::min(left, area.left()); + right = std::max(right, area.right()); + top = std::min(top, area.top()); + bottom = std::max(bottom, area.bottom()); + } + y += line.height; + } + if (left > right) + left = right = position.x; + + if (top > bottom) + top = bottom = position.y; + + return Rect::ltrb(left, top, right, bottom); +} + +void SpanLayout::AddText(const std::string& more_text, std::shared_ptr font, const Colorf& color, int id) +{ + SpanObject object; + object.type = object_text; + object.start = text.length(); + object.end = object.start + more_text.length(); + object.font = font; + object.color = color; + object.id = id; + objects.push_back(object); + text += more_text; +} + +void SpanLayout::AddImage(std::shared_ptr image, double baseline_offset, int id) +{ + SpanObject object; + object.type = object_image; + object.image = image; + object.baseline_offset = baseline_offset; + object.id = id; + object.start = text.length(); + object.end = object.start + 1; + objects.push_back(object); + text += "*"; +} + +void SpanLayout::AddWidget(Widget* component, double baseline_offset, int id) +{ + SpanObject object; + object.type = object_component; + object.component = component; + object.baseline_offset = baseline_offset; + object.id = id; + object.start = text.length(); + object.end = object.start + 1; + objects.push_back(object); + text += "*"; +} + +void SpanLayout::Layout(Canvas* canvas, double max_width) +{ + LayoutLines(canvas, max_width); + + switch (alignment) + { + case span_right: AlignRight(max_width); break; + case span_center: AlignCenter(max_width); break; + case span_justify: AlignJustify(max_width); break; + case span_left: + default: break; + } +} + +void SpanLayout::SetPosition(const Point& pos) +{ + position = pos; +} + +SpanLayout::TextSizeResult SpanLayout::FindTextSize(Canvas* canvas, const TextBlock& block, size_t object_index) +{ + std::shared_ptr font = objects[object_index].font; + if (layout_cache.object_index != (int)object_index) + { + layout_cache.object_index = (int)object_index; + layout_cache.metrics = canvas->getFontMetrics(font); + } + + TextSizeResult result; + result.start = block.start; + size_t pos = block.start; + double x_position = 0; + while (pos != block.end) + { + size_t end = std::min(objects[object_index].end, block.end); + std::string subtext = text.substr(pos, end - pos); + + Size text_size = canvas->measureText(font, subtext).size(); + + result.width += text_size.width; + result.height = std::max(result.height, layout_cache.metrics.height + layout_cache.metrics.external_leading); + result.ascender = std::max(result.ascender, layout_cache.metrics.ascent); + result.descender = std::max(result.descender, layout_cache.metrics.descent); + + LineSegment segment; + segment.type = object_text; + segment.start = pos; + segment.end = end; + segment.font = objects[object_index].font; + segment.color = objects[object_index].color; + segment.id = objects[object_index].id; + segment.x_position = x_position; + segment.width = text_size.width; + segment.ascender = layout_cache.metrics.ascent; + segment.descender = layout_cache.metrics.descent; + x_position += text_size.width; + result.segments.push_back(segment); + + pos = end; + if (pos == objects[object_index].end) + { + object_index++; + result.objects_traversed++; + + if (object_index < objects.size()) + { + layout_cache.object_index = (int)object_index; + font = objects[object_index].font; + layout_cache.metrics = canvas->getFontMetrics(font); + } + } + } + result.end = pos; + return result; +} + +std::vector SpanLayout::FindTextBlocks() +{ + std::vector blocks; + std::vector::iterator block_object_it; + + // Find first object that is not text: + for (block_object_it = objects.begin(); block_object_it != objects.end() && (*block_object_it).type == object_text; ++block_object_it); + + std::string::size_type pos = 0; + while (pos < text.size()) + { + // Find end of text block: + std::string::size_type end_pos; + switch (text[pos]) + { + case ' ': + case '\t': + case '\n': + end_pos = text.find_first_not_of(text[pos], pos); + break; + default: + end_pos = text.find_first_of(" \t\n", pos); + break; + } + + if (end_pos == std::string::npos) + end_pos = text.length(); + + // If we traversed past an object that is not text: + if (block_object_it != objects.end() && (*block_object_it).start < end_pos) + { + // End text block + end_pos = (*block_object_it).start; + if (end_pos > pos) + { + TextBlock block; + block.start = pos; + block.end = end_pos; + blocks.push_back(block); + } + + // Create object block: + pos = end_pos; + end_pos = pos + 1; + + TextBlock block; + block.start = pos; + block.end = end_pos; + blocks.push_back(block); + + // Find next object that is not text: + for (++block_object_it; block_object_it != objects.end() && (*block_object_it).type == object_text; ++block_object_it); + } + else + { + if (end_pos > pos) + { + TextBlock block; + block.start = pos; + block.end = end_pos; + blocks.push_back(block); + } + } + + pos = end_pos; + } + + return blocks; +} + +void SpanLayout::SetAlign(SpanAlign align) +{ + alignment = align; +} + +void SpanLayout::LayoutLines(Canvas* canvas, double max_width) +{ + lines.clear(); + if (objects.empty()) + return; + + layout_cache.metrics = {}; + layout_cache.object_index = -1; + + CurrentLine current_line; + std::vector blocks = FindTextBlocks(); + for (std::vector::size_type block_index = 0; block_index < blocks.size(); block_index++) + { + if (objects[current_line.object_index].type == object_text) + LayoutText(canvas, blocks, block_index, current_line, max_width); + else + LayoutBlock(current_line, max_width, blocks, block_index); + } + NextLine(current_line); +} + +void SpanLayout::LayoutBlock(CurrentLine& current_line, double max_width, std::vector& blocks, std::vector::size_type block_index) +{ + if (objects[current_line.object_index].float_type == float_none) + LayoutInlineBlock(current_line, max_width, blocks, block_index); + else + LayoutFloatBlock(current_line, max_width); + + current_line.object_index++; +} + +void SpanLayout::LayoutInlineBlock(CurrentLine& current_line, double max_width, std::vector& blocks, std::vector::size_type block_index) +{ + Size size; + LineSegment segment; + if (objects[current_line.object_index].type == object_image) + { + size = Size(objects[current_line.object_index].image->GetWidth(), objects[current_line.object_index].image->GetHeight()); + segment.type = object_image; + segment.image = objects[current_line.object_index].image; + } + else if (objects[current_line.object_index].type == object_component) + { + size = objects[current_line.object_index].component->GetSize(); + segment.type = object_component; + segment.component = objects[current_line.object_index].component; + } + + if (current_line.x_position + size.width > max_width) + NextLine(current_line); + + segment.x_position = current_line.x_position; + segment.width = size.width; + segment.start = blocks[block_index].start; + segment.end = blocks[block_index].end; + segment.id = objects[current_line.object_index].id; + segment.ascender = size.height - objects[current_line.object_index].baseline_offset; + current_line.cur_line.segments.push_back(segment); + current_line.cur_line.height = std::max(current_line.cur_line.height, size.height + objects[current_line.object_index].baseline_offset); + current_line.cur_line.ascender = std::max(current_line.cur_line.ascender, segment.ascender); + current_line.x_position += size.width; +} + +void SpanLayout::LayoutFloatBlock(CurrentLine& current_line, double max_width) +{ + FloatBox floatbox; + floatbox.type = objects[current_line.object_index].type; + floatbox.image = objects[current_line.object_index].image; + floatbox.component = objects[current_line.object_index].component; + floatbox.id = objects[current_line.object_index].id; + if (objects[current_line.object_index].type == object_image) + floatbox.rect = Rect::xywh(0, current_line.y_position, floatbox.image->GetWidth(), floatbox.image->GetHeight()); + else if (objects[current_line.object_index].type == object_component) + floatbox.rect = Rect::xywh(0, current_line.y_position, floatbox.component->GetWidth(), floatbox.component->GetHeight()); + + if (objects[current_line.object_index].float_type == float_left) + floats_left.push_back(FloatBoxLeft(floatbox, max_width)); + else + floats_right.push_back(FloatBoxRight(floatbox, max_width)); + + ReflowLine(current_line, max_width); +} + +void SpanLayout::ReflowLine(CurrentLine& step, double max_width) +{ +} + +SpanLayout::FloatBox SpanLayout::FloatBoxLeft(FloatBox box, double max_width) +{ + return FloatBoxAny(box, max_width, floats_left); +} + +SpanLayout::FloatBox SpanLayout::FloatBoxRight(FloatBox box, double max_width) +{ + return FloatBoxAny(box, max_width, floats_right); +} + +SpanLayout::FloatBox SpanLayout::FloatBoxAny(FloatBox box, double max_width, const std::vector& floats1) +{ + bool restart; + do + { + restart = false; + for (size_t i = 0; i < floats1.size(); i++) + { + double top = std::max(floats1[i].rect.top(), box.rect.top()); + double bottom = std::min(floats1[i].rect.bottom(), box.rect.bottom()); + if (bottom > top && box.rect.left() < floats1[i].rect.right()) + { + Size s = box.rect.size(); + box.rect.x = floats1[i].rect.x; + box.rect.width = s.width; + + if (!BoxFitsOnLine(box, max_width)) + { + box.rect.x = 0; + box.rect.width = s.width; + box.rect.y = floats1[i].rect.bottom(); + box.rect.height = s.height; + restart = true; + break; + } + } + } + } while (restart); + return box; +} + +bool SpanLayout::BoxFitsOnLine(const FloatBox& box, double max_width) +{ + for (size_t i = 0; i < floats_right.size(); i++) + { + double top = std::max(floats_right[i].rect.top(), box.rect.top()); + double bottom = std::min(floats_right[i].rect.bottom(), box.rect.bottom()); + if (bottom > top) + { + if (box.rect.right() + floats_right[i].rect.right() > max_width) + return false; + } + } + return true; +} + +void SpanLayout::LayoutText(Canvas* canvas, std::vector blocks, std::vector::size_type block_index, CurrentLine& current_line, double max_width) +{ + TextSizeResult text_size_result = FindTextSize(canvas, blocks[block_index], current_line.object_index); + current_line.object_index += text_size_result.objects_traversed; + + current_line.cur_line.width = current_line.x_position; + + if (IsNewline(blocks[block_index])) + { + current_line.cur_line.height = std::max(current_line.cur_line.height, text_size_result.height); + current_line.cur_line.ascender = std::max(current_line.cur_line.ascender, text_size_result.ascender); + NextLine(current_line); + } + else + { + if (!FitsOnLine(current_line.x_position, text_size_result, max_width) && !IsWhitespace(blocks[block_index])) + { + if (LargerThanLine(text_size_result, max_width)) + { + // force line breaks to make it fit + ForcePlaceLineSegments(current_line, text_size_result, max_width); + } + else + { + NextLine(current_line); + PlaceLineSegments(current_line, text_size_result); + } + } + else + { + PlaceLineSegments(current_line, text_size_result); + } + } +} + +void SpanLayout::NextLine(CurrentLine& current_line) +{ + current_line.cur_line.width = current_line.x_position; + for (std::vector::reverse_iterator it = current_line.cur_line.segments.rbegin(); it != current_line.cur_line.segments.rend(); ++it) + { + LineSegment& segment = *it; + if (segment.type == object_text) + { + std::string s = text.substr(segment.start, segment.end - segment.start); + if (s.find_first_not_of(" \t\r\n") != std::string::npos) + { + current_line.cur_line.width = segment.x_position + segment.width; + break; + } + else + { + // We remove the width so that GetRect() reports the correct sizes + segment.width = 0; + } + } + else + { + current_line.cur_line.width = segment.x_position + segment.width; + break; + } + } + + double height = current_line.cur_line.height; + lines.push_back(current_line.cur_line); + current_line.cur_line = Line(); + current_line.x_position = 0; + current_line.y_position += height; +} + +void SpanLayout::PlaceLineSegments(CurrentLine& current_line, TextSizeResult& text_size_result) +{ + for (std::vector::iterator it = text_size_result.segments.begin(); it != text_size_result.segments.end(); ++it) + { + LineSegment segment = *it; + segment.x_position += current_line.x_position; + current_line.cur_line.segments.push_back(segment); + } + current_line.x_position += text_size_result.width; + current_line.cur_line.height = std::max(current_line.cur_line.height, text_size_result.height); + current_line.cur_line.ascender = std::max(current_line.cur_line.ascender, text_size_result.ascender); +} + +void SpanLayout::ForcePlaceLineSegments(CurrentLine& current_line, TextSizeResult& text_size_result, double max_width) +{ + if (current_line.x_position != 0) + NextLine(current_line); + + // to do: do this properly - for now we just place the entire block on one line + PlaceLineSegments(current_line, text_size_result); +} + +bool SpanLayout::IsNewline(const TextBlock& block) +{ + return block.start != block.end && text[block.start] == '\n'; +} + +bool SpanLayout::IsWhitespace(const TextBlock& block) +{ + return block.start != block.end && text[block.start] == ' '; +} + +bool SpanLayout::FitsOnLine(double x_position, const TextSizeResult& text_size_result, double max_width) +{ + return x_position + text_size_result.width <= max_width; +} + +bool SpanLayout::LargerThanLine(const TextSizeResult& text_size_result, double max_width) +{ + return text_size_result.width > max_width; +} + +void SpanLayout::AlignRight(double max_width) +{ + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + Line& line = lines[line_index]; + double offset = max_width - line.width; + if (offset < 0) offset = 0; + + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + segment.x_position += offset; + } + } +} + +void SpanLayout::AlignCenter(double max_width) +{ + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + Line& line = lines[line_index]; + double offset = (max_width - line.width) / 2; + if (offset < 0) offset = 0; + + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + segment.x_position += offset; + } + } +} + +void SpanLayout::AlignJustify(double max_width) +{ + // Note, we do not justify the last line + for (std::vector::size_type line_index = 0; line_index + 1 < lines.size(); line_index++) + { + Line& line = lines[line_index]; + double offset = max_width - line.width; + if (offset < 0) offset = 0; + + if (line.segments.size() <= 1) // Do not justify line if only one word exists + continue; + + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + segment.x_position += (offset * segment_index) / (line.segments.size() - 1); + } + } +} + +Size SpanLayout::FindPreferredSize(Canvas* canvas) +{ + LayoutLines(canvas, 0x70000000); // Feed it with a very long length so it ends up on one line + return GetRect().size(); +} + +void SpanLayout::SetSelectionRange(std::string::size_type start, std::string::size_type end) +{ + sel_start = start; + sel_end = end; + if (sel_end < sel_start) + sel_end = sel_start; +} + +void SpanLayout::SetSelectionColors(const Colorf& foreground, const Colorf& background) +{ + sel_foreground = foreground; + sel_background = background; +} + +void SpanLayout::ShowCursor() +{ + cursor_visible = true; +} + +void SpanLayout::HideCursor() +{ + cursor_visible = false; +} + +void SpanLayout::SetCursorPos(std::string::size_type pos) +{ + cursor_pos = pos; +} + +void SpanLayout::SetCursorOverwriteMode(bool enable) +{ + cursor_overwrite_mode = enable; +} + +void SpanLayout::SetCursorColor(const Colorf& color) +{ + cursor_color = color; +} + +std::string SpanLayout::GetCombinedText() const +{ + return text; +} + +void SpanLayout::SetComponentGeometry() +{ + double x = position.x; + double y = position.y; + for (size_t i = 0; i < lines.size(); i++) + { + for (size_t j = 0; j < lines[i].segments.size(); j++) + { + if (lines[i].segments[j].type == object_component) + { + Point pos(x + lines[i].segments[j].x_position, y + lines[i].ascender - lines[i].segments[j].ascender); + Size size = lines[i].segments[j].component->GetSize(); + Rect rect(pos, size); + lines[i].segments[j].component->SetFrameGeometry(rect); + } + } + y += lines[i].height; + } +} + +double SpanLayout::GetFirstBaselineOffset() +{ + if (!lines.empty()) + { + return lines.front().ascender; + } + else + { + return 0; + } +} + +double SpanLayout::GetLastBaselineOffset() +{ + if (!lines.empty()) + { + double y = 0; + for (size_t i = 0; i + 1 < lines.size(); i++) + y += lines[i].height; + return y + lines.back().ascender; + } + else + { + return 0; + } +} diff --git a/libraries/ZWidget/src/core/timer.cpp b/libraries/ZWidget/src/core/timer.cpp new file mode 100644 index 0000000000..4c748561b7 --- /dev/null +++ b/libraries/ZWidget/src/core/timer.cpp @@ -0,0 +1,43 @@ + +#include "core/timer.h" +#include "core/widget.h" +#include "window/window.h" + +Timer::Timer(Widget* owner) : OwnerObj(owner) +{ + PrevTimerObj = owner->FirstTimerObj; + if (PrevTimerObj) + PrevTimerObj->PrevTimerObj = this; + owner->FirstTimerObj = this; +} + +Timer::~Timer() +{ + if (PrevTimerObj) + PrevTimerObj->NextTimerObj = NextTimerObj; + if (NextTimerObj) + NextTimerObj->PrevTimerObj = PrevTimerObj; + if (OwnerObj->FirstTimerObj == this) + OwnerObj->FirstTimerObj = NextTimerObj; +} + +void Timer::Start(int timeoutMilliseconds, bool repeat) +{ + Stop(); + + TimerId = DisplayWindow::StartTimer(timeoutMilliseconds, [=]() { + if (!repeat) + Stop(); + if (FuncExpired) + FuncExpired(); + }); +} + +void Timer::Stop() +{ + if (TimerId != 0) + { + DisplayWindow::StopTimer(TimerId); + TimerId = 0; + } +} diff --git a/libraries/ZWidget/src/core/utf8reader.cpp b/libraries/ZWidget/src/core/utf8reader.cpp new file mode 100644 index 0000000000..200da9fdc4 --- /dev/null +++ b/libraries/ZWidget/src/core/utf8reader.cpp @@ -0,0 +1,153 @@ +/* +** Copyright (c) 1997-2015 Mark Page +** +** This software is provided 'as-is', without any express or implied +** warranty. In no event will the authors be held liable for any damages +** arising from the use of this software. +** +** Permission is granted to anyone to use this software for any purpose, +** including commercial applications, and to alter it and redistribute it +** freely, subject to the following restrictions: +** +** 1. The origin of this software must not be misrepresented; you must not +** claim that you wrote the original software. If you use this software +** in a product, an acknowledgment in the product documentation would be +** appreciated but is not required. +** 2. Altered source versions must be plainly marked as such, and must not be +** misrepresented as being the original software. +** 3. This notice may not be removed or altered from any source distribution. +** +*/ + +#include "core/utf8reader.h" + +namespace +{ + static const char trailing_bytes_for_utf8[256] = + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5 + }; + + static const unsigned char bitmask_leadbyte_for_utf8[6] = + { + 0x7f, + 0x1f, + 0x0f, + 0x07, + 0x03, + 0x01 + }; +} + +UTF8Reader::UTF8Reader(const std::string::value_type *text, std::string::size_type length) : length(length), data((unsigned char *)text) +{ +} + +bool UTF8Reader::is_end() +{ + return current_position >= length; +} + +unsigned int UTF8Reader::character() +{ + if (current_position >= length) + return 0; + + int trailing_bytes = trailing_bytes_for_utf8[data[current_position]]; + if (trailing_bytes == 0 && (data[current_position] & 0x80) == 0x80) + return '?'; + + if (current_position + 1 + trailing_bytes > length) + { + return '?'; + } + else + { + unsigned int ucs4 = (data[current_position] & bitmask_leadbyte_for_utf8[trailing_bytes]); + for (std::string::size_type i = 0; i < trailing_bytes; i++) + { + if ((data[current_position + 1 + i] & 0xC0) == 0x80) + ucs4 = (ucs4 << 6) + (data[current_position + 1 + i] & 0x3f); + else + return '?'; + } + + // To do: verify that the ucs4 value is in the range for the trailing_bytes specified in the lead byte. + + return ucs4; + } + +} + +std::string::size_type UTF8Reader::char_length() +{ + if (current_position < length) + { + int trailing_bytes = trailing_bytes_for_utf8[data[current_position]]; + if (current_position + 1 + trailing_bytes > length) + return 1; + + for (std::string::size_type i = 0; i < trailing_bytes; i++) + { + if ((data[current_position + 1 + i] & 0xC0) != 0x80) + return 1; + } + + return 1 + trailing_bytes; + } + else + { + return 0; + } +} + +void UTF8Reader::prev() +{ + if (current_position > length) + current_position = length; + + if (current_position > 0) + { + current_position--; + move_to_leadbyte(); + } +} + +void UTF8Reader::next() +{ + current_position += char_length(); + +} + +void UTF8Reader::move_to_leadbyte() +{ + if (current_position < length) + { + int lead_position = (int)current_position; + + while (lead_position > 0 && (data[lead_position] & 0xC0) == 0x80) + lead_position--; + + int trailing_bytes = trailing_bytes_for_utf8[data[lead_position]]; + if (lead_position + trailing_bytes >= current_position) + current_position = lead_position; + } + +} + +std::string::size_type UTF8Reader::position() +{ + return current_position; +} + +void UTF8Reader::set_position(std::string::size_type position) +{ + current_position = position; +} diff --git a/libraries/ZWidget/src/core/widget.cpp b/libraries/ZWidget/src/core/widget.cpp new file mode 100644 index 0000000000..9c6dc63190 --- /dev/null +++ b/libraries/ZWidget/src/core/widget.cpp @@ -0,0 +1,653 @@ + +#include "core/widget.h" +#include "core/timer.h" +#include "core/colorf.h" +#include + +Widget::Widget(Widget* parent, WidgetType type) : Type(type) +{ + if (type != WidgetType::Child) + { + DispWindow = DisplayWindow::Create(this); + DispCanvas = Canvas::create(DispWindow.get()); + } + + SetParent(parent); +} + +Widget::~Widget() +{ + while (LastChildObj) + delete LastChildObj; + + while (FirstTimerObj) + delete FirstTimerObj; + + DetachFromParent(); +} + +void Widget::SetParent(Widget* newParent) +{ + if (ParentObj != newParent) + { + if (ParentObj) + DetachFromParent(); + + if (newParent) + { + PrevSiblingObj = newParent->LastChildObj; + if (PrevSiblingObj) PrevSiblingObj->NextSiblingObj = this; + newParent->LastChildObj = this; + if (!newParent->FirstChildObj) newParent->FirstChildObj = this; + ParentObj = newParent; + } + } +} + +void Widget::MoveBefore(Widget* sibling) +{ + if (sibling && sibling->ParentObj != ParentObj) throw std::runtime_error("Invalid sibling passed to Widget.MoveBefore"); + if (!ParentObj) throw std::runtime_error("Widget must have a parent before it can be moved"); + + if (NextSiblingObj != sibling) + { + Widget* p = ParentObj; + DetachFromParent(); + + ParentObj = p; + if (sibling) + { + NextSiblingObj = sibling; + PrevSiblingObj = sibling->PrevSiblingObj; + sibling->PrevSiblingObj = this; + if (PrevSiblingObj) PrevSiblingObj->NextSiblingObj = this; + if (ParentObj->FirstChildObj == sibling) ParentObj->FirstChildObj = this; + } + else + { + PrevSiblingObj = ParentObj->LastChildObj; + if (PrevSiblingObj) PrevSiblingObj->NextSiblingObj = this; + ParentObj->LastChildObj = this; + if (!ParentObj->FirstChildObj) ParentObj->FirstChildObj = this; + } + } +} + +void Widget::DetachFromParent() +{ + for (Widget* cur = ParentObj; cur; cur = cur->ParentObj) + { + if (cur->FocusWidget == this) + cur->FocusWidget = nullptr; + if (cur->CaptureWidget == this) + cur->CaptureWidget = nullptr; + if (cur->HoverWidget == this) + cur->HoverWidget = nullptr; + + if (cur->DispWindow) + break; + } + + if (PrevSiblingObj) + PrevSiblingObj->NextSiblingObj = NextSiblingObj; + if (NextSiblingObj) + NextSiblingObj->PrevSiblingObj = PrevSiblingObj; + if (ParentObj) + { + if (ParentObj->FirstChildObj == this) + ParentObj->FirstChildObj = NextSiblingObj; + if (ParentObj->LastChildObj == this) + ParentObj->LastChildObj = PrevSiblingObj; + } + PrevSiblingObj = nullptr; + NextSiblingObj = nullptr; + ParentObj = nullptr; +} + +std::string Widget::GetWindowTitle() const +{ + return WindowTitle; +} + +void Widget::SetWindowTitle(const std::string& text) +{ + if (WindowTitle != text) + { + WindowTitle = text; + if (DispWindow) + DispWindow->SetWindowTitle(WindowTitle); + } +} + +Size Widget::GetSize() const +{ + return ContentGeometry.size(); +} + +Rect Widget::GetFrameGeometry() const +{ + if (Type == WidgetType::Child) + { + return FrameGeometry; + } + else + { + return DispWindow->GetWindowFrame(); + } +} + +void Widget::SetNoncontentSizes(double left, double top, double right, double bottom) +{ + Noncontent.Left = left; + Noncontent.Top = top; + Noncontent.Right = right; + Noncontent.Bottom = bottom; +} + +void Widget::SetFrameGeometry(const Rect& geometry) +{ + if (Type == WidgetType::Child) + { + FrameGeometry = geometry; + double left = FrameGeometry.left() + Noncontent.Left; + double top = FrameGeometry.top() + Noncontent.Top; + double right = FrameGeometry.right() - Noncontent.Right; + double bottom = FrameGeometry.bottom() - Noncontent.Bottom; + left = std::min(left, FrameGeometry.right()); + top = std::min(top, FrameGeometry.bottom()); + right = std::max(right, FrameGeometry.left()); + bottom = std::max(bottom, FrameGeometry.top()); + ContentGeometry = Rect::ltrb(left, top, right, bottom); + OnGeometryChanged(); + } + else + { + DispWindow->SetWindowFrame(geometry); + } +} + +void Widget::Show() +{ + if (Type != WidgetType::Child) + { + DispWindow->Show(); + } +} + +void Widget::ShowFullscreen() +{ + if (Type != WidgetType::Child) + { + DispWindow->ShowFullscreen(); + } +} + +void Widget::ShowMaximized() +{ + if (Type != WidgetType::Child) + { + DispWindow->ShowMaximized(); + } +} + +void Widget::ShowMinimized() +{ + if (Type != WidgetType::Child) + { + DispWindow->ShowMinimized(); + } +} + +void Widget::ShowNormal() +{ + if (Type != WidgetType::Child) + { + DispWindow->ShowNormal(); + } +} + +void Widget::Hide() +{ + if (Type != WidgetType::Child) + { + if (DispWindow) + DispWindow->Hide(); + } +} + +void Widget::ActivateWindow() +{ + if (Type != WidgetType::Child) + { + DispWindow->Activate(); + } +} + +void Widget::Close() +{ + OnClose(); +} + +void Widget::SetWindowBackground(const Colorf& color) +{ + Widget* w = Window(); + if (w && w->WindowBackground != color) + { + w->WindowBackground = color; + Update(); + } +} + +void Widget::SetWindowBorderColor(const Colorf& color) +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->SetBorderColor(color.toBgra8()); + w->DispWindow->Update(); + } +} + +void Widget::SetWindowCaptionColor(const Colorf& color) +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->SetCaptionColor(color.toBgra8()); + w->DispWindow->Update(); + } +} + +void Widget::SetWindowCaptionTextColor(const Colorf& color) +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->SetCaptionTextColor(color.toBgra8()); + w->DispWindow->Update(); + } +} + +void Widget::Update() +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->Update(); + } +} + +void Widget::Repaint() +{ + Widget* w = Window(); + w->DispCanvas->begin(WindowBackground); + w->Paint(w->DispCanvas.get()); + w->DispCanvas->end(); +} + +void Widget::Paint(Canvas* canvas) +{ + Point oldOrigin = canvas->getOrigin(); + canvas->pushClip(FrameGeometry); + canvas->setOrigin(oldOrigin + FrameGeometry.topLeft()); + OnPaintFrame(canvas); + canvas->setOrigin(oldOrigin); + canvas->popClip(); + + canvas->pushClip(ContentGeometry); + canvas->setOrigin(oldOrigin + ContentGeometry.topLeft()); + OnPaint(canvas); + for (Widget* w = FirstChild(); w != nullptr; w = w->NextSibling()) + { + if (w->Type == WidgetType::Child) + w->Paint(canvas); + } + canvas->setOrigin(oldOrigin); + canvas->popClip(); +} + +bool Widget::GetKeyState(EInputKey key) +{ + Widget* window = Window(); + return window ? window->DispWindow->GetKeyState(key) : false; +} + +bool Widget::HasFocus() +{ + Widget* window = Window(); + return window ? window->FocusWidget == this : false; +} + +bool Widget::IsEnabled() +{ + return true; +} + +bool Widget::IsVisible() +{ + return true; +} + +void Widget::SetFocus() +{ + Widget* window = Window(); + if (window) + { + if (window->FocusWidget) + window->FocusWidget->OnLostFocus(); + window->FocusWidget = this; + window->FocusWidget->OnSetFocus(); + window->ActivateWindow(); + } +} + +void Widget::SetEnabled(bool value) +{ +} + +void Widget::LockCursor() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != this) + { + w->CaptureWidget = this; + w->DispWindow->LockCursor(); + } +} + +void Widget::UnlockCursor() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != nullptr) + { + w->CaptureWidget = nullptr; + w->DispWindow->UnlockCursor(); + } +} + +void Widget::SetCursor(StandardCursor cursor) +{ + if (CurrentCursor != cursor) + { + CurrentCursor = cursor; + if (HoverWidget == this || CaptureWidget == this) + { + Widget* w = Window(); + if (w) + { + w->DispWindow->SetCursor(CurrentCursor); + } + } + } +} + +void Widget::CaptureMouse() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != this) + { + w->CaptureWidget = this; + w->DispWindow->CaptureMouse(); + } +} + +void Widget::ReleaseMouseCapture() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != nullptr) + { + w->CaptureWidget = nullptr; + w->DispWindow->ReleaseMouseCapture(); + } +} + +std::string Widget::GetClipboardText() +{ + Widget* w = Window(); + if (w) + return w->DispWindow->GetClipboardText(); + else + return {}; +} + +void Widget::SetClipboardText(const std::string& text) +{ + Widget* w = Window(); + if (w) + w->DispWindow->SetClipboardText(text); +} + +Widget* Widget::Window() +{ + for (Widget* w = this; w != nullptr; w = w->Parent()) + { + if (w->DispWindow) + return w; + } + return nullptr; +} + +Canvas* Widget::GetCanvas() +{ + for (Widget* w = this; w != nullptr; w = w->Parent()) + { + if (w->DispCanvas) + return w->DispCanvas.get(); + } + return nullptr; +} + +Widget* Widget::ChildAt(const Point& pos) +{ + for (Widget* cur = LastChild(); cur != nullptr; cur = cur->PrevSibling()) + { + if (cur->FrameGeometry.contains(pos)) + { + Widget* cur2 = cur->ChildAt(pos - cur->ContentGeometry.topLeft()); + return cur2 ? cur2 : cur; + } + } + return nullptr; +} + +Point Widget::MapFrom(const Widget* parent, const Point& pos) const +{ + Point p = pos; + for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) + { + if (cur == parent) + return p; + p -= cur->ContentGeometry.topLeft(); + } + throw std::runtime_error("MapFrom: not a parent of widget"); +} + +Point Widget::MapFromGlobal(const Point& pos) const +{ + Point p = pos; + for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) + { + if (cur->DispWindow) + { + return p - cur->GetFrameGeometry().topLeft(); + } + p -= cur->ContentGeometry.topLeft(); + } + throw std::runtime_error("MapFromGlobal: no window widget found"); +} + +Point Widget::MapTo(const Widget* parent, const Point& pos) const +{ + Point p = pos; + for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) + { + if (cur == parent) + return p; + p += cur->ContentGeometry.topLeft(); + } + throw std::runtime_error("MapTo: not a parent of widget"); +} + +Point Widget::MapToGlobal(const Point& pos) const +{ + Point p = pos; + for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) + { + if (cur->DispWindow) + { + return cur->GetFrameGeometry().topLeft() + p; + } + p += cur->ContentGeometry.topLeft(); + } + throw std::runtime_error("MapFromGlobal: no window widget found"); +} + +void Widget::OnWindowPaint() +{ + Repaint(); +} + +void Widget::OnWindowMouseMove(const Point& pos) +{ + if (CaptureWidget) + { + DispWindow->SetCursor(CaptureWidget->CurrentCursor); + CaptureWidget->OnMouseMove(CaptureWidget->MapFrom(this, pos)); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + + if (HoverWidget != widget) + { + if (HoverWidget) + HoverWidget->OnMouseLeave(); + HoverWidget = widget; + } + + DispWindow->SetCursor(widget->CurrentCursor); + widget->OnMouseMove(widget->MapFrom(this, pos)); + } +} + +void Widget::OnWindowMouseDown(const Point& pos, EInputKey key) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseDown(CaptureWidget->MapFrom(this, pos), key); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + widget->OnMouseDown(widget->MapFrom(this, pos), key); + } +} + +void Widget::OnWindowMouseDoubleclick(const Point& pos, EInputKey key) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseDoubleclick(CaptureWidget->MapFrom(this, pos), key); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + widget->OnMouseDoubleclick(widget->MapFrom(this, pos), key); + } +} + +void Widget::OnWindowMouseUp(const Point& pos, EInputKey key) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseUp(CaptureWidget->MapFrom(this, pos), key); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + widget->OnMouseUp(widget->MapFrom(this, pos), key); + } +} + +void Widget::OnWindowMouseWheel(const Point& pos, EInputKey key) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseWheel(CaptureWidget->MapFrom(this, pos), key); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + widget->OnMouseWheel(widget->MapFrom(this, pos), key); + } +} + +void Widget::OnWindowRawMouseMove(int dx, int dy) +{ + if (CaptureWidget) + { + CaptureWidget->OnRawMouseMove(dx, dy); + } + else if (FocusWidget) + { + FocusWidget->OnRawMouseMove(dx, dy); + } +} + +void Widget::OnWindowKeyChar(std::string chars) +{ + if (FocusWidget) + FocusWidget->OnKeyChar(chars); +} + +void Widget::OnWindowKeyDown(EInputKey key) +{ + if (FocusWidget) + FocusWidget->OnKeyDown(key); +} + +void Widget::OnWindowKeyUp(EInputKey key) +{ + if (FocusWidget) + FocusWidget->OnKeyUp(key); +} + +void Widget::OnWindowGeometryChanged() +{ + Size size = DispWindow->GetClientSize(); + FrameGeometry = Rect::xywh(0.0, 0.0, size.width, size.height); + ContentGeometry = FrameGeometry; + OnGeometryChanged(); +} + +void Widget::OnWindowClose() +{ + Close(); +} + +void Widget::OnWindowActivated() +{ +} + +void Widget::OnWindowDeactivated() +{ +} + +void Widget::OnWindowDpiScaleChanged() +{ +} + +Size Widget::GetScreenSize() +{ + return DisplayWindow::GetScreenSize(); +} diff --git a/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp b/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp new file mode 100644 index 0000000000..deed9d5078 --- /dev/null +++ b/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp @@ -0,0 +1,91 @@ + +#include "widgets/checkboxlabel/checkboxlabel.h" + +CheckboxLabel::CheckboxLabel(Widget* parent) : Widget(parent) +{ +} + +void CheckboxLabel::SetText(const std::string& value) +{ + if (text != value) + { + text = value; + Update(); + } +} + +const std::string& CheckboxLabel::GetText() const +{ + return text; +} + +void CheckboxLabel::SetChecked(bool value) +{ + if (value != checked) + { + checked = value; + Update(); + } +} + +bool CheckboxLabel::GetChecked() const +{ + return checked; +} + +double CheckboxLabel::GetPreferredHeight() const +{ + return 20.0; +} + +void CheckboxLabel::OnPaint(Canvas* canvas) +{ + + if (checked) + { + canvas->fillRect(Rect::xywh(0.0, GetHeight() * 0.5 - 6.0, 10.0, 10.0), Colorf::fromRgba8(100, 100, 100)); + canvas->fillRect(Rect::xywh(1.0, GetHeight() * 0.5 - 5.0, 8.0, 8.0), Colorf::fromRgba8(51, 51, 51)); + canvas->fillRect(Rect::xywh(2.0, GetHeight() * 0.5 - 4.0, 6.0, 6.0), Colorf::fromRgba8(226, 223, 219)); + } + else + { + canvas->fillRect(Rect::xywh(0.0, GetHeight() * 0.5 - 6.0, 10.0, 10.0), Colorf::fromRgba8(99, 99, 99)); + canvas->fillRect(Rect::xywh(1.0, GetHeight() * 0.5 - 5.0, 8.0, 8.0), Colorf::fromRgba8(51, 51, 51)); + } + + canvas->drawText(Point(14.0, GetHeight() - 5.0), Colorf::fromRgba8(255, 255, 255), text); +} + +void CheckboxLabel::OnMouseDown(const Point& pos, int key) +{ + mouseDownActive = true; + SetFocus(); +} + +void CheckboxLabel::OnMouseUp(const Point& pos, int key) +{ + if (mouseDownActive) + { + Toggle(); + } + mouseDownActive = false; +} + +void CheckboxLabel::OnMouseLeave() +{ + mouseDownActive = false; +} + +void CheckboxLabel::OnKeyUp(EInputKey key) +{ + if (key == IK_Space) + Toggle(); +} + +void CheckboxLabel::Toggle() +{ + bool oldchecked = checked; + checked = radiostyle? true : !checked; + Update(); + if (checked != oldchecked && FuncChanged) FuncChanged(checked); +} diff --git a/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp b/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp new file mode 100644 index 0000000000..d81a39ab13 --- /dev/null +++ b/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp @@ -0,0 +1,32 @@ + +#include "widgets/imagebox/imagebox.h" + +ImageBox::ImageBox(Widget* parent) : Widget(parent) +{ +} + +double ImageBox::GetPreferredHeight() const +{ + if (image) + return (double)image->GetHeight(); + else + return 0.0; +} + +void ImageBox::SetImage(std::shared_ptr newImage) +{ + if (image != newImage) + { + image = newImage; + Update(); + } +} + +void ImageBox::OnPaint(Canvas* canvas) +{ + canvas->fillRect(Rect::xywh(0.0, 0.0, GetWidth(), GetHeight()), Colorf::fromRgba8(0, 0, 0)); + if (image) + { + canvas->drawImage(image, Point((GetWidth() - (double)image->GetWidth()) * 0.5, (GetHeight() - (double)image->GetHeight()) * 0.5)); + } +} diff --git a/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp b/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp new file mode 100644 index 0000000000..bab04c3fb1 --- /dev/null +++ b/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp @@ -0,0 +1,1188 @@ + +#include "widgets/lineedit/lineedit.h" +#include "core/utf8reader.h" +#include "core/colorf.h" + +LineEdit::LineEdit(Widget* parent) : Widget(parent) +{ + SetNoncontentSizes(5.0, 3.0, 5.0, 3.0); + + timer = new Timer(this); + timer->FuncExpired = [=]() { OnTimerExpired(); }; + + scroll_timer = new Timer(this); + scroll_timer->FuncExpired = [=]() { OnScrollTimerExpired(); }; + + SetCursor(StandardCursor::ibeam); +} + +LineEdit::~LineEdit() +{ +} + +bool LineEdit::IsReadOnly() const +{ + return readonly; +} + +LineEdit::Alignment LineEdit::GetAlignment() const +{ + return align_left; +} + +bool LineEdit::IsLowercase() const +{ + return lowercase; +} + +bool LineEdit::IsUppercase() const +{ + return uppercase; +} + +bool LineEdit::IsPasswordMode() const +{ + return password_mode; +} + +int LineEdit::GetMaxLength() const +{ + return max_length; +} + +std::string LineEdit::GetText() const +{ + return text; +} + +int LineEdit::GetTextInt() const +{ + return std::atoi(text.c_str()); +} + +float LineEdit::GetTextFloat() const +{ + return (float)std::atof(text.c_str()); +} + +std::string LineEdit::GetSelection() const +{ + int start = std::min(selection_start, selection_start + selection_length); + return text.substr(start, std::abs(selection_length)); +} + +int LineEdit::GetSelectionStart() const +{ + return selection_start; +} + +int LineEdit::GetSelectionLength() const +{ + return selection_length; +} + +int LineEdit::GetCursorPos() const +{ + return cursor_pos; +} + +Size LineEdit::GetTextSize() +{ + Canvas* canvas = GetCanvas(); + return GetVisualTextSize(canvas); +} + +Size LineEdit::GetTextSize(const std::string& str) +{ + Canvas* canvas = GetCanvas(); + return canvas->measureText(str).size(); +} + +double LineEdit::GetPreferredContentWidth() +{ + return GetTextSize().width; +} + +double LineEdit::GetPreferredContentHeight(double width) +{ + return GetTextSize().height; +} + +void LineEdit::SelectAll() +{ + SetTextSelection(0, (int)text.size()); + Update(); +} + +void LineEdit::SetReadOnly(bool enable) +{ + if (readonly != enable) + { + readonly = enable; + Update(); + } +} + +void LineEdit::SetAlignment(Alignment alignment) +{ + if (alignment != alignment) + { + alignment = alignment; + Update(); + } +} + +void LineEdit::SetLowercase(bool enable) +{ + if (lowercase != enable) + { + lowercase = enable; + text = ToLower(text); + Update(); + } +} + +void LineEdit::SetUppercase(bool enable) +{ + if (uppercase != enable) + { + uppercase = enable; + text = ToUpper(text); + Update(); + } +} + +void LineEdit::SetPasswordMode(bool enable) +{ + if (password_mode != enable) + { + password_mode = enable; + Update(); + } +} + +void LineEdit::SetMaxLength(int length) +{ + if (max_length != length) + { + max_length = length; + if ((int)text.length() > length) + { + if (FuncBeforeEditChanged) + FuncBeforeEditChanged(); + text = text.substr(0, length); + if (FuncAfterEditChanged) + FuncAfterEditChanged(); + } + Update(); + } +} + +void LineEdit::SetText(const std::string& newtext) +{ + if (lowercase) + text = ToLower(newtext); + else if (uppercase) + text = ToUpper(newtext); + else + text = newtext; + + clip_start_offset = 0; + UpdateTextClipping(); + SetCursorPos((int)text.size()); + SetTextSelection(0, 0); + Update(); +} + +void LineEdit::SetTextInt(int number) +{ + text = std::to_string(number); + clip_start_offset = 0; + UpdateTextClipping(); + SetCursorPos((int)text.size()); + SetTextSelection(0, 0); + Update(); +} + +void LineEdit::SetTextFloat(float number, int num_decimal_places) +{ + text = ToFixed(number, num_decimal_places); + clip_start_offset = 0; + UpdateTextClipping(); + SetCursorPos((int)text.size()); + SetTextSelection(0, 0); + Update(); +} + +void LineEdit::SetSelection(int pos, int length) +{ + //don't call FuncSelectionChanged() here, because this + //member is for public usage + selection_start = pos; + selection_length = length; + Update(); +} + +void LineEdit::ClearSelection() +{ + //don't call FuncSelectionChanged() here, because this + //member is for public usage + SetSelection(0, 0); + Update(); +} + +void LineEdit::DeleteSelectedText() +{ + if (GetSelectionLength() == 0) + return; + + int sel_start = selection_start; + int sel_end = selection_start + selection_length; + if (sel_start > sel_end) + std::swap(sel_start, sel_end); + + text = text.substr(0, sel_start) + text.substr(sel_end, text.size()); + cursor_pos = sel_start; + SetTextSelection(0, 0); + int old_pos = GetCursorPos(); + SetCursorPos(0); + SetCursorPos(old_pos); +} + +void LineEdit::SetCursorPos(int pos) +{ + cursor_pos = pos; + UpdateTextClipping(); + Update(); +} + +void LineEdit::SetInputMask(const std::string& mask) +{ + input_mask = mask; +} + +void LineEdit::SetNumericMode(bool enable, bool decimals) +{ + numeric_mode = enable; + numeric_mode_decimals = decimals; +} + +void LineEdit::SetDecimalCharacter(const std::string& new_decimal_char) +{ + decimal_char = new_decimal_char; +} + +void LineEdit::SetSelectAllOnFocusGain(bool enable) +{ + select_all_on_focus_gain = enable; +} + +void LineEdit::OnMouseMove(const Point& pos) +{ + if (mouse_selecting && !ignore_mouse_events) + { + if (pos.x < 0.0 || pos.x >= GetWidth()) + { + if (pos.x < 0.0) + mouse_moves_left = true; + else + mouse_moves_left = false; + + if (!readonly) + scroll_timer->Start(50, true); + } + else + { + scroll_timer->Stop(); + cursor_pos = GetCharacterIndex(pos.x); + SetSelectionLength(cursor_pos - selection_start); + Update(); + } + } +} + +void LineEdit::OnMouseDown(const Point& pos, int key) +{ + if (key == IK_LeftMouse) + { + if (HasFocus()) + { + CaptureMouse(); + mouse_selecting = true; + cursor_pos = GetCharacterIndex(pos.x); + SetTextSelection(cursor_pos, 0); + } + else + { + SetFocus(); + } + Update(); + } +} + +void LineEdit::OnMouseDoubleclick(const Point& pos, int key) +{ +} + +void LineEdit::OnMouseUp(const Point& pos, int key) +{ + if (mouse_selecting && key == IK_LeftMouse) + { + if (ignore_mouse_events) // This prevents text selection from changing from what was set when focus was gained. + { + ReleaseMouseCapture(); + ignore_mouse_events = false; + mouse_selecting = false; + } + else + { + scroll_timer->Stop(); + ReleaseMouseCapture(); + mouse_selecting = false; + int sel_end = GetCharacterIndex(pos.x); + SetSelectionLength(sel_end - selection_start); + cursor_pos = sel_end; + SetFocus(); + Update(); + } + } +} + +void LineEdit::OnKeyChar(std::string chars) +{ + if (FuncFilterKeyChar) + { + chars = FuncFilterKeyChar(chars); + if (chars.empty()) + return; + } + + if (!chars.empty() && !(chars[0] >= 0 && chars[0] < 32)) + { + if (FuncBeforeEditChanged) + FuncBeforeEditChanged(); + + DeleteSelectedText(); + if (input_mask.empty()) + { + if (numeric_mode) + { + // '-' can only be added once, and only as the first character. + if (chars == "-" && cursor_pos == 0 && text.find("-") == std::string::npos) + { + if (InsertText(cursor_pos, chars)) + cursor_pos += (int)chars.size(); + } + else if (numeric_mode_decimals && chars == decimal_char && cursor_pos > 0) // add decimal char + { + if (text.find(decimal_char) == std::string::npos) // allow only one decimal char. + { + if (InsertText(cursor_pos, chars)) + cursor_pos += (int)chars.size(); + } + } + else if (numeric_mode_characters.find(chars) != std::string::npos) // 0-9 + { + if (InsertText(cursor_pos, chars)) + cursor_pos += (int)chars.size(); + } + } + else + { + // not in any special mode, just insert the string. + if (InsertText(cursor_pos, chars)) + cursor_pos += (int)chars.size(); + } + } + else + { + if (InputMaskAcceptsInput(cursor_pos, chars)) + { + if (InsertText(cursor_pos, chars)) + cursor_pos += (int)chars.size(); + } + } + UpdateTextClipping(); + + if (FuncAfterEditChanged) + FuncAfterEditChanged(); + } +} + +void LineEdit::OnKeyDown(EInputKey key) +{ + if (FuncIgnoreKeyDown && FuncIgnoreKeyDown(key)) + return; + + if (key == IK_Enter) + { + if (FuncEnterPressed) + FuncEnterPressed(); + return; + } + + if (!readonly) // Do not flash cursor when readonly + { + cursor_blink_visible = true; + timer->Start(500); // don't blink cursor when moving or typing. + } + + if (key == IK_Enter || key == IK_Escape || key == IK_Tab) + { + // Do not consume these. + return; + } + else if (key == IK_A && GetKeyState(IK_Ctrl)) + { + // select all + SetTextSelection(0, (int)text.size()); + cursor_pos = selection_length; + UpdateTextClipping(); + Update(); + } + else if (key == IK_C && GetKeyState(IK_Ctrl)) + { + if (!password_mode) // Do not allow copying the password to clipboard + { + std::string str = GetSelection(); + SetClipboardText(str); + } + } + else if (readonly) + { + // Do not consume messages on read only component (only allow CTRL-A and CTRL-C) + return; + } + else if (key == IK_Left) + { + Move(-1, GetKeyState(IK_Ctrl), GetKeyState(IK_Shift)); + } + else if (key == IK_Right) + { + Move(1, GetKeyState(IK_Ctrl), GetKeyState(IK_Shift)); + } + else if (key == IK_Backspace) + { + Backspace(); + UpdateTextClipping(); + } + else if (key == IK_Delete) + { + Del(); + UpdateTextClipping(); + } + else if (key == IK_Home) + { + SetSelectionStart(cursor_pos); + cursor_pos = 0; + if (GetKeyState(IK_Shift)) + SetSelectionLength(-selection_start); + else + SetTextSelection(0, 0); + UpdateTextClipping(); + Update(); + } + else if (key == IK_End) + { + SetSelectionStart(cursor_pos); + cursor_pos = (int)text.size(); + if (GetKeyState(IK_Shift)) + SetSelectionLength((int)text.size() - selection_start); + else + SetTextSelection(0, 0); + UpdateTextClipping(); + Update(); + } + else if (key == IK_X && GetKeyState(IK_Ctrl)) + { + std::string str = GetSelection(); + DeleteSelectedText(); + SetClipboardText(str); + UpdateTextClipping(); + } + else if (key == IK_V && GetKeyState(IK_Ctrl)) + { + std::string str = GetClipboardText(); + std::string::const_iterator end_str = std::remove(str.begin(), str.end(), '\n'); + str.resize(end_str - str.begin()); + end_str = std::remove(str.begin(), str.end(), '\r'); + str.resize(end_str - str.begin()); + DeleteSelectedText(); + + if (input_mask.empty()) + { + if (numeric_mode) + { + std::string present_text = GetText(); + + bool present_minus = present_text.find('-') != std::string::npos; + bool str_minus = str.find('-') != std::string::npos; + + if (!present_minus || !str_minus) + { + if ((!present_minus && !str_minus) || //if no minus found + (str_minus && cursor_pos == 0 && str[0] == '-') || //if there's minus in text to paste + (present_minus && cursor_pos > 0)) //if there's minus in the beginning of control's text + { + if (numeric_mode_decimals) + { + std::string::size_type decimal_point_pos; + if ((decimal_point_pos = str.find_first_not_of(numeric_mode_characters, str[0] == '-' ? 1 : 0)) == std::string::npos) //no decimal char inside string to paste + { //we don't look at the position of decimal char inside of text in the textbox, if it's present + if (InsertText(cursor_pos, str)) + SetCursorPos(cursor_pos + (int)str.length()); + } + else + { + if (present_text.find(decimal_char) == std::string::npos && + str[decimal_point_pos] == decimal_char[0] && + str.find_first_not_of(numeric_mode_characters, decimal_point_pos + 1) == std::string::npos) //allow only one decimal char in the string to paste + { + if (InsertText(cursor_pos, str)) + SetCursorPos(cursor_pos + (int)str.length()); + } + } + } + else + { + if (str.find_first_not_of(numeric_mode_characters, str[0] == '-' ? 1 : 0) == std::string::npos) + { + if (InsertText(cursor_pos, str)) + SetCursorPos(cursor_pos + (int)str.length()); + } + } + } + } + } + else + { + if (InsertText(cursor_pos, str)) + SetCursorPos(cursor_pos + (int)str.length()); + } + } + else + { + if (InputMaskAcceptsInput(cursor_pos, str)) + { + if (InsertText(cursor_pos, str)) + SetCursorPos(cursor_pos + (int)str.length()); + } + } + + UpdateTextClipping(); + } + else if (GetKeyState(IK_Ctrl) && key == IK_Z) + { + if (!readonly) + { + std::string tmp = undo_info.undo_text; + undo_info.undo_text = GetText(); + SetText(tmp); + } + } + else if (key == IK_Shift) + { + if (selection_start == -1) + SetTextSelection(cursor_pos, 0); + } + + if (FuncAfterEditChanged) + FuncAfterEditChanged(); +} + +void LineEdit::OnKeyUp(EInputKey key) +{ +} + +void LineEdit::OnSetFocus() +{ + if (!readonly) + timer->Start(500); + if (select_all_on_focus_gain) + SelectAll(); + ignore_mouse_events = true; + cursor_pos = (int)text.length(); + + Update(); + + if (FuncFocusGained) + FuncFocusGained(); +} + +void LineEdit::OnLostFocus() +{ + timer->Stop(); + SetTextSelection(0, 0); + + Update(); + + if (FuncFocusLost) + FuncFocusLost(); +} + +void LineEdit::Move(int steps, bool ctrl, bool shift) +{ + if (shift && selection_length == 0) + SetSelectionStart(cursor_pos); + + // Jump over words if control is pressed. + if (ctrl) + { + if (steps < 0) + steps = FindPreviousBreakCharacter(cursor_pos - 1) - cursor_pos; + else + steps = FindNextBreakCharacter(cursor_pos + 1) - cursor_pos; + + cursor_pos += steps; + if (cursor_pos < 0) + cursor_pos = 0; + if (cursor_pos > (int)text.size()) + cursor_pos = (int)text.size(); + } + else + { + UTF8Reader utf8_reader(text.data(), text.length()); + utf8_reader.set_position(cursor_pos); + if (steps > 0) + { + for (int i = 0; i < steps; i++) + utf8_reader.next(); + } + else if (steps < 0) + { + for (int i = 0; i < -steps; i++) + utf8_reader.prev(); + } + + cursor_pos = (int)utf8_reader.position(); + } + + + // Clear the selection if a cursor key is pressed but shift isn't down. + if (shift) + SetSelectionLength(cursor_pos - selection_start); + else + SetTextSelection(-1, 0); + + UpdateTextClipping(); + + Update(); + + undo_info.first_text_insert = true; +} + +bool LineEdit::InsertText(int pos, const std::string& str) +{ + undo_info.first_erase = false; + if (undo_info.first_text_insert) + { + undo_info.undo_text = GetText(); + undo_info.first_text_insert = false; + } + + // checking if insert exceeds max length + if (UTF8Reader::utf8_length(text) + UTF8Reader::utf8_length(str) > max_length) + { + return false; + } + + if (lowercase) + text.insert(pos, ToLower(str)); + else if (uppercase) + text.insert(pos, ToUpper(str)); + else + text.insert(pos, str); + + UpdateTextClipping(); + Update(); + return true; +} + +void LineEdit::Backspace() +{ + if (undo_info.first_erase) + { + undo_info.first_erase = false; + undo_info.undo_text = GetText(); + } + + if (GetSelectionLength() != 0) + { + DeleteSelectedText(); + Update(); + } + else + { + if (cursor_pos > 0) + { + UTF8Reader utf8_reader(text.data(), text.length()); + utf8_reader.set_position(cursor_pos); + utf8_reader.prev(); + size_t length = utf8_reader.char_length(); + text.erase(cursor_pos - length, length); + cursor_pos -= (int)length; + Update(); + } + } + + int old_pos = GetCursorPos(); + SetCursorPos(0); + SetCursorPos(old_pos); +} + +void LineEdit::Del() +{ + if (undo_info.first_erase) + { + undo_info.first_erase = false; + undo_info.undo_text = GetText(); + } + + if (GetSelectionLength() != 0) + { + DeleteSelectedText(); + Update(); + } + else + { + if (cursor_pos < (int)text.size()) + { + UTF8Reader utf8_reader(text.data(), text.length()); + utf8_reader.set_position(cursor_pos); + size_t length = utf8_reader.char_length(); + text.erase(cursor_pos, length); + Update(); + } + } +} + +int LineEdit::GetCharacterIndex(double mouse_x) +{ + if (text.size() <= 1) + { + return (int)text.size(); + } + + Canvas* canvas = GetCanvas(); + UTF8Reader utf8_reader(text.data(), text.length()); + + int seek_start = clip_start_offset; + int seek_end = (int)text.size(); + int seek_center = (seek_start + seek_end) / 2; + + //fast search + while (true) + { + utf8_reader.set_position(seek_center); + utf8_reader.move_to_leadbyte(); + + seek_center = (int)utf8_reader.position(); + + Size text_size = GetVisualTextSize(canvas, clip_start_offset, seek_center - clip_start_offset); + + if (text_size.width > mouse_x) + seek_end = seek_center; + else + seek_start = seek_center; + + if (seek_end - seek_start < 7) + break; //go to accurate search + + seek_center = (seek_start + seek_end) / 2; + } + + utf8_reader.set_position(seek_start); + utf8_reader.move_to_leadbyte(); + + //accurate search + while (true) + { + seek_center = (int)utf8_reader.position(); + + Size text_size = GetVisualTextSize(canvas, clip_start_offset, seek_center - clip_start_offset); + if (text_size.width > mouse_x || utf8_reader.is_end()) + break; + + utf8_reader.next(); + } + + return seek_center; +} + +void LineEdit::UpdateTextClipping() +{ + Canvas* canvas = GetCanvas(); + + Size text_size = GetVisualTextSize(canvas, clip_start_offset, (int)text.size() - clip_start_offset); + + if (cursor_pos < clip_start_offset) + clip_start_offset = cursor_pos; + + Rect cursor_rect = GetCursorRect(); + + UTF8Reader utf8_reader(text.data(), text.length()); + double width = GetWidth(); + while (cursor_rect.x + cursor_rect.width > width) + { + utf8_reader.set_position(clip_start_offset); + utf8_reader.next(); + clip_start_offset = (int)utf8_reader.position(); + if (clip_start_offset == text.size()) + break; + cursor_rect = GetCursorRect(); + } + + // Get number of chars of current text fitting in the lineedit. + int search_upper = (int)text.size(); + int search_lower = clip_start_offset; + + while (true) + { + int midpoint = (search_lower + search_upper) / 2; + + utf8_reader.set_position(midpoint); + utf8_reader.move_to_leadbyte(); + if (midpoint != utf8_reader.position()) + utf8_reader.next(); + midpoint = (int)utf8_reader.position(); + + if (midpoint == search_lower || midpoint == search_upper) + break; + + Size midpoint_size = GetVisualTextSize(canvas, clip_start_offset, midpoint - clip_start_offset); + + if (width < midpoint_size.width) + search_upper = midpoint; + else + search_lower = midpoint; + } + clip_end_offset = search_upper; + + if (cursor_rect.x < 0.0) + { + clip_start_offset = cursor_pos; + } +} + +Rect LineEdit::GetCursorRect() +{ + Canvas* canvas = GetCanvas(); + + int substr_end = cursor_pos - clip_start_offset; + if (substr_end < 0) + substr_end = 0; + + std::string clipped_text = text.substr(clip_start_offset, substr_end); + + if (password_mode) + { + // If we are in password mode, we gonna return the right characters + clipped_text = CreatePassword(UTF8Reader::utf8_length(clipped_text)); + } + + Size text_size_before_cursor = canvas->measureText(clipped_text).size(); + + Rect cursor_rect; + cursor_rect.x = text_size_before_cursor.width; + cursor_rect.width = 1.0f; + + cursor_rect.y = vertical_text_align.top; + cursor_rect.height = vertical_text_align.bottom - vertical_text_align.top; + + return cursor_rect; +} + +Rect LineEdit::GetSelectionRect() +{ + Canvas* canvas = GetCanvas(); + + // text before selection: + + std::string txt_before = GetVisibleTextBeforeSelection(); + Size text_size_before_selection = canvas->measureText(txt_before).size(); + + // selection text: + std::string txt_selected = GetVisibleSelectedText(); + Size text_size_selection = canvas->measureText(txt_selected).size(); + + Rect selection_rect; + selection_rect.x = text_size_before_selection.width; + selection_rect.width = text_size_selection.width; + selection_rect.y = vertical_text_align.top; + selection_rect.height = vertical_text_align.bottom - vertical_text_align.top; + return selection_rect; +} + +int LineEdit::FindNextBreakCharacter(int search_start) +{ + if (search_start >= int(text.size()) - 1) + return (int)text.size(); + + size_t pos = text.find_first_of(break_characters, search_start); + if (pos == std::string::npos) + return (int)text.size(); + return (int)pos; +} + +int LineEdit::FindPreviousBreakCharacter(int search_start) +{ + if (search_start <= 0) + return 0; + size_t pos = text.find_last_of(break_characters, search_start); + if (pos == std::string::npos) + return 0; + return (int)pos; +} + +void LineEdit::OnTimerExpired() +{ + if (!IsVisible()) + { + timer->Stop(); + return; + } + + if (cursor_blink_visible) + timer->Start(500); + else + timer->Start(500); + + cursor_blink_visible = !cursor_blink_visible; + Update(); +} + +void LineEdit::OnGeometryChanged() +{ + Canvas* canvas = GetCanvas(); + + vertical_text_align = canvas->verticalTextAlign(); + + clip_start_offset = 0; + cursor_pos = 0; + UpdateTextClipping(); +} + +std::string LineEdit::GetVisibleTextBeforeSelection() +{ + std::string ret; + int sel_start = std::min(selection_start, selection_start + selection_length); + int start = std::min(sel_start, clip_start_offset); + + if (start < clip_start_offset) + return ret; + + int end = std::min(sel_start, clip_end_offset); + + ret = text.substr(start, end - start); + + // If we are in password mode, we gonna return the right characters + if (password_mode) + ret = CreatePassword(UTF8Reader::utf8_length(ret)); + + return ret; +} + +std::string LineEdit::GetVisibleSelectedText() +{ + std::string ret; + + if (selection_length == 0) + return ret; + + int sel_start = std::min(selection_start, selection_start + selection_length); + int sel_end = std::max(selection_start, selection_start + selection_length); + int end = std::min(clip_end_offset, sel_end); + int start = std::max(clip_start_offset, sel_start); + + if (start > end) + return ret; + + if (start == end) + return ret; + + ret = text.substr(start, end - start); + + // If we are in password mode, we gonna return the right characters + if (password_mode) + ret = CreatePassword(UTF8Reader::utf8_length(ret)); + + return ret; +} + +void LineEdit::SetSelectionStart(int start) +{ + if (FuncSelectionChanged && selection_length && selection_start != start) + FuncSelectionChanged(); + + selection_start = start; +} + +void LineEdit::SetSelectionLength(int length) +{ + if (FuncSelectionChanged && selection_length != length) + FuncSelectionChanged(); + + selection_length = length; +} + +void LineEdit::SetTextSelection(int start, int length) +{ + if (FuncSelectionChanged && (selection_length != length || (selection_length && selection_start != start))) + FuncSelectionChanged(); + + selection_start = start; + selection_length = length; +} + +std::string LineEdit::GetVisibleTextAfterSelection() +{ + // returns the whole visible string if there is no selection. + std::string ret; + + int sel_end = std::max(selection_start, selection_start + selection_length); + int start = std::max(clip_start_offset, sel_end); + + int end = clip_end_offset; + if (start > end) + return ret; + + if (clip_end_offset == sel_end) + return ret; + + if (sel_end <= 0) + return ret; + else + { + ret = text.substr(start, end - start); + // If we are in password mode, we gonna return the right characters + if (password_mode) + ret = CreatePassword(UTF8Reader::utf8_length(ret)); + + return ret; + } +} + +void LineEdit::OnPaintFrame(Canvas* canvas) +{ + double w = GetFrameGeometry().width; + double h = GetFrameGeometry().height; + Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); + canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); + canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); + canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); +} + +void LineEdit::OnPaint(Canvas* canvas) +{ + std::string txt_before = GetVisibleTextBeforeSelection(); + std::string txt_selected = GetVisibleSelectedText(); + std::string txt_after = GetVisibleTextAfterSelection(); + + if (txt_before.empty() && txt_selected.empty() && txt_after.empty()) + { + txt_after = text.substr(clip_start_offset, clip_end_offset - clip_start_offset); + + // If we are in password mode, we gonna return the right characters + if (password_mode) + txt_after = CreatePassword(UTF8Reader::utf8_length(txt_after)); + } + + Size size_before = canvas->measureText(txt_before).size(); + Size size_selected = canvas->measureText(txt_selected).size(); + + if (!txt_selected.empty()) + { + // Draw selection box. + Rect selection_rect = GetSelectionRect(); + canvas->fillRect(selection_rect, HasFocus() ? Colorf::fromRgba8(100, 100, 100) : Colorf::fromRgba8(68, 68, 68)); + } + + // Draw text before selection + if (!txt_before.empty()) + { + canvas->drawText(Point(0.0, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_before); + } + if (!txt_selected.empty()) + { + canvas->drawText(Point(size_before.width, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_selected); + } + if (!txt_after.empty()) + { + canvas->drawText(Point(size_before.width + size_selected.width, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_after); + } + + // draw cursor + if (HasFocus()) + { + if (cursor_blink_visible) + { + Rect cursor_rect = GetCursorRect(); + canvas->fillRect(cursor_rect, Colorf::fromRgba8(255, 255, 255)); + } + } +} + +void LineEdit::OnScrollTimerExpired() +{ + if (mouse_moves_left) + Move(-1, false, false); + else + Move(1, false, false); +} + +void LineEdit::OnEnableChanged() +{ + bool enabled = IsEnabled(); + + if (!enabled) + { + cursor_blink_visible = false; + timer->Stop(); + } + Update(); +} + +bool LineEdit::InputMaskAcceptsInput(int cursor_pos, const std::string& str) +{ + return str.find_first_not_of(input_mask) == std::string::npos; +} + +std::string LineEdit::CreatePassword(std::string::size_type num_letters) const +{ + return std::string(num_letters, '*'); +} + +Size LineEdit::GetVisualTextSize(Canvas* canvas, int pos, int npos) const +{ + return canvas->measureText(password_mode ? CreatePassword(UTF8Reader::utf8_length(text.substr(pos, npos))) : text.substr(pos, npos)).size(); +} + +Size LineEdit::GetVisualTextSize(Canvas* canvas) const +{ + return canvas->measureText(password_mode ? CreatePassword(UTF8Reader::utf8_length(text)) : text).size(); +} + +std::string LineEdit::ToFixed(float number, int num_decimal_places) +{ + for (int i = 0; i < num_decimal_places; i++) + number *= 10.0f; + std::string val = std::to_string((int)std::round(number)); + if ((int)val.size() < num_decimal_places) + val.resize(num_decimal_places + 1, 0); + return val.substr(0, val.size() - num_decimal_places) + "." + val.substr(val.size() - num_decimal_places); +} + +std::string LineEdit::ToLower(const std::string& text) +{ + return text; +} + +std::string LineEdit::ToUpper(const std::string& text) +{ + return text; +} + +const std::string LineEdit::break_characters = " ::;,.-"; +const std::string LineEdit::numeric_mode_characters = "0123456789"; diff --git a/libraries/ZWidget/src/widgets/listview/listview.cpp b/libraries/ZWidget/src/widgets/listview/listview.cpp new file mode 100644 index 0000000000..29346ad574 --- /dev/null +++ b/libraries/ZWidget/src/widgets/listview/listview.cpp @@ -0,0 +1,149 @@ + +#include "widgets/listview/listview.h" +#include "widgets/scrollbar/scrollbar.h" + +ListView::ListView(Widget* parent) : Widget(parent) +{ + SetNoncontentSizes(10.0, 10.0, 3.0, 10.0); + + scrollbar = new Scrollbar(this); + scrollbar->FuncScroll = [=]() { OnScrollbarScroll(); }; +} + +void ListView::AddItem(const std::string& text) +{ + items.push_back(text); + Update(); +} + +void ListView::Activate() +{ + if (OnActivated) + OnActivated(); +} + +void ListView::ScrollToItem(int index) +{ + double itemHeight = 20.0; + double y = itemHeight * index; + if (y < scrollbar->GetPosition()) + { + scrollbar->SetPosition(y); + } + else if (y + itemHeight > scrollbar->GetPosition() + GetHeight()) + { + scrollbar->SetPosition(std::max(y + itemHeight - GetHeight(), 0.0)); + } +} + +void ListView::OnScrollbarScroll() +{ + Update(); +} + +void ListView::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + double sw = scrollbar->GetPreferredWidth(); + scrollbar->SetFrameGeometry(Rect::xywh(w - sw, 0.0, sw, h)); + scrollbar->SetRanges(h, items.size() * 20.0); +} + +void ListView::OnPaint(Canvas* canvas) +{ + double y = -scrollbar->GetPosition(); + double x = 2.0; + double w = GetWidth() - scrollbar->GetPreferredWidth() - 2.0; + double h = 20.0; + + int index = 0; + for (const std::string& item : items) + { + double itemY = y; + if (itemY + h >= 0.0 && itemY < GetHeight()) + { + if (index == selectedItem) + { + canvas->fillRect(Rect::xywh(x - 2.0, itemY, w, h), Colorf::fromRgba8(100, 100, 100)); + } + canvas->drawText(Point(x, y + 15.0), Colorf::fromRgba8(255, 255, 255), item); + } + y += h; + index++; + } +} + +void ListView::OnPaintFrame(Canvas* canvas) +{ + double w = GetFrameGeometry().width; + double h = GetFrameGeometry().height; + Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); + canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); + canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); + canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); +} + +void ListView::OnMouseDown(const Point& pos, int key) +{ + SetFocus(); + + if (key == IK_LeftMouse) + { + int index = (int)((pos.y - 5.0 + scrollbar->GetPosition()) / 20.0); + if (index >= 0 && (size_t)index < items.size()) + { + selectedItem = index; + Update(); + ScrollToItem(selectedItem); + } + } +} + +void ListView::OnMouseDoubleclick(const Point& pos, int key) +{ + if (key == IK_LeftMouse) + { + Activate(); + } +} + +void ListView::OnMouseWheel(const Point& pos, EInputKey key) +{ + if (key == IK_MouseWheelUp) + { + scrollbar->SetPosition(std::max(scrollbar->GetPosition() - 20.0, 0.0)); + } + else if (key == IK_MouseWheelDown) + { + scrollbar->SetPosition(std::max(scrollbar->GetPosition() + 20.0, scrollbar->GetMax())); + } +} + +void ListView::OnKeyDown(EInputKey key) +{ + if (key == IK_Down) + { + if (selectedItem + 1 < (int)items.size()) + { + selectedItem++; + Update(); + } + ScrollToItem(selectedItem); + } + else if (key == IK_Up) + { + if (selectedItem > 0) + { + selectedItem--; + Update(); + } + ScrollToItem(selectedItem); + } + else if (key == IK_Enter) + { + Activate(); + } +} diff --git a/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp b/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp new file mode 100644 index 0000000000..7a98a2ce59 --- /dev/null +++ b/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp @@ -0,0 +1,40 @@ + +#include "widgets/mainwindow/mainwindow.h" +#include "widgets/menubar/menubar.h" +#include "widgets/toolbar/toolbar.h" +#include "widgets/statusbar/statusbar.h" + +MainWindow::MainWindow() : Widget(nullptr, WidgetType::Window) +{ + MenubarWidget = new Menubar(this); + // ToolbarWidget = new Toolbar(this); + StatusbarWidget = new Statusbar(this); +} + +MainWindow::~MainWindow() +{ +} + +void MainWindow::SetCentralWidget(Widget* widget) +{ + if (CentralWidget != widget) + { + delete CentralWidget; + CentralWidget = widget; + if (CentralWidget) + CentralWidget->SetParent(this); + OnGeometryChanged(); + } +} + +void MainWindow::OnGeometryChanged() +{ + Size s = GetSize(); + + MenubarWidget->SetFrameGeometry(0.0, 0.0, s.width, 32.0); + // ToolbarWidget->SetFrameGeometry(0.0, 32.0, s.width, 36.0); + StatusbarWidget->SetFrameGeometry(0.0, s.height - 32.0, s.width, 32.0); + + if (CentralWidget) + CentralWidget->SetFrameGeometry(0.0, 32.0, s.width, s.height - 32.0 - 32.0); +} diff --git a/libraries/ZWidget/src/widgets/menubar/menubar.cpp b/libraries/ZWidget/src/widgets/menubar/menubar.cpp new file mode 100644 index 0000000000..8a66ce8f31 --- /dev/null +++ b/libraries/ZWidget/src/widgets/menubar/menubar.cpp @@ -0,0 +1,16 @@ + +#include "widgets/menubar/menubar.h" +#include "core/colorf.h" + +Menubar::Menubar(Widget* parent) : Widget(parent) +{ +} + +Menubar::~Menubar() +{ +} + +void Menubar::OnPaint(Canvas* canvas) +{ + canvas->drawText(Point(16.0, 21.0), Colorf::fromRgba8(0, 0, 0), "File Edit View Tools Window Help"); +} diff --git a/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp b/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp new file mode 100644 index 0000000000..2e2e348cb5 --- /dev/null +++ b/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp @@ -0,0 +1,109 @@ + +#include "widgets/pushbutton/pushbutton.h" + +PushButton::PushButton(Widget* parent) : Widget(parent) +{ + SetNoncontentSizes(10.0, 5.0, 10.0, 5.0); +} + +void PushButton::SetText(const std::string& value) +{ + if (text != value) + { + text = value; + Update(); + } +} + +const std::string& PushButton::GetText() const +{ + return text; +} + +double PushButton::GetPreferredHeight() const +{ + return 30.0; +} + +void PushButton::OnPaintFrame(Canvas* canvas) +{ + double w = GetFrameGeometry().width; + double h = GetFrameGeometry().height; + Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); + Colorf buttoncolor = Colorf::fromRgba8(68, 68, 68); + if (buttonDown) + buttoncolor = Colorf::fromRgba8(88, 88, 88); + else if (hot) + buttoncolor = Colorf::fromRgba8(78, 78, 78); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), buttoncolor); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); + canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); + canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); + canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); +} + +void PushButton::OnPaint(Canvas* canvas) +{ + Rect box = canvas->measureText(text); + canvas->drawText(Point((GetWidth() - box.width) * 0.5, GetHeight() - 5.0), Colorf::fromRgba8(255, 255, 255), text); +} + +void PushButton::OnMouseMove(const Point& pos) +{ + if (!hot) + { + hot = true; + Update(); + } +} + +void PushButton::OnMouseDown(const Point& pos, int key) +{ + SetFocus(); + buttonDown = true; + Update(); +} + +void PushButton::OnMouseUp(const Point& pos, int key) +{ + if (buttonDown) + { + buttonDown = false; + hot = false; + Repaint(); + Click(); + } +} + +void PushButton::OnMouseLeave() +{ + hot = false; + buttonDown = false; + Update(); +} + +void PushButton::OnKeyDown(EInputKey key) +{ + if (key == IK_Space || key == IK_Enter) + { + buttonDown = true; + Update(); + } +} + +void PushButton::OnKeyUp(EInputKey key) +{ + if (key == IK_Space || key == IK_Enter) + { + buttonDown = false; + hot = false; + Repaint(); + Click(); + } +} + +void PushButton::Click() +{ + if (OnClick) + OnClick(); +} diff --git a/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp b/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp new file mode 100644 index 0000000000..1462981630 --- /dev/null +++ b/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp @@ -0,0 +1,406 @@ + +#include "widgets/scrollbar/scrollbar.h" +#include "core/colorf.h" +#include + +Scrollbar::Scrollbar(Widget* parent) : Widget(parent) +{ + UpdatePartPositions(); + + mouse_down_timer = new Timer(this); + mouse_down_timer->FuncExpired = [=]() { OnTimerExpired(); }; +} + +Scrollbar::~Scrollbar() +{ +} + +bool Scrollbar::IsVertical() const +{ + return vertical; +} + +bool Scrollbar::IsHorizontal() const +{ + return !vertical; +} + +double Scrollbar::GetMin() const +{ + return scroll_min; +} + +double Scrollbar::GetMax() const +{ + return scroll_max; +} + +double Scrollbar::GetLineStep() const +{ + return line_step; +} + +double Scrollbar::GetPageStep() const +{ + return page_step; +} + +double Scrollbar::GetPosition() const +{ + return position; +} + +void Scrollbar::SetVertical() +{ + vertical = true; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetHorizontal() +{ + vertical = false; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetMin(double new_scroll_min) +{ + SetRanges(new_scroll_min, scroll_max, line_step, page_step); +} + +void Scrollbar::SetMax(double new_scroll_max) +{ + SetRanges(scroll_min, new_scroll_max, line_step, page_step); +} + +void Scrollbar::SetLineStep(double step) +{ + SetRanges(scroll_min, scroll_max, step, page_step); +} + +void Scrollbar::SetPageStep(double step) +{ + SetRanges(scroll_min, scroll_max, line_step, step); +} + +void Scrollbar::SetRanges(double new_scroll_min, double new_scroll_max, double new_line_step, double new_page_step) +{ + if (new_scroll_min >= new_scroll_max || new_line_step <= 0.0 || new_page_step <= 0.0) + throw std::runtime_error("Scrollbar ranges out of bounds!"); + scroll_min = new_scroll_min; + scroll_max = new_scroll_max; + line_step = new_line_step; + page_step = new_page_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetRanges(double view_size, double total_size) +{ + if (view_size <= 0.0 || total_size <= 0.0) + { + SetRanges(0.0, 1.0, 1.0, 1.0); + } + else + { + double scroll_max = std::max(1.0, total_size - view_size + 1.0); + double page_step = std::max(1.0, view_size); + SetRanges(0.0, scroll_max, 10, page_step); + } +} + +void Scrollbar::SetPosition(double pos) +{ + position = pos; + if (pos >= scroll_max) + position = scroll_max - 1.0; + if (pos < scroll_min) + position = scroll_min; + + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::OnMouseMove(const Point& pos) +{ + if (mouse_down_mode == mouse_down_thumb_drag) + { + double last_position = position; + + if (pos.x < -100.0 || pos.x > GetWidth() + 100.0 || pos.y < -100.0 || pos.y > GetHeight() + 100.0) + { + position = thumb_start_position; + } + else + { + double delta = vertical ? (pos.y - mouse_drag_start_pos.y) : (pos.x - mouse_drag_start_pos.x); + double position_pixels = thumb_start_pixel_position + delta; + + double track_height = 0; + if (vertical) + track_height = rect_track_decrement.height + rect_track_increment.height; + else + track_height = rect_track_decrement.width + rect_track_increment.width; + + if (track_height != 0.0) + position = scroll_min + position_pixels * (scroll_max - scroll_min) / track_height; + else + position = 0; + + if (position >= scroll_max) + position = scroll_max - 1; + if (position < scroll_min) + position = scroll_min; + + } + + if (position != last_position) + { + InvokeScrollEvent(&FuncScrollThumbTrack); + UpdatePartPositions(); + } + } + + Update(); +} + +void Scrollbar::OnMouseDown(const Point& pos, int key) +{ + mouse_drag_start_pos = pos; + + if (rect_button_decrement.contains(pos)) + { + mouse_down_mode = mouse_down_button_decr; + FuncScrollOnMouseDown = &FuncScrollLineDecrement; + + double last_position = position; + + position -= line_step; + last_step_size = -line_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + + if (last_position != position) + InvokeScrollEvent(&FuncScrollLineDecrement); + } + else if (rect_button_increment.contains(pos)) + { + mouse_down_mode = mouse_down_button_incr; + FuncScrollOnMouseDown = &FuncScrollLineIncrement; + + double last_position = position; + + position += line_step; + last_step_size = line_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + + if (last_position != position) + InvokeScrollEvent(&FuncScrollLineIncrement); + } + else if (rect_thumb.contains(pos)) + { + mouse_down_mode = mouse_down_thumb_drag; + thumb_start_position = position; + thumb_start_pixel_position = vertical ? (rect_thumb.y - rect_track_decrement.y) : (rect_thumb.x - rect_track_decrement.x); + } + else if (rect_track_decrement.contains(pos)) + { + mouse_down_mode = mouse_down_track_decr; + FuncScrollOnMouseDown = &FuncScrollPageDecrement; + + double last_position = position; + + position -= page_step; + last_step_size = -page_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + + if (last_position != position) + InvokeScrollEvent(&FuncScrollPageDecrement); + } + else if (rect_track_increment.contains(pos)) + { + mouse_down_mode = mouse_down_track_incr; + FuncScrollOnMouseDown = &FuncScrollPageIncrement; + + double last_position = position; + + position += page_step; + last_step_size = page_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + + if (last_position != position) + InvokeScrollEvent(&FuncScrollPageIncrement); + } + + mouse_down_timer->Start(100, false); + + UpdatePartPositions(); + + Update(); + CaptureMouse(); +} + +void Scrollbar::OnMouseUp(const Point& pos, int key) +{ + if (mouse_down_mode == mouse_down_thumb_drag) + { + if (FuncScrollThumbRelease) + FuncScrollThumbRelease(); + } + + mouse_down_mode = mouse_down_none; + mouse_down_timer->Stop(); + + Update(); + ReleaseMouseCapture(); +} + +void Scrollbar::OnMouseLeave() +{ + Update(); +} + +void Scrollbar::OnGeometryChanged() +{ + UpdatePartPositions(); +} + +void Scrollbar::OnPaint(Canvas* canvas) +{ + /* + part_button_decrement.render_box(canvas, rect_button_decrement); + part_track_decrement.render_box(canvas, rect_track_decrement); + part_thumb.render_box(canvas, rect_thumb); + part_thumb_gripper.render_box(canvas, rect_thumb); + part_track_increment.render_box(canvas, rect_track_increment); + part_button_increment.render_box(canvas, rect_button_increment); + */ + + canvas->fillRect(Rect::shrink(Rect::xywh(0.0, 0.0, GetWidth(), GetHeight()), 4.0, 0.0, 4.0, 0.0), Colorf::fromRgba8(33, 33, 33)); + canvas->fillRect(Rect::shrink(rect_thumb, 4.0, 0.0, 4.0, 0.0), Colorf::fromRgba8(58, 58, 58)); +} + +// Calculates positions of all parts. Returns true if thumb position was changed compared to previously, false otherwise. +bool Scrollbar::UpdatePartPositions() +{ + double decr_height = showbuttons ? 16.0 : 0.0; + double incr_height = showbuttons ? 16.0 : 0.0; + + double total_height = vertical ? GetHeight() : GetWidth(); + double track_height = std::max(0.0, total_height - decr_height - incr_height); + double thumb_height = CalculateThumbSize(track_height); + + double thumb_offset = decr_height + CalculateThumbPosition(thumb_height, track_height); + + Rect previous_rect_thumb = rect_thumb; + + rect_button_decrement = CreateRect(0.0, decr_height); + rect_track_decrement = CreateRect(decr_height, thumb_offset); + rect_thumb = CreateRect(thumb_offset, thumb_offset + thumb_height); + rect_track_increment = CreateRect(thumb_offset + thumb_height, decr_height + track_height); + rect_button_increment = CreateRect(decr_height + track_height, decr_height + track_height + incr_height); + + return (previous_rect_thumb != rect_thumb); +} + +double Scrollbar::CalculateThumbSize(double track_size) +{ + double minimum_thumb_size = 20.0; + double range = scroll_max - scroll_min; + double length = range + page_step - 1; + double thumb_size = page_step * track_size / length; + if (thumb_size < minimum_thumb_size) + thumb_size = minimum_thumb_size; + if (thumb_size > track_size) + thumb_size = track_size; + return thumb_size; +} + +double Scrollbar::CalculateThumbPosition(double thumb_size, double track_size) +{ + double relative_pos = position - scroll_min; + double range = scroll_max - scroll_min - 1; + if (range != 0) + { + double available_area = std::max(0.0, track_size - thumb_size); + return relative_pos * available_area / range; + } + else + { + return 0; + } +} + +Rect Scrollbar::CreateRect(double start, double end) +{ + if (vertical) + return Rect(0.0, start, GetWidth(), end - start); + else + return Rect(start, 0.0, end - start, GetHeight()); +} + +void Scrollbar::OnTimerExpired() +{ + if (mouse_down_mode == mouse_down_thumb_drag) + return; + + mouse_down_timer->Start(100, false); + + double last_position = position; + position += last_step_size; + if (position >= scroll_max) + position = scroll_max - 1; + + if (position < scroll_min) + position = scroll_min; + + if (position != last_position) + { + InvokeScrollEvent(FuncScrollOnMouseDown); + + if (UpdatePartPositions()) + Update(); + } +} + +void Scrollbar::OnEnableChanged() +{ + Update(); +} + +void Scrollbar::InvokeScrollEvent(std::function* event_ptr) +{ + if (position == scroll_max - 1) + { + if (FuncScrollMax) + FuncScrollMax(); + } + + if (position == scroll_min) + { + if (FuncScrollMin) + FuncScrollMin(); + } + + if (FuncScroll) + FuncScroll(); + + if (event_ptr && *event_ptr) + (*event_ptr)(); +} diff --git a/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp b/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp new file mode 100644 index 0000000000..fd914298c7 --- /dev/null +++ b/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp @@ -0,0 +1,19 @@ + +#include "widgets/statusbar/statusbar.h" +#include "widgets/lineedit/lineedit.h" +#include "core/colorf.h" + +Statusbar::Statusbar(Widget* parent) : Widget(parent) +{ + CommandEdit = new LineEdit(this); + CommandEdit->SetFrameGeometry(Rect::xywh(90.0, 4.0, 400.0, 23.0)); +} + +Statusbar::~Statusbar() +{ +} + +void Statusbar::OnPaint(Canvas* canvas) +{ + canvas->drawText(Point(16.0, 21.0), Colorf::fromRgba8(0, 0, 0), "Command:"); +} diff --git a/libraries/ZWidget/src/widgets/textedit/textedit.cpp b/libraries/ZWidget/src/widgets/textedit/textedit.cpp new file mode 100644 index 0000000000..9ddb5fba49 --- /dev/null +++ b/libraries/ZWidget/src/widgets/textedit/textedit.cpp @@ -0,0 +1,1047 @@ + +#include "widgets/textedit/textedit.h" +#include "widgets/scrollbar/scrollbar.h" +#include "core/utf8reader.h" +#include "core/colorf.h" + +#ifdef _MSC_VER +#pragma warning(disable: 4267) // warning C4267: 'initializing': conversion from 'size_t' to 'int', possible loss of data +#endif + +TextEdit::TextEdit(Widget* parent) : Widget(parent) +{ + SetNoncontentSizes(8.0, 8.0, 8.0, 8.0); + + timer = new Timer(this); + timer->FuncExpired = [=]() { OnTimerExpired(); }; + + scroll_timer = new Timer(this); + scroll_timer->FuncExpired = [=]() { OnScrollTimerExpired(); }; + + SetCursor(StandardCursor::ibeam); + + CreateComponents(); +} + +TextEdit::~TextEdit() +{ +} + +bool TextEdit::IsReadOnly() const +{ + return readonly; +} + +bool TextEdit::IsLowercase() const +{ + return lowercase; +} + +bool TextEdit::IsUppercase() const +{ + return uppercase; +} + +int TextEdit::GetMaxLength() const +{ + return max_length; +} + +std::string TextEdit::GetLineText(int line) const +{ + if (line >= 0 && line < (int)lines.size()) + return lines[line].text; + else + return std::string(); +} + +std::string TextEdit::GetText() const +{ + std::string::size_type size = 0; + for (size_t i = 0; i < lines.size(); i++) + size += lines[i].text.size(); + size += lines.size() - 1; + + std::string text; + text.reserve(size); + + for (size_t i = 0; i < lines.size(); i++) + { + if (i > 0) + text.push_back('\n'); + text += lines[i].text; + } + + return text; +} + +int TextEdit::GetLineCount() const +{ + return (int)lines.size(); +} + +std::string TextEdit::GetSelection() const +{ + std::string::size_type offset = ToOffset(selection_start); + int start = (int)std::min(offset, offset + selection_length); + return GetText().substr(start, abs(selection_length)); +} + +int TextEdit::GetSelectionStart() const +{ + return (int)ToOffset(selection_start); +} + +int TextEdit::GetSelectionLength() const +{ + return selection_length; +} + +int TextEdit::GetCursorPos() const +{ + return (int)ToOffset(cursor_pos); +} + +int TextEdit::GetCursorLineNumber() const +{ + return cursor_pos.y; +} + +void TextEdit::SelectAll() +{ + std::string::size_type size = 0; + for (size_t i = 0; i < lines.size(); i++) + size += lines[i].text.size(); + size += lines.size() - 1; + SetSelection(0, (int)size); +} + +void TextEdit::SetReadOnly(bool enable) +{ + if (readonly != enable) + { + readonly = enable; + Update(); + } +} + +void TextEdit::SetLowercase(bool enable) +{ + if (lowercase != enable) + { + lowercase = enable; + Update(); + } +} + +void TextEdit::SetUppercase(bool enable) +{ + if (uppercase != enable) + { + uppercase = enable; + Update(); + } +} + +void TextEdit::SetMaxLength(int length) +{ + if (max_length != length) + { + max_length = length; + + std::string::size_type size = 0; + for (size_t i = 0; i < lines.size(); i++) + size += lines[i].text.size(); + size += lines.size() - 1; + + if ((int)size > length) + { + if (FuncBeforeEditChanged) + FuncBeforeEditChanged(); + SetSelection(length, (int)size - length); + DeleteSelectedText(); + if (FuncAfterEditChanged) + FuncAfterEditChanged(); + } + Update(); + } +} + +void TextEdit::SetText(const std::string& text) +{ + lines.clear(); + std::string::size_type start = 0; + std::string::size_type end = text.find('\n'); + while (end != std::string::npos) + { + TextEdit::Line line; + line.text = text.substr(start, end - start); + lines.push_back(line); + start = end + 1; + end = text.find('\n', start); + } + TextEdit::Line line; + line.text = text.substr(start); + lines.push_back(line); + + clip_start_offset = 0; + SetCursorPos(0); + ClearSelection(); + Update(); +} + +void TextEdit::AddText(const std::string& text) +{ + std::string::size_type start = 0; + std::string::size_type end = text.find('\n'); + while (end != std::string::npos) + { + TextEdit::Line line; + line.text = text.substr(start, end - start); + lines.push_back(line); + start = end + 1; + end = text.find('\n', start); + } + TextEdit::Line line; + line.text = text.substr(start); + lines.push_back(line); + + // clip_start_offset = 0; + // SetCursorPos(0); + ClearSelection(); + Update(); +} + +void TextEdit::SetSelection(int pos, int length) +{ + selection_start = ivec2(pos, 0); + selection_length = length; + Update(); +} + +void TextEdit::ClearSelection() +{ + SetSelection(0, 0); + Update(); +} + +void TextEdit::DeleteSelectedText() +{ + if (GetSelectionLength() == 0) + return; + + std::string::size_type offset = ToOffset(selection_start); + int start = (int)std::min(offset, offset + selection_length); + int length = std::abs(selection_length); + + ClearSelection(); + std::string text = GetText(); + SetText(text.substr(0, start) + text.substr(start + length)); + SetCursorPos(start); +} + +void TextEdit::SetCursorPos(int pos) +{ + cursor_pos = FromOffset(pos); + Update(); +} + +void TextEdit::SetInputMask(const std::string& mask) +{ + input_mask = mask; +} + +void TextEdit::SetCursorDrawingEnabled(bool enable) +{ + if (!readonly) + timer->Start(500); +} + +void TextEdit::SetSelectAllOnFocusGain(bool enable) +{ + select_all_on_focus_gain = enable; +} + +void TextEdit::OnMouseMove(const Point& pos) +{ + if (mouse_selecting && !ignore_mouse_events) + { + if (pos.x < 0.0 || pos.x > GetWidth()) + { + if (pos.x < 0.0) + mouse_moves_left = true; + else + mouse_moves_left = false; + + if (!readonly) + scroll_timer->Start(50, true); + } + else + { + scroll_timer->Stop(); + cursor_pos = GetCharacterIndex(pos); + selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); + Update(); + } + } +} + +void TextEdit::OnMouseDown(const Point& pos, int key) +{ + if (key == IK_LeftMouse) + { + CaptureMouse(); + mouse_selecting = true; + cursor_pos = GetCharacterIndex(pos); + selection_start = cursor_pos; + selection_length = 0; + + Update(); + } +} + +void TextEdit::OnMouseDoubleclick(const Point& pos, int key) +{ +} + +void TextEdit::OnMouseUp(const Point& pos, int key) +{ + if (mouse_selecting && key == IK_LeftMouse) + { + if (ignore_mouse_events) // This prevents text selection from changing from what was set when focus was gained. + { + ReleaseMouseCapture(); + ignore_mouse_events = false; + mouse_selecting = false; + } + else + { + scroll_timer->Stop(); + ReleaseMouseCapture(); + mouse_selecting = false; + ivec2 sel_end = GetCharacterIndex(pos); + selection_length = ToOffset(sel_end) - ToOffset(selection_start); + cursor_pos = sel_end; + SetFocus(); + Update(); + } + } +} + +void TextEdit::OnKeyChar(std::string chars) +{ + if (!chars.empty() && !(chars[0] >= 0 && chars[0] < 32)) + { + if (FuncBeforeEditChanged) + FuncBeforeEditChanged(); + + DeleteSelectedText(); + ClearSelection(); + if (input_mask.empty()) + { + // not in any special mode, just insert the string. + InsertText(cursor_pos, chars); + cursor_pos.x += chars.size(); + } + else + { + if (InputMaskAcceptsInput(cursor_pos, chars)) + { + InsertText(cursor_pos, chars); + cursor_pos.x += chars.size(); + } + } + + if (FuncAfterEditChanged) + FuncAfterEditChanged(); + } +} + +void TextEdit::OnKeyDown(EInputKey key) +{ + if (!readonly && key == IK_Enter) + { + if (FuncEnterPressed) + { + FuncEnterPressed(); + } + else + { + ClearSelection(); + InsertText(cursor_pos, "\n"); + SetCursorPos(GetCursorPos() + 1); + } + return; + } + + if (!readonly) // Do not flash cursor when readonly + { + cursor_blink_visible = true; + timer->Start(500); // don't blink cursor when moving or typing. + } + + if (key == IK_Enter || key == IK_Escape || key == IK_Tab) + { + // Do not consume these. + return; + } + else if (key == IK_A && GetKeyState(IK_Ctrl)) + { + // select all + SelectAll(); + } + else if (key == IK_C && GetKeyState(IK_Ctrl)) + { + std::string str = GetSelection(); + SetClipboardText(str); + } + else if (readonly) + { + // Do not consume messages on read only component (only allow CTRL-A and CTRL-C) + return; + } + else if (key == IK_Up) + { + if (GetKeyState(IK_Shift) && selection_length == 0) + selection_start = cursor_pos; + + if (cursor_pos.y > 0) + { + cursor_pos.y--; + cursor_pos.x = std::min(lines[cursor_pos.y].text.size(), (size_t)cursor_pos.x); + } + + if (GetKeyState(IK_Shift)) + { + selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); + } + else + { + // Clear the selection if a cursor key is pressed but shift isn't down. + selection_start = ivec2(0, 0); + selection_length = 0; + } + MoveVerticalScroll(); + Update(); + undo_info.first_text_insert = true; + } + else if (key == IK_Down) + { + if (GetKeyState(IK_Shift) && selection_length == 0) + selection_start = cursor_pos; + + if (cursor_pos.y < lines.size() - 1) + { + cursor_pos.y++; + cursor_pos.x = std::min(lines[cursor_pos.y].text.size(), (size_t)cursor_pos.x); + } + + if (GetKeyState(IK_Shift)) + { + selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); + } + else + { + // Clear the selection if a cursor key is pressed but shift isn't down. + selection_start = ivec2(0, 0); + selection_length = 0; + } + MoveVerticalScroll(); + + Update(); + undo_info.first_text_insert = true; + } + else if (key == IK_Left) + { + Move(-1, GetKeyState(IK_Shift), GetKeyState(IK_Ctrl)); + } + else if (key == IK_Right) + { + Move(1, GetKeyState(IK_Shift), GetKeyState(IK_Ctrl)); + } + else if (key == IK_Backspace) + { + Backspace(); + } + else if (key == IK_Delete) + { + Del(); + } + else if (key == IK_Home) + { + if (GetKeyState(IK_Ctrl)) + cursor_pos = ivec2(0, 0); + else + cursor_pos.x = 0; + if (GetKeyState(IK_Shift)) + selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); + else + ClearSelection(); + Update(); + MoveVerticalScroll(); + } + else if (key == IK_End) + { + if (GetKeyState(IK_Ctrl)) + cursor_pos = ivec2(lines.back().text.length(), lines.size() - 1); + else + cursor_pos.x = lines[cursor_pos.y].text.size(); + + if (GetKeyState(IK_Shift)) + selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); + else + ClearSelection(); + Update(); + } + else if (key == IK_X && GetKeyState(IK_Ctrl)) + { + std::string str = GetSelection(); + DeleteSelectedText(); + SetClipboardText(str); + } + else if (key == IK_V && GetKeyState(IK_Ctrl)) + { + std::string str = GetClipboardText(); + std::string::const_iterator end_str = std::remove(str.begin(), str.end(), '\r'); + str.resize(end_str - str.begin()); + DeleteSelectedText(); + + if (input_mask.empty()) + { + InsertText(cursor_pos, str); + SetCursorPos(GetCursorPos() + str.length()); + } + else + { + if (InputMaskAcceptsInput(cursor_pos, str)) + { + InsertText(cursor_pos, str); + SetCursorPos(GetCursorPos() + str.length()); + } + } + MoveVerticalScroll(); + } + else if (GetKeyState(IK_Ctrl) && key == IK_Z) + { + if (!readonly) + { + std::string tmp = undo_info.undo_text; + undo_info.undo_text = GetText(); + SetText(tmp); + } + } + else if (key == IK_Shift) + { + if (selection_length == 0) + selection_start = cursor_pos; + } + + if (FuncAfterEditChanged) + FuncAfterEditChanged(); +} + +void TextEdit::OnKeyUp(EInputKey key) +{ +} + +void TextEdit::OnSetFocus() +{ + if (!readonly) + timer->Start(500); + if (select_all_on_focus_gain) + SelectAll(); + ignore_mouse_events = true; + cursor_pos.y = lines.size() - 1; + cursor_pos.x = lines[cursor_pos.y].text.length(); + + Update(); + + if (FuncFocusGained) + FuncFocusGained(); +} + +void TextEdit::OnLostFocus() +{ + timer->Stop(); + ClearSelection(); + + Update(); + + if (FuncFocusLost) + FuncFocusLost(); +} + +void TextEdit::CreateComponents() +{ + vert_scrollbar = new Scrollbar(this); + vert_scrollbar->FuncScroll = [=]() { OnVerticalScroll(); }; + vert_scrollbar->SetVisible(false); + vert_scrollbar->SetVertical(); +} + +void TextEdit::OnVerticalScroll() +{ +} + +void TextEdit::UpdateVerticalScroll() +{ + Rect rect( + GetWidth() - 16.0/*vert_scrollbar->GetWidth()*/, + 0.0, + 16.0/*vert_scrollbar->GetWidth()*/, + GetHeight()); + + vert_scrollbar->SetFrameGeometry(rect); + + double total_height = GetTotalLineHeight(); + double height_per_line = std::max(1.0, total_height / std::max(1.0, (double)lines.size())); + bool visible = total_height > GetHeight(); + vert_scrollbar->SetRanges((int)std::round(GetHeight() / height_per_line), (int)std::round(total_height / height_per_line)); + vert_scrollbar->SetLineStep(1); + vert_scrollbar->SetVisible(visible); + + if (visible == false) + vert_scrollbar->SetPosition(0); +} + +void TextEdit::MoveVerticalScroll() +{ + double total_height = GetTotalLineHeight(); + double height_per_line = std::max(1.0, total_height / std::max((size_t)1, lines.size())); + int lines_fit = (int)(GetHeight() / height_per_line); + if (cursor_pos.y >= vert_scrollbar->GetPosition() + lines_fit) + { + vert_scrollbar->SetPosition(cursor_pos.y - lines_fit + 1); + } + else if (cursor_pos.y < vert_scrollbar->GetPosition()) + { + vert_scrollbar->SetPosition(cursor_pos.y); + } +} + +double TextEdit::GetTotalLineHeight() +{ + double total = 0; + for (std::vector::const_iterator iter = lines.begin(); iter != lines.end(); iter++) + { + total += iter->layout.GetSize().height; + } + return total; +} + +void TextEdit::Move(int steps, bool shift, bool ctrl) +{ + if (shift && selection_length == 0) + selection_start = cursor_pos; + + // Jump over words if control is pressed. + if (ctrl) + { + if (steps < 0 && cursor_pos.x == 0 && cursor_pos.y > 0) + { + cursor_pos.x = (int)lines[cursor_pos.y - 1].text.size(); + cursor_pos.y--; + } + else if (steps > 0 && cursor_pos.x == (int)lines[cursor_pos.y].text.size() && cursor_pos.y + 1 < (int)lines.size()) + { + cursor_pos.x = 0; + cursor_pos.y++; + } + + ivec2 new_pos; + if (steps < 0) + new_pos = FindPreviousBreakCharacter(cursor_pos); + else + new_pos = FindNextBreakCharacter(cursor_pos); + + cursor_pos = new_pos; + } + else if (steps < 0 && cursor_pos.x == 0 && cursor_pos.y > 0) + { + cursor_pos.x = (int)lines[cursor_pos.y - 1].text.size(); + cursor_pos.y--; + } + else if (steps > 0 && cursor_pos.x == (int)lines[cursor_pos.y].text.size() && cursor_pos.y + 1 < (int)lines.size()) + { + cursor_pos.x = 0; + cursor_pos.y++; + } + else + { + UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length()); + utf8_reader.set_position(cursor_pos.x); + if (steps > 0) + { + for (int i = 0; i < steps; i++) + utf8_reader.next(); + } + else if (steps < 0) + { + for (int i = 0; i < -steps; i++) + utf8_reader.prev(); + } + + cursor_pos.x = (int)utf8_reader.position(); + } + + if (shift) + { + selection_length = (int)ToOffset(cursor_pos) - (int)ToOffset(selection_start); + } + else + { + // Clear the selection if a cursor key is pressed but shift isn't down. + selection_start = ivec2(0, 0); + selection_length = 0; + } + + + MoveVerticalScroll(); + Update(); + + undo_info.first_text_insert = true; +} + +std::string TextEdit::break_characters = " ::;,.-"; + +TextEdit::ivec2 TextEdit::FindNextBreakCharacter(ivec2 search_start) +{ + search_start.x++; + if (search_start.x >= int(lines[search_start.y].text.size()) - 1) + return ivec2(lines[search_start.y].text.size(), search_start.y); + + int pos = lines[search_start.y].text.find_first_of(break_characters, search_start.x); + if (pos == std::string::npos) + return ivec2(lines[search_start.y].text.size(), search_start.y); + return ivec2(pos, search_start.y); +} + +TextEdit::ivec2 TextEdit::FindPreviousBreakCharacter(ivec2 search_start) +{ + search_start.x--; + if (search_start.x <= 0) + return ivec2(0, search_start.y); + int pos = lines[search_start.y].text.find_last_of(break_characters, search_start.x); + if (pos == std::string::npos) + return ivec2(0, search_start.y); + return ivec2(pos, search_start.y); +} + +void TextEdit::InsertText(ivec2 pos, const std::string& str) +{ + undo_info.first_erase = false; + if (undo_info.first_text_insert) + { + undo_info.undo_text = GetText(); + undo_info.first_text_insert = false; + } + + // checking if insert exceeds max length + if (ToOffset(ivec2(lines[lines.size() - 1].text.size(), lines.size() - 1)) + str.length() > max_length) + { + return; + } + + std::string::size_type start = 0; + while (true) + { + std::string::size_type next_newline = str.find('\n', start); + + lines[pos.y].text.insert(pos.x, str.substr(start, next_newline - start)); + lines[pos.y].invalidated = true; + + if (next_newline == std::string::npos) + break; + + pos.x += next_newline - start; + + Line line; + line.text = lines[pos.y].text.substr(pos.x); + lines.insert(lines.begin() + pos.y + 1, line); + lines[pos.y].text = lines[pos.y].text.substr(0, pos.x); + lines[pos.y].invalidated = true; + pos = ivec2(0, pos.y + 1); + + start = next_newline + 1; + } + + MoveVerticalScroll(); + + Update(); +} + +void TextEdit::Backspace() +{ + if (undo_info.first_erase) + { + undo_info.first_erase = false; + undo_info.undo_text = GetText(); + } + + if (GetSelectionLength() != 0) + { + DeleteSelectedText(); + ClearSelection(); + Update(); + } + else + { + if (cursor_pos.x > 0) + { + UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length()); + utf8_reader.set_position(cursor_pos.x); + utf8_reader.prev(); + int length = utf8_reader.char_length(); + lines[cursor_pos.y].text.erase(cursor_pos.x - length, length); + lines[cursor_pos.y].invalidated = true; + cursor_pos.x -= length; + Update(); + } + else if (cursor_pos.y > 0) + { + selection_start = ivec2(lines[cursor_pos.y - 1].text.length(), cursor_pos.y - 1); + selection_length = 1; + DeleteSelectedText(); + } + } + MoveVerticalScroll(); +} + +void TextEdit::Del() +{ + if (undo_info.first_erase) + { + undo_info.first_erase = false; + undo_info.undo_text = GetText(); + } + + if (GetSelectionLength() != 0) + { + DeleteSelectedText(); + ClearSelection(); + Update(); + } + else + { + if (cursor_pos.x < (int)lines[cursor_pos.y].text.size()) + { + UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length()); + utf8_reader.set_position(cursor_pos.x); + int length = utf8_reader.char_length(); + lines[cursor_pos.y].text.erase(cursor_pos.x, length); + lines[cursor_pos.y].invalidated = true; + Update(); + } + else if (cursor_pos.y + 1 < lines.size()) + { + selection_start = ivec2(lines[cursor_pos.y].text.length(), cursor_pos.y); + selection_length = 1; + DeleteSelectedText(); + } + } + MoveVerticalScroll(); +} + +void TextEdit::OnTimerExpired() +{ + if (IsVisible() == false) + { + timer->Stop(); + return; + } + + if (cursor_blink_visible) + timer->Start(500); + else + timer->Start(500); + + cursor_blink_visible = !cursor_blink_visible; + Update(); +} + +void TextEdit::OnGeometryChanged() +{ + Canvas* canvas = GetCanvas(); + + vertical_text_align = canvas->verticalTextAlign(); + + clip_start_offset = 0; + UpdateVerticalScroll(); +} + +void TextEdit::OnScrollTimerExpired() +{ + if (mouse_moves_left) + Move(-1, false, false); + else + Move(1, false, false); +} + +void TextEdit::OnEnableChanged() +{ + bool enabled = IsEnabled(); + if (!enabled) + { + cursor_blink_visible = false; + timer->Stop(); + } + Update(); +} + +bool TextEdit::InputMaskAcceptsInput(ivec2 cursor_pos, const std::string& str) +{ + return str.find_first_not_of(input_mask) == std::string::npos; +} + +std::string::size_type TextEdit::ToOffset(ivec2 pos) const +{ + if (pos.y < lines.size()) + { + std::string::size_type offset = 0; + for (int line = 0; line < pos.y; line++) + { + offset += lines[line].text.size() + 1; + } + return offset + std::min((size_t)pos.x, lines[pos.y].text.size()); + } + else + { + std::string::size_type offset = 0; + for (size_t line = 0; line < lines.size(); line++) + { + offset += lines[line].text.size() + 1; + } + return offset - 1; + } +} + +TextEdit::ivec2 TextEdit::FromOffset(std::string::size_type offset) const +{ + int line_offset = 0; + for (int line = 0; line < lines.size(); line++) + { + if (offset <= line_offset + lines[line].text.size()) + { + return ivec2(offset - line_offset, line); + } + line_offset += lines[line].text.size() + 1; + } + return ivec2(lines.back().text.size(), lines.size() - 1); +} + +double TextEdit::GetTotalHeight() +{ + Canvas* canvas = GetCanvas(); + LayoutLines(canvas); + if (!lines.empty()) + { + return lines.back().box.bottom(); + } + else + { + return GetHeight(); + } +} + +void TextEdit::LayoutLines(Canvas* canvas) +{ + ivec2 sel_start; + ivec2 sel_end; + if (selection_length > 0) + { + sel_start = selection_start; + sel_end = FromOffset(ToOffset(selection_start) + selection_length); + } + else if (selection_length < 0) + { + sel_start = FromOffset(ToOffset(selection_start) + selection_length); + sel_end = selection_start; + } + + Point draw_pos; + for (size_t i = vert_scrollbar->GetPosition(); i < lines.size(); i++) + { + Line& line = lines[i]; + if (line.invalidated) + { + line.layout.Clear(); + if (!line.text.empty()) + line.layout.AddText(line.text, font, Colorf::fromRgba8(255, 255, 255)); + else + line.layout.AddText(" ", font, Colorf::fromRgba8(255, 255, 255)); // Draw one space character to get the correct height + line.layout.Layout(canvas, GetWidth()); + line.box = Rect(draw_pos, line.layout.GetSize()); + line.invalidated = false; + } + + if (sel_start != sel_end && sel_start.y <= i && sel_end.y >= i) + { + line.layout.SetSelectionRange(sel_start.y < i ? 0 : sel_start.x, sel_end.y > i ? line.text.size() : sel_end.x); + } + else + { + line.layout.SetSelectionRange(0, 0); + } + + line.layout.HideCursor(); + if (HasFocus()) + { + if (cursor_blink_visible && cursor_pos.y == i) + { + line.layout.SetCursorPos(cursor_pos.x); + line.layout.SetCursorColor(Colorf::fromRgba8(255, 255, 255)); + line.layout.ShowCursor(); + } + } + + line.box.x = draw_pos.x; + line.box.y = draw_pos.y; + line.layout.SetPosition(line.box.topLeft()); + + draw_pos = line.box.bottomLeft(); + } + UpdateVerticalScroll(); +} + +void TextEdit::OnPaintFrame(Canvas* canvas) +{ + double w = GetFrameGeometry().width; + double h = GetFrameGeometry().height; + Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); + canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); + canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); + canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); +} + +void TextEdit::OnPaint(Canvas* canvas) +{ + LayoutLines(canvas); + for (size_t i = vert_scrollbar->GetPosition(); i < lines.size(); i++) + lines[i].layout.DrawLayout(canvas); +} + +TextEdit::ivec2 TextEdit::GetCharacterIndex(Point mouse_wincoords) +{ + Canvas* canvas = GetCanvas(); + for (size_t i = 0; i < lines.size(); i++) + { + Line& line = lines[i]; + if (line.box.top() <= mouse_wincoords.y && line.box.bottom() > mouse_wincoords.y) + { + SpanLayout::HitTestResult result = line.layout.HitTest(canvas, mouse_wincoords); + switch (result.type) + { + case SpanLayout::HitTestResult::inside: + return ivec2(clamp(result.offset, (size_t)0, line.text.size()), i); + case SpanLayout::HitTestResult::outside_left: + return ivec2(0, i); + case SpanLayout::HitTestResult::outside_right: + return ivec2(line.text.size(), i); + } + } + } + + return ivec2(lines.back().text.size(), lines.size() - 1); +} diff --git a/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp b/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp new file mode 100644 index 0000000000..3c034ddb20 --- /dev/null +++ b/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp @@ -0,0 +1,54 @@ + +#include "widgets/textlabel/textlabel.h" + +TextLabel::TextLabel(Widget* parent) : Widget(parent) +{ +} + +void TextLabel::SetText(const std::string& value) +{ + if (text != value) + { + text = value; + Update(); + } +} + +const std::string& TextLabel::GetText() const +{ + return text; +} + +void TextLabel::SetTextAlignment(TextLabelAlignment alignment) +{ + if (textAlignment != alignment) + { + textAlignment = alignment; + Update(); + } +} + +TextLabelAlignment TextLabel::GetTextAlignment() const +{ + return textAlignment; +} + +double TextLabel::GetPreferredHeight() const +{ + return 20.0; +} + +void TextLabel::OnPaint(Canvas* canvas) +{ + double x = 0.0; + if (textAlignment == TextLabelAlignment::Center) + { + x = (GetWidth() - canvas->measureText(text).width) * 0.5; + } + else if (textAlignment == TextLabelAlignment::Right) + { + x = GetWidth() - canvas->measureText(text).width; + } + + canvas->drawText(Point(x, GetHeight() - 5.0), Colorf::fromRgba8(255, 255, 255), text); +} diff --git a/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp b/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp new file mode 100644 index 0000000000..d028dd2d21 --- /dev/null +++ b/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp @@ -0,0 +1,10 @@ + +#include "widgets/toolbar/toolbar.h" + +Toolbar::Toolbar(Widget* parent) : Widget(parent) +{ +} + +Toolbar::~Toolbar() +{ +} diff --git a/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp b/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp new file mode 100644 index 0000000000..adb6d08bf8 --- /dev/null +++ b/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp @@ -0,0 +1,14 @@ + +#include "widgets/toolbar/toolbarbutton.h" + +ToolbarButton::ToolbarButton(Widget* parent) : Widget(parent) +{ +} + +ToolbarButton::~ToolbarButton() +{ +} + +void ToolbarButton::OnPaint(Canvas* canvas) +{ +} diff --git a/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.cpp b/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.cpp new file mode 100644 index 0000000000..2cb52e2b5b --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.cpp @@ -0,0 +1,676 @@ + +#include "sdl2displaywindow.h" +#include + +Uint32 SDL2DisplayWindow::PaintEventNumber = 0xffffffff; +bool SDL2DisplayWindow::ExitRunLoop; +std::unordered_map SDL2DisplayWindow::WindowList; + +class InitSDL +{ +public: + InitSDL() + { + int result = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); + if (result != 0) + throw std::runtime_error(std::string("Unable to initialize SDL:") + SDL_GetError()); + + SDL2DisplayWindow::PaintEventNumber = SDL_RegisterEvents(1); + } +}; + +static void CheckInitSDL() +{ + static InitSDL initsdl; +} + +SDL2DisplayWindow::SDL2DisplayWindow(DisplayWindowHost* windowHost) : WindowHost(windowHost) +{ + CheckInitSDL(); + + int result = SDL_CreateWindowAndRenderer(320, 200, SDL_WINDOW_HIDDEN /*| SDL_WINDOW_ALLOW_HIGHDPI*/, &WindowHandle, &RendererHandle); + if (result != 0) + throw std::runtime_error(std::string("Unable to create SDL window:") + SDL_GetError()); + + WindowList[SDL_GetWindowID(WindowHandle)] = this; +} + +SDL2DisplayWindow::~SDL2DisplayWindow() +{ + WindowList.erase(WindowList.find(SDL_GetWindowID(WindowHandle))); + + if (BackBufferTexture) + { + SDL_DestroyTexture(BackBufferTexture); + BackBufferTexture = nullptr; + } + + SDL_DestroyRenderer(RendererHandle); + SDL_DestroyWindow(WindowHandle); + RendererHandle = nullptr; + WindowHandle = nullptr; +} + +void SDL2DisplayWindow::SetWindowTitle(const std::string& text) +{ + SDL_SetWindowTitle(WindowHandle, text.c_str()); +} + +void SDL2DisplayWindow::SetWindowFrame(const Rect& box) +{ + // SDL2 doesn't really seem to have an API for this. + // The docs aren't clear what you're setting when calling SDL_SetWindowSize. + SetClientFrame(box); +} + +void SDL2DisplayWindow::SetClientFrame(const Rect& box) +{ + // Is there a way to set both in one call? + + double uiscale = GetDpiScale(); + int x = (int)std::round(box.x * uiscale); + int y = (int)std::round(box.y * uiscale); + int w = (int)std::round(box.width * uiscale); + int h = (int)std::round(box.height * uiscale); + + SDL_SetWindowPosition(WindowHandle, x, y); + SDL_SetWindowSize(WindowHandle, w, h); +} + +void SDL2DisplayWindow::Show() +{ + SDL_ShowWindow(WindowHandle); +} + +void SDL2DisplayWindow::ShowFullscreen() +{ + SDL_SetWindowFullscreen(WindowHandle, SDL_WINDOW_FULLSCREEN_DESKTOP); +} + +void SDL2DisplayWindow::ShowMaximized() +{ + SDL_ShowWindow(WindowHandle); + SDL_MaximizeWindow(WindowHandle); +} + +void SDL2DisplayWindow::ShowMinimized() +{ + SDL_ShowWindow(WindowHandle); + SDL_MinimizeWindow(WindowHandle); +} + +void SDL2DisplayWindow::ShowNormal() +{ + SDL_ShowWindow(WindowHandle); + SDL_SetWindowFullscreen(WindowHandle, 0); +} + +void SDL2DisplayWindow::Hide() +{ + SDL_HideWindow(WindowHandle); +} + +void SDL2DisplayWindow::Activate() +{ + SDL_RaiseWindow(WindowHandle); +} + +void SDL2DisplayWindow::ShowCursor(bool enable) +{ + SDL_ShowCursor(enable); +} + +void SDL2DisplayWindow::LockCursor() +{ + SDL_SetWindowGrab(WindowHandle, SDL_TRUE); + SDL_ShowCursor(0); +} + +void SDL2DisplayWindow::UnlockCursor() +{ + SDL_SetWindowGrab(WindowHandle, SDL_FALSE); + SDL_ShowCursor(1); +} + +void SDL2DisplayWindow::CaptureMouse() +{ +} + +void SDL2DisplayWindow::ReleaseMouseCapture() +{ +} + +void SDL2DisplayWindow::SetCursor(StandardCursor cursor) +{ +} + +void SDL2DisplayWindow::Update() +{ + SDL_Event event = {}; + event.type = PaintEventNumber; + event.user.windowID = SDL_GetWindowID(WindowHandle); + SDL_PushEvent(&event); +} + +bool SDL2DisplayWindow::GetKeyState(EInputKey key) +{ + int numkeys = 0; + const Uint8* state = SDL_GetKeyboardState(&numkeys); + if (!state) return false; + + SDL_Scancode index = InputKeyToScancode(key); + return (index < numkeys) ? state[index] != 0 : false; +} + +Rect SDL2DisplayWindow::GetWindowFrame() const +{ + int x = 0; + int y = 0; + int w = 0; + int h = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowPosition(WindowHandle, &x, &y); + SDL_GetWindowSize(WindowHandle, &w, &h); + return Rect::xywh(x / uiscale, y / uiscale, w / uiscale, h / uiscale); +} + +Size SDL2DisplayWindow::GetClientSize() const +{ + int w = 0; + int h = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowSize(WindowHandle, &w, &h); + return Size(w / uiscale, h / uiscale); +} + +int SDL2DisplayWindow::GetPixelWidth() const +{ + int w = 0; + int h = 0; + int result = SDL_GetRendererOutputSize(RendererHandle, &w, &h); + return w; +} + +int SDL2DisplayWindow::GetPixelHeight() const +{ + int w = 0; + int h = 0; + int result = SDL_GetRendererOutputSize(RendererHandle, &w, &h); + return h; +} + +double SDL2DisplayWindow::GetDpiScale() const +{ + // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. + return 1.0; +} + +void SDL2DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + if (!BackBufferTexture || BackBufferWidth != width || BackBufferHeight != height) + { + if (BackBufferTexture) + { + SDL_DestroyTexture(BackBufferTexture); + BackBufferTexture = nullptr; + } + + BackBufferTexture = SDL_CreateTexture(RendererHandle, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height); + if (!BackBufferTexture) + return; + + BackBufferWidth = width; + BackBufferHeight = height; + } + + int destpitch = 0; + void* dest = nullptr; + int result = SDL_LockTexture(BackBufferTexture, nullptr, &dest, &destpitch); + if (result != 0) return; + for (int y = 0; y < height; y++) + { + const void* sline = pixels + y * width; + void* dline = (uint8_t*)dest + y * destpitch; + memcpy(dline, sline, width << 2); + } + SDL_UnlockTexture(BackBufferTexture); + + SDL_RenderCopy(RendererHandle, BackBufferTexture, nullptr, nullptr); + SDL_RenderPresent(RendererHandle); +} + +void SDL2DisplayWindow::SetBorderColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +void SDL2DisplayWindow::SetCaptionColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +void SDL2DisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +std::string SDL2DisplayWindow::GetClipboardText() +{ + char* buffer = SDL_GetClipboardText(); + if (!buffer) + return {}; + std::string text = buffer; + SDL_free(buffer); + return text; +} + +void SDL2DisplayWindow::SetClipboardText(const std::string& text) +{ + SDL_SetClipboardText(text.c_str()); +} + +void SDL2DisplayWindow::ProcessEvents() +{ + CheckInitSDL(); + + SDL_Event event; + while (SDL_PollEvent(&event) != 0) + { + DispatchEvent(event); + } +} + +void SDL2DisplayWindow::RunLoop() +{ + CheckInitSDL(); + + while (!ExitRunLoop) + { + SDL_Event event; + int result = SDL_WaitEvent(&event); + if (result == 0) + throw std::runtime_error(std::string("SDL_WaitEvent failed:") + SDL_GetError()); + DispatchEvent(event); + } +} + +void SDL2DisplayWindow::ExitLoop() +{ + CheckInitSDL(); + + ExitRunLoop = true; +} + +Size SDL2DisplayWindow::GetScreenSize() +{ + CheckInitSDL(); + + SDL_Rect rect = {}; + int result = SDL_GetDisplayBounds(0, &rect); + if (result != 0) + throw std::runtime_error(std::string("Unable to get screen size:") + SDL_GetError()); + + double uiscale = 1.0; // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. + return Size(rect.w / uiscale, rect.h / uiscale); +} + +void* SDL2DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + CheckInitSDL(); + + // To do: implement timers + + return nullptr; +} + +void SDL2DisplayWindow::StopTimer(void* timerID) +{ + CheckInitSDL(); + + // To do: implement timers +} + +SDL2DisplayWindow* SDL2DisplayWindow::FindEventWindow(const SDL_Event& event) +{ + int windowID; + switch (event.type) + { + case SDL_WINDOWEVENT: windowID = event.window.windowID; break; + case SDL_TEXTINPUT: windowID = event.text.windowID; break; + case SDL_KEYUP: windowID = event.key.windowID; break; + case SDL_KEYDOWN: windowID = event.key.windowID; break; + case SDL_MOUSEBUTTONUP: windowID = event.button.windowID; break; + case SDL_MOUSEBUTTONDOWN: windowID = event.button.windowID; break; + case SDL_MOUSEWHEEL: windowID = event.wheel.windowID; break; + case SDL_MOUSEMOTION: windowID = event.motion.windowID; break; + default: + if (event.type == PaintEventNumber) windowID = event.user.windowID; + else return nullptr; + } + + auto it = WindowList.find(windowID); + return it != WindowList.end() ? it->second : nullptr; +} + +void SDL2DisplayWindow::DispatchEvent(const SDL_Event& event) +{ + SDL2DisplayWindow* window = FindEventWindow(event); + if (!window) return; + + switch (event.type) + { + case SDL_WINDOWEVENT: window->OnWindowEvent(event.window); break; + case SDL_TEXTINPUT: window->OnTextInput(event.text); break; + case SDL_KEYUP: window->OnKeyUp(event.key); break; + case SDL_KEYDOWN: window->OnKeyDown(event.key); break; + case SDL_MOUSEBUTTONUP: window->OnMouseButtonUp(event.button); break; + case SDL_MOUSEBUTTONDOWN: window->OnMouseButtonDown(event.button); break; + case SDL_MOUSEWHEEL: window->OnMouseWheel(event.wheel); break; + case SDL_MOUSEMOTION: window->OnMouseMotion(event.motion); break; + default: + if (event.type == PaintEventNumber) window->OnPaintEvent(); + } +} + +void SDL2DisplayWindow::OnWindowEvent(const SDL_WindowEvent& event) +{ + switch (event.event) + { + case SDL_WINDOWEVENT_CLOSE: WindowHost->OnWindowClose(); break; + case SDL_WINDOWEVENT_MOVED: WindowHost->OnWindowGeometryChanged(); break; + case SDL_WINDOWEVENT_RESIZED: WindowHost->OnWindowGeometryChanged(); break; + case SDL_WINDOWEVENT_SHOWN: WindowHost->OnWindowPaint(); break; + case SDL_WINDOWEVENT_EXPOSED: WindowHost->OnWindowPaint(); break; + case SDL_WINDOWEVENT_FOCUS_GAINED: WindowHost->OnWindowActivated(); break; + case SDL_WINDOWEVENT_FOCUS_LOST: WindowHost->OnWindowDeactivated(); break; + } +} + +void SDL2DisplayWindow::OnTextInput(const SDL_TextInputEvent& event) +{ + WindowHost->OnWindowKeyChar(event.text); +} + +void SDL2DisplayWindow::OnKeyUp(const SDL_KeyboardEvent& event) +{ + WindowHost->OnWindowKeyUp(ScancodeToInputKey(event.keysym.scancode)); +} + +void SDL2DisplayWindow::OnKeyDown(const SDL_KeyboardEvent& event) +{ + WindowHost->OnWindowKeyDown(ScancodeToInputKey(event.keysym.scancode)); +} + +EInputKey SDL2DisplayWindow::GetMouseButtonKey(const SDL_MouseButtonEvent& event) +{ + switch (event.button) + { + case SDL_BUTTON_LEFT: return IK_LeftMouse; + case SDL_BUTTON_MIDDLE: return IK_MiddleMouse; + case SDL_BUTTON_RIGHT: return IK_RightMouse; + // case SDL_BUTTON_X1: return IK_XButton1; + // case SDL_BUTTON_X2: return IK_XButton2; + default: return IK_None; + } +} + +void SDL2DisplayWindow::OnMouseButtonUp(const SDL_MouseButtonEvent& event) +{ + EInputKey key = GetMouseButtonKey(event); + if (key != IK_None) + { + WindowHost->OnWindowMouseUp(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseButtonDown(const SDL_MouseButtonEvent& event) +{ + EInputKey key = GetMouseButtonKey(event); + if (key != IK_None) + { + WindowHost->OnWindowMouseDown(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseWheel(const SDL_MouseWheelEvent& event) +{ + EInputKey key = (event.y > 0) ? IK_MouseWheelUp : (event.y < 0) ? IK_MouseWheelDown : IK_None; + if (key != IK_None) + { + WindowHost->OnWindowMouseWheel(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseMotion(const SDL_MouseMotionEvent& event) +{ + WindowHost->OnWindowMouseMove(GetMousePos(event)); +} + +void SDL2DisplayWindow::OnPaintEvent() +{ + WindowHost->OnWindowPaint(); +} + +EInputKey SDL2DisplayWindow::ScancodeToInputKey(SDL_Scancode keycode) +{ + switch (keycode) + { + case SDL_SCANCODE_BACKSPACE: return IK_Backspace; + case SDL_SCANCODE_TAB: return IK_Tab; + case SDL_SCANCODE_CLEAR: return IK_OEMClear; + case SDL_SCANCODE_RETURN: return IK_Enter; + case SDL_SCANCODE_MENU: return IK_Alt; + case SDL_SCANCODE_PAUSE: return IK_Pause; + case SDL_SCANCODE_ESCAPE: return IK_Escape; + case SDL_SCANCODE_SPACE: return IK_Space; + case SDL_SCANCODE_END: return IK_End; + case SDL_SCANCODE_HOME: return IK_Home; + case SDL_SCANCODE_LEFT: return IK_Left; + case SDL_SCANCODE_UP: return IK_Up; + case SDL_SCANCODE_RIGHT: return IK_Right; + case SDL_SCANCODE_DOWN: return IK_Down; + case SDL_SCANCODE_SELECT: return IK_Select; + case SDL_SCANCODE_PRINTSCREEN: return IK_Print; + case SDL_SCANCODE_EXECUTE: return IK_Execute; + case SDL_SCANCODE_INSERT: return IK_Insert; + case SDL_SCANCODE_DELETE: return IK_Delete; + case SDL_SCANCODE_HELP: return IK_Help; + case SDL_SCANCODE_0: return IK_0; + case SDL_SCANCODE_1: return IK_1; + case SDL_SCANCODE_2: return IK_2; + case SDL_SCANCODE_3: return IK_3; + case SDL_SCANCODE_4: return IK_4; + case SDL_SCANCODE_5: return IK_5; + case SDL_SCANCODE_6: return IK_6; + case SDL_SCANCODE_7: return IK_7; + case SDL_SCANCODE_8: return IK_8; + case SDL_SCANCODE_9: return IK_9; + case SDL_SCANCODE_A: return IK_A; + case SDL_SCANCODE_B: return IK_B; + case SDL_SCANCODE_C: return IK_C; + case SDL_SCANCODE_D: return IK_D; + case SDL_SCANCODE_E: return IK_E; + case SDL_SCANCODE_F: return IK_F; + case SDL_SCANCODE_G: return IK_G; + case SDL_SCANCODE_H: return IK_H; + case SDL_SCANCODE_I: return IK_I; + case SDL_SCANCODE_J: return IK_J; + case SDL_SCANCODE_K: return IK_K; + case SDL_SCANCODE_L: return IK_L; + case SDL_SCANCODE_M: return IK_M; + case SDL_SCANCODE_N: return IK_N; + case SDL_SCANCODE_O: return IK_O; + case SDL_SCANCODE_P: return IK_P; + case SDL_SCANCODE_Q: return IK_Q; + case SDL_SCANCODE_R: return IK_R; + case SDL_SCANCODE_S: return IK_S; + case SDL_SCANCODE_T: return IK_T; + case SDL_SCANCODE_U: return IK_U; + case SDL_SCANCODE_V: return IK_V; + case SDL_SCANCODE_W: return IK_W; + case SDL_SCANCODE_X: return IK_X; + case SDL_SCANCODE_Y: return IK_Y; + case SDL_SCANCODE_Z: return IK_Z; + case SDL_SCANCODE_KP_0: return IK_NumPad0; + case SDL_SCANCODE_KP_1: return IK_NumPad1; + case SDL_SCANCODE_KP_2: return IK_NumPad2; + case SDL_SCANCODE_KP_3: return IK_NumPad3; + case SDL_SCANCODE_KP_4: return IK_NumPad4; + case SDL_SCANCODE_KP_5: return IK_NumPad5; + case SDL_SCANCODE_KP_6: return IK_NumPad6; + case SDL_SCANCODE_KP_7: return IK_NumPad7; + case SDL_SCANCODE_KP_8: return IK_NumPad8; + case SDL_SCANCODE_KP_9: return IK_NumPad9; + // case SDL_SCANCODE_KP_ENTER: return IK_NumPadEnter; + // case SDL_SCANCODE_KP_MULTIPLY: return IK_Multiply; + // case SDL_SCANCODE_KP_PLUS: return IK_Add; + case SDL_SCANCODE_SEPARATOR: return IK_Separator; + // case SDL_SCANCODE_KP_MINUS: return IK_Subtract; + case SDL_SCANCODE_KP_PERIOD: return IK_NumPadPeriod; + // case SDL_SCANCODE_KP_DIVIDE: return IK_Divide; + case SDL_SCANCODE_F1: return IK_F1; + case SDL_SCANCODE_F2: return IK_F2; + case SDL_SCANCODE_F3: return IK_F3; + case SDL_SCANCODE_F4: return IK_F4; + case SDL_SCANCODE_F5: return IK_F5; + case SDL_SCANCODE_F6: return IK_F6; + case SDL_SCANCODE_F7: return IK_F7; + case SDL_SCANCODE_F8: return IK_F8; + case SDL_SCANCODE_F9: return IK_F9; + case SDL_SCANCODE_F10: return IK_F10; + case SDL_SCANCODE_F11: return IK_F11; + case SDL_SCANCODE_F12: return IK_F12; + case SDL_SCANCODE_F13: return IK_F13; + case SDL_SCANCODE_F14: return IK_F14; + case SDL_SCANCODE_F15: return IK_F15; + case SDL_SCANCODE_F16: return IK_F16; + case SDL_SCANCODE_F17: return IK_F17; + case SDL_SCANCODE_F18: return IK_F18; + case SDL_SCANCODE_F19: return IK_F19; + case SDL_SCANCODE_F20: return IK_F20; + case SDL_SCANCODE_F21: return IK_F21; + case SDL_SCANCODE_F22: return IK_F22; + case SDL_SCANCODE_F23: return IK_F23; + case SDL_SCANCODE_F24: return IK_F24; + case SDL_SCANCODE_NUMLOCKCLEAR: return IK_NumLock; + case SDL_SCANCODE_SCROLLLOCK: return IK_ScrollLock; + case SDL_SCANCODE_LSHIFT: return IK_LShift; + case SDL_SCANCODE_RSHIFT: return IK_RShift; + case SDL_SCANCODE_LCTRL: return IK_LControl; + case SDL_SCANCODE_RCTRL: return IK_RControl; + case SDL_SCANCODE_GRAVE: return IK_Tilde; + default: return IK_None; + } +} + +SDL_Scancode SDL2DisplayWindow::InputKeyToScancode(EInputKey inputkey) +{ + switch (inputkey) + { + case IK_Backspace: return SDL_SCANCODE_BACKSPACE; + case IK_Tab: return SDL_SCANCODE_TAB; + case IK_OEMClear: return SDL_SCANCODE_CLEAR; + case IK_Enter: return SDL_SCANCODE_RETURN; + case IK_Alt: return SDL_SCANCODE_MENU; + case IK_Pause: return SDL_SCANCODE_PAUSE; + case IK_Escape: return SDL_SCANCODE_ESCAPE; + case IK_Space: return SDL_SCANCODE_SPACE; + case IK_End: return SDL_SCANCODE_END; + case IK_Home: return SDL_SCANCODE_HOME; + case IK_Left: return SDL_SCANCODE_LEFT; + case IK_Up: return SDL_SCANCODE_UP; + case IK_Right: return SDL_SCANCODE_RIGHT; + case IK_Down: return SDL_SCANCODE_DOWN; + case IK_Select: return SDL_SCANCODE_SELECT; + case IK_Print: return SDL_SCANCODE_PRINTSCREEN; + case IK_Execute: return SDL_SCANCODE_EXECUTE; + case IK_Insert: return SDL_SCANCODE_INSERT; + case IK_Delete: return SDL_SCANCODE_DELETE; + case IK_Help: return SDL_SCANCODE_HELP; + case IK_0: return SDL_SCANCODE_0; + case IK_1: return SDL_SCANCODE_1; + case IK_2: return SDL_SCANCODE_2; + case IK_3: return SDL_SCANCODE_3; + case IK_4: return SDL_SCANCODE_4; + case IK_5: return SDL_SCANCODE_5; + case IK_6: return SDL_SCANCODE_6; + case IK_7: return SDL_SCANCODE_7; + case IK_8: return SDL_SCANCODE_8; + case IK_9: return SDL_SCANCODE_9; + case IK_A: return SDL_SCANCODE_A; + case IK_B: return SDL_SCANCODE_B; + case IK_C: return SDL_SCANCODE_C; + case IK_D: return SDL_SCANCODE_D; + case IK_E: return SDL_SCANCODE_E; + case IK_F: return SDL_SCANCODE_F; + case IK_G: return SDL_SCANCODE_G; + case IK_H: return SDL_SCANCODE_H; + case IK_I: return SDL_SCANCODE_I; + case IK_J: return SDL_SCANCODE_J; + case IK_K: return SDL_SCANCODE_K; + case IK_L: return SDL_SCANCODE_L; + case IK_M: return SDL_SCANCODE_M; + case IK_N: return SDL_SCANCODE_N; + case IK_O: return SDL_SCANCODE_O; + case IK_P: return SDL_SCANCODE_P; + case IK_Q: return SDL_SCANCODE_Q; + case IK_R: return SDL_SCANCODE_R; + case IK_S: return SDL_SCANCODE_S; + case IK_T: return SDL_SCANCODE_T; + case IK_U: return SDL_SCANCODE_U; + case IK_V: return SDL_SCANCODE_V; + case IK_W: return SDL_SCANCODE_W; + case IK_X: return SDL_SCANCODE_X; + case IK_Y: return SDL_SCANCODE_Y; + case IK_Z: return SDL_SCANCODE_Z; + case IK_NumPad0: return SDL_SCANCODE_KP_0; + case IK_NumPad1: return SDL_SCANCODE_KP_1; + case IK_NumPad2: return SDL_SCANCODE_KP_2; + case IK_NumPad3: return SDL_SCANCODE_KP_3; + case IK_NumPad4: return SDL_SCANCODE_KP_4; + case IK_NumPad5: return SDL_SCANCODE_KP_5; + case IK_NumPad6: return SDL_SCANCODE_KP_6; + case IK_NumPad7: return SDL_SCANCODE_KP_7; + case IK_NumPad8: return SDL_SCANCODE_KP_8; + case IK_NumPad9: return SDL_SCANCODE_KP_9; + // case IK_NumPadEnter: return SDL_SCANCODE_KP_ENTER; + // case IK_Multiply return SDL_SCANCODE_KP_MULTIPLY:; + // case IK_Add: return SDL_SCANCODE_KP_PLUS; + case IK_Separator: return SDL_SCANCODE_SEPARATOR; + // case IK_Subtract: return SDL_SCANCODE_KP_MINUS; + case IK_NumPadPeriod: return SDL_SCANCODE_KP_PERIOD; + // case IK_Divide: return SDL_SCANCODE_KP_DIVIDE; + case IK_F1: return SDL_SCANCODE_F1; + case IK_F2: return SDL_SCANCODE_F2; + case IK_F3: return SDL_SCANCODE_F3; + case IK_F4: return SDL_SCANCODE_F4; + case IK_F5: return SDL_SCANCODE_F5; + case IK_F6: return SDL_SCANCODE_F6; + case IK_F7: return SDL_SCANCODE_F7; + case IK_F8: return SDL_SCANCODE_F8; + case IK_F9: return SDL_SCANCODE_F9; + case IK_F10: return SDL_SCANCODE_F10; + case IK_F11: return SDL_SCANCODE_F11; + case IK_F12: return SDL_SCANCODE_F12; + case IK_F13: return SDL_SCANCODE_F13; + case IK_F14: return SDL_SCANCODE_F14; + case IK_F15: return SDL_SCANCODE_F15; + case IK_F16: return SDL_SCANCODE_F16; + case IK_F17: return SDL_SCANCODE_F17; + case IK_F18: return SDL_SCANCODE_F18; + case IK_F19: return SDL_SCANCODE_F19; + case IK_F20: return SDL_SCANCODE_F20; + case IK_F21: return SDL_SCANCODE_F21; + case IK_F22: return SDL_SCANCODE_F22; + case IK_F23: return SDL_SCANCODE_F23; + case IK_F24: return SDL_SCANCODE_F24; + case IK_NumLock: return SDL_SCANCODE_NUMLOCKCLEAR; + case IK_ScrollLock: return SDL_SCANCODE_SCROLLLOCK; + case IK_LShift: return SDL_SCANCODE_LSHIFT; + case IK_RShift: return SDL_SCANCODE_RSHIFT; + case IK_LControl: return SDL_SCANCODE_LCTRL; + case IK_RControl: return SDL_SCANCODE_RCTRL; + case IK_Tilde: return SDL_SCANCODE_GRAVE; + default: return (SDL_Scancode)0; + } +} diff --git a/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.h b/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.h new file mode 100644 index 0000000000..fa0e8253cc --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +class SDL2DisplayWindow : public DisplayWindow +{ +public: + SDL2DisplayWindow(DisplayWindowHost* windowHost); + ~SDL2DisplayWindow(); + + void SetWindowTitle(const std::string& text) override; + void SetWindowFrame(const Rect& box) override; + void SetClientFrame(const Rect& box) override; + void Show() override; + void ShowFullscreen() override; + void ShowMaximized() override; + void ShowMinimized() override; + void ShowNormal() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(EInputKey key) override; + void SetCursor(StandardCursor cursor) override; + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + static void DispatchEvent(const SDL_Event& event); + static SDL2DisplayWindow* FindEventWindow(const SDL_Event& event); + + void OnWindowEvent(const SDL_WindowEvent& event); + void OnTextInput(const SDL_TextInputEvent& event); + void OnKeyUp(const SDL_KeyboardEvent& event); + void OnKeyDown(const SDL_KeyboardEvent& event); + void OnMouseButtonUp(const SDL_MouseButtonEvent& event); + void OnMouseButtonDown(const SDL_MouseButtonEvent& event); + void OnMouseWheel(const SDL_MouseWheelEvent& event); + void OnMouseMotion(const SDL_MouseMotionEvent& event); + void OnPaintEvent(); + + EInputKey GetMouseButtonKey(const SDL_MouseButtonEvent& event); + + static EInputKey ScancodeToInputKey(SDL_Scancode keycode); + static SDL_Scancode InputKeyToScancode(EInputKey inputkey); + + template + Point GetMousePos(const T& event) + { + double uiscale = GetDpiScale(); + return Point(event.x / uiscale, event.y / uiscale); + } + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + static Size GetScreenSize(); + + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + + DisplayWindowHost* WindowHost = nullptr; + SDL_Window* WindowHandle = nullptr; + SDL_Renderer* RendererHandle = nullptr; + SDL_Texture* BackBufferTexture = nullptr; + int BackBufferWidth = 0; + int BackBufferHeight = 0; + + static bool ExitRunLoop; + static Uint32 PaintEventNumber; + static std::unordered_map WindowList; +}; diff --git a/libraries/ZWidget/src/window/win32/win32window.cpp b/libraries/ZWidget/src/window/win32/win32window.cpp new file mode 100644 index 0000000000..f279bcf3f3 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32window.cpp @@ -0,0 +1,665 @@ + +#include "win32window.h" +#include +#include +#include +#include +#include + +#pragma comment(lib, "dwmapi.lib") + +#ifndef HID_USAGE_PAGE_GENERIC +#define HID_USAGE_PAGE_GENERIC ((USHORT) 0x01) +#endif + +#ifndef HID_USAGE_GENERIC_MOUSE +#define HID_USAGE_GENERIC_MOUSE ((USHORT) 0x02) +#endif + +#ifndef HID_USAGE_GENERIC_JOYSTICK +#define HID_USAGE_GENERIC_JOYSTICK ((USHORT) 0x04) +#endif + +#ifndef HID_USAGE_GENERIC_GAMEPAD +#define HID_USAGE_GENERIC_GAMEPAD ((USHORT) 0x05) +#endif + +#ifndef RIDEV_INPUTSINK +#define RIDEV_INPUTSINK (0x100) +#endif + +static std::string from_utf16(const std::wstring& str) +{ + if (str.empty()) return {}; + int needed = WideCharToMultiByte(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0, nullptr, nullptr); + if (needed == 0) + throw std::runtime_error("WideCharToMultiByte failed"); + std::string result; + result.resize(needed); + needed = WideCharToMultiByte(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], (int)result.size(), nullptr, nullptr); + if (needed == 0) + throw std::runtime_error("WideCharToMultiByte failed"); + return result; +} + +static std::wstring to_utf16(const std::string& str) +{ + if (str.empty()) return {}; + int needed = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0); + if (needed == 0) + throw std::runtime_error("MultiByteToWideChar failed"); + std::wstring result; + result.resize(needed); + needed = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], (int)result.size()); + if (needed == 0) + throw std::runtime_error("MultiByteToWideChar failed"); + return result; +} + +Win32Window::Win32Window(DisplayWindowHost* windowHost) : WindowHost(windowHost) +{ + Windows.push_front(this); + WindowsIterator = Windows.begin(); + + WNDCLASSEX classdesc = {}; + classdesc.cbSize = sizeof(WNDCLASSEX); + classdesc.hInstance = GetModuleHandle(0); + classdesc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; + classdesc.lpszClassName = L"ZWidgetWindow"; + classdesc.lpfnWndProc = &Win32Window::WndProc; + RegisterClassEx(&classdesc); + + // Microsoft logic at its finest: + // WS_EX_DLGMODALFRAME hides the sysmenu icon + // WS_CAPTION shows the caption (yay! actually a flag that does what it says it does!) + // WS_SYSMENU shows the min/max/close buttons + // WS_THICKFRAME makes the window resizable + CreateWindowEx(WS_EX_APPWINDOW | WS_EX_DLGMODALFRAME, L"ZWidgetWindow", L"", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, 0, 0, 100, 100, 0, 0, GetModuleHandle(0), this); + + /* + RAWINPUTDEVICE rid; + rid.usUsagePage = HID_USAGE_PAGE_GENERIC; + rid.usUsage = HID_USAGE_GENERIC_MOUSE; + rid.dwFlags = RIDEV_INPUTSINK; + rid.hwndTarget = WindowHandle; + BOOL result = RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE)); + */ +} + +Win32Window::~Win32Window() +{ + if (WindowHandle) + { + DestroyWindow(WindowHandle); + WindowHandle = 0; + } + + Windows.erase(WindowsIterator); +} + +void Win32Window::SetWindowTitle(const std::string& text) +{ + SetWindowText(WindowHandle, to_utf16(text).c_str()); +} + +void Win32Window::SetBorderColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle, 34/*DWMWA_BORDER_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32Window::SetCaptionColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle, 35/*DWMWA_CAPTION_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32Window::SetCaptionTextColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle, 36/*DWMWA_TEXT_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32Window::SetWindowFrame(const Rect& box) +{ + double dpiscale = GetDpiScale(); + SetWindowPos(WindowHandle, nullptr, (int)std::round(box.x * dpiscale), (int)std::round(box.y * dpiscale), (int)std::round(box.width * dpiscale), (int)std::round(box.height * dpiscale), SWP_NOACTIVATE | SWP_NOZORDER); +} + +void Win32Window::SetClientFrame(const Rect& box) +{ + double dpiscale = GetDpiScale(); + + RECT rect = {}; + rect.left = (int)std::round(box.x * dpiscale); + rect.top = (int)std::round(box.y * dpiscale); + rect.right = rect.left + (int)std::round(box.width * dpiscale); + rect.bottom = rect.top + (int)std::round(box.height * dpiscale); + + DWORD style = (DWORD)GetWindowLongPtr(WindowHandle, GWL_STYLE); + DWORD exstyle = (DWORD)GetWindowLongPtr(WindowHandle, GWL_EXSTYLE); + AdjustWindowRectExForDpi(&rect, style, FALSE, exstyle, GetDpiForWindow(WindowHandle)); + + SetWindowPos(WindowHandle, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE | SWP_NOZORDER); +} + +void Win32Window::Show() +{ + ShowWindow(WindowHandle, SW_SHOW); +} + +void Win32Window::ShowFullscreen() +{ + HDC screenDC = GetDC(0); + int width = GetDeviceCaps(screenDC, HORZRES); + int height = GetDeviceCaps(screenDC, VERTRES); + ReleaseDC(0, screenDC); + SetWindowLongPtr(WindowHandle, GWL_EXSTYLE, WS_EX_APPWINDOW); + SetWindowLongPtr(WindowHandle, GWL_STYLE, WS_OVERLAPPED); + SetWindowPos(WindowHandle, HWND_TOP, 0, 0, width, height, SWP_FRAMECHANGED | SWP_SHOWWINDOW); + Fullscreen = true; +} + +void Win32Window::ShowMaximized() +{ + ShowWindow(WindowHandle, SW_SHOWMAXIMIZED); +} + +void Win32Window::ShowMinimized() +{ + ShowWindow(WindowHandle, SW_SHOWMINIMIZED); +} + +void Win32Window::ShowNormal() +{ + ShowWindow(WindowHandle, SW_NORMAL); +} + +void Win32Window::Hide() +{ + ShowWindow(WindowHandle, SW_HIDE); +} + +void Win32Window::Activate() +{ + SetFocus(WindowHandle); +} + +void Win32Window::ShowCursor(bool enable) +{ +} + +void Win32Window::LockCursor() +{ + if (!MouseLocked) + { + MouseLocked = true; + GetCursorPos(&MouseLockPos); + ::ShowCursor(FALSE); + } +} + +void Win32Window::UnlockCursor() +{ + if (MouseLocked) + { + MouseLocked = false; + SetCursorPos(MouseLockPos.x, MouseLockPos.y); + ::ShowCursor(TRUE); + } +} + +void Win32Window::CaptureMouse() +{ + SetCapture(WindowHandle); +} + +void Win32Window::ReleaseMouseCapture() +{ + ReleaseCapture(); +} + +void Win32Window::Update() +{ + InvalidateRect(WindowHandle, nullptr, FALSE); +} + +bool Win32Window::GetKeyState(EInputKey key) +{ + return ::GetKeyState((int)key) & 0x8000; // High bit (0x8000) means key is down, Low bit (0x0001) means key is sticky on (like Caps Lock, Num Lock, etc.) +} + +void Win32Window::SetCursor(StandardCursor cursor) +{ + if (cursor != CurrentCursor) + { + CurrentCursor = cursor; + UpdateCursor(); + } +} + +Rect Win32Window::GetWindowFrame() const +{ + RECT box = {}; + GetWindowRect(WindowHandle, &box); + double dpiscale = GetDpiScale(); + return Rect(box.left / dpiscale, box.top / dpiscale, box.right / dpiscale, box.bottom / dpiscale); +} + +Size Win32Window::GetClientSize() const +{ + RECT box = {}; + GetClientRect(WindowHandle, &box); + double dpiscale = GetDpiScale(); + return Size(box.right / dpiscale, box.bottom / dpiscale); +} + +int Win32Window::GetPixelWidth() const +{ + RECT box = {}; + GetClientRect(WindowHandle, &box); + return box.right; +} + +int Win32Window::GetPixelHeight() const +{ + RECT box = {}; + GetClientRect(WindowHandle, &box); + return box.bottom; +} + +double Win32Window::GetDpiScale() const +{ + return GetDpiForWindow(WindowHandle) / 96.0; +} + +std::string Win32Window::GetClipboardText() +{ + BOOL result = OpenClipboard(WindowHandle); + if (result == FALSE) + throw std::runtime_error("Unable to open clipboard"); + + HANDLE handle = GetClipboardData(CF_UNICODETEXT); + if (handle == 0) + { + CloseClipboard(); + return std::string(); + } + + std::wstring::value_type* data = (std::wstring::value_type*)GlobalLock(handle); + if (data == 0) + { + CloseClipboard(); + return std::string(); + } + std::string str = from_utf16(data); + GlobalUnlock(handle); + + CloseClipboard(); + return str; +} + +void Win32Window::SetClipboardText(const std::string& text) +{ + std::wstring text16 = to_utf16(text); + + BOOL result = OpenClipboard(WindowHandle); + if (result == FALSE) + throw std::runtime_error("Unable to open clipboard"); + + result = EmptyClipboard(); + if (result == FALSE) + { + CloseClipboard(); + throw std::runtime_error("Unable to empty clipboard"); + } + + unsigned int length = (text16.length() + 1) * sizeof(std::wstring::value_type); + HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, length); + if (handle == 0) + { + CloseClipboard(); + throw std::runtime_error("Unable to allocate clipboard memory"); + } + + void* data = GlobalLock(handle); + if (data == 0) + { + GlobalFree(handle); + CloseClipboard(); + throw std::runtime_error("Unable to lock clipboard memory"); + } + memcpy(data, text16.c_str(), length); + GlobalUnlock(handle); + + HANDLE data_result = SetClipboardData(CF_UNICODETEXT, handle); + + if (data_result == 0) + { + GlobalFree(handle); + CloseClipboard(); + throw std::runtime_error("Unable to set clipboard data"); + } + + CloseClipboard(); +} + +void Win32Window::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + BITMAPV5HEADER header = {}; + header.bV5Size = sizeof(BITMAPV5HEADER); + header.bV5Width = width; + header.bV5Height = -height; + header.bV5Planes = 1; + header.bV5BitCount = 32; + header.bV5Compression = BI_BITFIELDS; + header.bV5AlphaMask = 0xff000000; + header.bV5RedMask = 0x00ff0000; + header.bV5GreenMask = 0x0000ff00; + header.bV5BlueMask = 0x000000ff; + header.bV5SizeImage = width * height * sizeof(uint32_t); + header.bV5CSType = LCS_sRGB; + + HDC dc = PaintDC; + if (dc != 0) + { + int result = SetDIBitsToDevice(dc, 0, 0, width, height, 0, 0, 0, height, pixels, (const BITMAPINFO*)&header, BI_RGB); + ReleaseDC(WindowHandle, dc); + } +} + +LRESULT Win32Window::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) +{ + LPARAM result = 0; + if (DwmDefWindowProc(WindowHandle, msg, wparam, lparam, &result)) + return result; + + if (msg == WM_INPUT) + { + bool hasFocus = GetFocus() != 0; + + HRAWINPUT handle = (HRAWINPUT)lparam; + UINT size = 0; + UINT result = GetRawInputData(handle, RID_INPUT, 0, &size, sizeof(RAWINPUTHEADER)); + if (result == 0 && size > 0) + { + size *= 2; + std::vector buffer(size); + result = GetRawInputData(handle, RID_INPUT, buffer.data(), &size, sizeof(RAWINPUTHEADER)); + if (result >= 0) + { + RAWINPUT* rawinput = (RAWINPUT*)buffer.data(); + if (rawinput->header.dwType == RIM_TYPEMOUSE) + { + if (hasFocus) + WindowHost->OnWindowRawMouseMove(rawinput->data.mouse.lLastX, rawinput->data.mouse.lLastY); + } + } + } + return DefWindowProc(WindowHandle, msg, wparam, lparam); + } + else if (msg == WM_PAINT) + { + PAINTSTRUCT paintStruct = {}; + PaintDC = BeginPaint(WindowHandle, &paintStruct); + if (PaintDC) + { + WindowHost->OnWindowPaint(); + EndPaint(WindowHandle, &paintStruct); + PaintDC = 0; + } + return 0; + } + else if (msg == WM_ACTIVATE) + { + WindowHost->OnWindowActivated(); + } + else if (msg == WM_MOUSEMOVE) + { + if (MouseLocked && GetFocus() != 0) + { + RECT box = {}; + GetClientRect(WindowHandle, &box); + + POINT center = {}; + center.x = box.right / 2; + center.y = box.bottom / 2; + ClientToScreen(WindowHandle, ¢er); + + SetCursorPos(center.x, center.y); + } + else + { + UpdateCursor(); + } + + WindowHost->OnWindowMouseMove(GetLParamPos(lparam)); + } + else if (msg == WM_LBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_LeftMouse); + } + else if (msg == WM_LBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_LeftMouse); + } + else if (msg == WM_LBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_LeftMouse); + } + else if (msg == WM_MBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_MiddleMouse); + } + else if (msg == WM_MBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_MiddleMouse); + } + else if (msg == WM_MBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_MiddleMouse); + } + else if (msg == WM_RBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_RightMouse); + } + else if (msg == WM_RBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_RightMouse); + } + else if (msg == WM_RBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_RightMouse); + } + else if (msg == WM_MOUSEWHEEL) + { + double delta = GET_WHEEL_DELTA_WPARAM(wparam) / (double)WHEEL_DELTA; + + // Note: WM_MOUSEWHEEL uses screen coordinates. GetLParamPos assumes client coordinates. + double dpiscale = GetDpiScale(); + POINT pos; + pos.x = GET_X_LPARAM(lparam); + pos.y = GET_Y_LPARAM(lparam); + ScreenToClient(WindowHandle, &pos); + + WindowHost->OnWindowMouseWheel(Point(pos.x / dpiscale, pos.y / dpiscale), delta < 0.0 ? IK_MouseWheelDown : IK_MouseWheelUp); + } + else if (msg == WM_CHAR) + { + wchar_t buf[2] = { (wchar_t)wparam, 0 }; + WindowHost->OnWindowKeyChar(from_utf16(buf)); + } + else if (msg == WM_KEYDOWN) + { + WindowHost->OnWindowKeyDown((EInputKey)wparam); + } + else if (msg == WM_KEYUP) + { + WindowHost->OnWindowKeyUp((EInputKey)wparam); + } + else if (msg == WM_SETFOCUS) + { + if (MouseLocked) + { + ::ShowCursor(FALSE); + } + } + else if (msg == WM_KILLFOCUS) + { + if (MouseLocked) + { + ::ShowCursor(TRUE); + } + } + else if (msg == WM_CLOSE) + { + WindowHost->OnWindowClose(); + return 0; + } + else if (msg == WM_SIZE) + { + WindowHost->OnWindowGeometryChanged(); + return 0; + } + /*else if (msg == WM_NCCALCSIZE && wparam == TRUE) // calculate client area for the window + { + NCCALCSIZE_PARAMS* calcsize = (NCCALCSIZE_PARAMS*)lparam; + return WVR_REDRAW; + }*/ + + return DefWindowProc(WindowHandle, msg, wparam, lparam); +} + +void Win32Window::UpdateCursor() +{ + LPCWSTR cursor = IDC_ARROW; + switch (CurrentCursor) + { + case StandardCursor::arrow: cursor = IDC_ARROW; break; + case StandardCursor::appstarting: cursor = IDC_APPSTARTING; break; + case StandardCursor::cross: cursor = IDC_CROSS; break; + case StandardCursor::hand: cursor = IDC_HAND; break; + case StandardCursor::ibeam: cursor = IDC_IBEAM; break; + case StandardCursor::no: cursor = IDC_NO; break; + case StandardCursor::size_all: cursor = IDC_SIZEALL; break; + case StandardCursor::size_nesw: cursor = IDC_SIZENESW; break; + case StandardCursor::size_ns: cursor = IDC_SIZENS; break; + case StandardCursor::size_nwse: cursor = IDC_SIZENWSE; break; + case StandardCursor::size_we: cursor = IDC_SIZEWE; break; + case StandardCursor::uparrow: cursor = IDC_UPARROW; break; + case StandardCursor::wait: cursor = IDC_WAIT; break; + default: break; + } + + ::SetCursor((HCURSOR)LoadImage(0, cursor, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_DEFAULTSIZE, LR_SHARED)); +} + +Point Win32Window::GetLParamPos(LPARAM lparam) const +{ + double dpiscale = GetDpiScale(); + return Point(GET_X_LPARAM(lparam) / dpiscale, GET_Y_LPARAM(lparam) / dpiscale); +} + +LRESULT Win32Window::WndProc(HWND windowhandle, UINT msg, WPARAM wparam, LPARAM lparam) +{ + if (msg == WM_CREATE) + { + CREATESTRUCT* createstruct = (CREATESTRUCT*)lparam; + Win32Window* viewport = (Win32Window*)createstruct->lpCreateParams; + viewport->WindowHandle = windowhandle; + SetWindowLongPtr(windowhandle, GWLP_USERDATA, (LONG_PTR)viewport); + return viewport->OnWindowMessage(msg, wparam, lparam); + } + else + { + Win32Window* viewport = (Win32Window*)GetWindowLongPtr(windowhandle, GWLP_USERDATA); + if (viewport) + { + LRESULT result = viewport->OnWindowMessage(msg, wparam, lparam); + if (msg == WM_DESTROY) + { + SetWindowLongPtr(windowhandle, GWLP_USERDATA, 0); + viewport->WindowHandle = 0; + } + return result; + } + else + { + return DefWindowProc(windowhandle, msg, wparam, lparam); + } + } +} + +void Win32Window::ProcessEvents() +{ + while (true) + { + MSG msg = {}; + if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE) <= 0) + break; + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +void Win32Window::RunLoop() +{ + while (!ExitRunLoop && !Windows.empty()) + { + MSG msg = {}; + if (GetMessage(&msg, 0, 0, 0) <= 0) + break; + TranslateMessage(&msg); + DispatchMessage(&msg); + } + ExitRunLoop = false; +} + +void Win32Window::ExitLoop() +{ + ExitRunLoop = true; +} + +Size Win32Window::GetScreenSize() +{ + HDC screenDC = GetDC(0); + int screenWidth = GetDeviceCaps(screenDC, HORZRES); + int screenHeight = GetDeviceCaps(screenDC, VERTRES); + double dpiScale = GetDeviceCaps(screenDC, LOGPIXELSX) / 96.0; + ReleaseDC(0, screenDC); + + return Size(screenWidth / dpiScale, screenHeight / dpiScale); +} + +static void CALLBACK Win32TimerCallback(HWND handle, UINT message, UINT_PTR timerID, DWORD timestamp) +{ + auto it = Win32Window::Timers.find(timerID); + if (it != Win32Window::Timers.end()) + { + it->second(); + } +} + +void* Win32Window::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + UINT_PTR result = SetTimer(0, 0, timeoutMilliseconds, Win32TimerCallback); + if (result == 0) + throw std::runtime_error("Could not create timer"); + Timers[result] = std::move(onTimer); + return (void*)result; +} + +void Win32Window::StopTimer(void* timerID) +{ + auto it = Timers.find((UINT_PTR)timerID); + if (it != Timers.end()) + { + Timers.erase(it); + KillTimer(0, (UINT_PTR)timerID); + } +} + +std::list Win32Window::Windows; +bool Win32Window::ExitRunLoop; + +std::unordered_map> Win32Window::Timers; diff --git a/libraries/ZWidget/src/window/win32/win32window.h b/libraries/ZWidget/src/window/win32/win32window.h new file mode 100644 index 0000000000..a0f493c8b2 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32window.h @@ -0,0 +1,86 @@ +#pragma once + +#define NOMINMAX +#define WIN32_MEAN_AND_LEAN +#ifndef WINVER +#define WINVER 0x0605 +#endif +#include + +#include +#include +#include + +class Win32Window : public DisplayWindow +{ +public: + Win32Window(DisplayWindowHost* windowHost); + ~Win32Window(); + + void SetWindowTitle(const std::string& text) override; + void SetWindowFrame(const Rect& box) override; + void SetClientFrame(const Rect& box) override; + void Show() override; + void ShowFullscreen() override; + void ShowMaximized() override; + void ShowMinimized() override; + void ShowNormal() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(EInputKey key) override; + + void SetCursor(StandardCursor cursor) override; + void UpdateCursor(); + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + Point GetLParamPos(LPARAM lparam) const; + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + static Size GetScreenSize(); + + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + + static bool ExitRunLoop; + static std::list Windows; + std::list::iterator WindowsIterator; + + static std::unordered_map> Timers; + + LRESULT OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam); + static LRESULT CALLBACK WndProc(HWND windowhandle, UINT msg, WPARAM wparam, LPARAM lparam); + + DisplayWindowHost* WindowHost = nullptr; + + HWND WindowHandle = 0; + bool Fullscreen = false; + + bool MouseLocked = false; + POINT MouseLockPos = {}; + + HDC PaintDC = 0; + + StandardCursor CurrentCursor = StandardCursor::arrow; +}; diff --git a/libraries/ZWidget/src/window/window.cpp b/libraries/ZWidget/src/window/window.cpp new file mode 100644 index 0000000000..234be61e43 --- /dev/null +++ b/libraries/ZWidget/src/window/window.cpp @@ -0,0 +1,120 @@ + +#include "window/window.h" +#include + +#ifdef WIN32 + +#include "win32/win32window.h" + +std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost) +{ + return std::make_unique(windowHost); +} + +void DisplayWindow::ProcessEvents() +{ + Win32Window::ProcessEvents(); +} + +void DisplayWindow::RunLoop() +{ + Win32Window::RunLoop(); +} + +void DisplayWindow::ExitLoop() +{ + Win32Window::ExitLoop(); +} + +Size DisplayWindow::GetScreenSize() +{ + return Win32Window::GetScreenSize(); +} + +void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return Win32Window::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void DisplayWindow::StopTimer(void* timerID) +{ + Win32Window::StopTimer(timerID); +} + +#elif defined(__APPLE__) + +std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost) +{ + throw std::runtime_error("DisplayWindow::Create not implemented"); +} + +void DisplayWindow::ProcessEvents() +{ + throw std::runtime_error("DisplayWindow::ProcessEvents not implemented"); +} + +void DisplayWindow::RunLoop() +{ + throw std::runtime_error("DisplayWindow::RunLoop not implemented"); +} + +void DisplayWindow::ExitLoop() +{ + throw std::runtime_error("DisplayWindow::ExitLoop not implemented"); +} + +Size DisplayWindow::GetScreenSize() +{ + throw std::runtime_error("DisplayWindow::GetScreenSize not implemented"); +} + +void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + throw std::runtime_error("DisplayWindow::StartTimer not implemented"); +} + +void DisplayWindow::StopTimer(void* timerID) +{ + throw std::runtime_error("DisplayWindow::StopTimer not implemented"); +} + +#else + +#include "sdl2/sdl2displaywindow.h" + +std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost) +{ + return std::make_unique(windowHost); +} + +void DisplayWindow::ProcessEvents() +{ + SDL2DisplayWindow::ProcessEvents(); +} + +void DisplayWindow::RunLoop() +{ + SDL2DisplayWindow::RunLoop(); +} + +void DisplayWindow::ExitLoop() +{ + SDL2DisplayWindow::ExitLoop(); +} + +Size DisplayWindow::GetScreenSize() +{ + return SDL2DisplayWindow::GetScreenSize(); +} + +void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return SDL2DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void DisplayWindow::StopTimer(void* timerID) +{ + SDL2DisplayWindow::StopTimer(timerID); +} + +#endif diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 8c35fc8c62..32d0659ba1 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -323,6 +323,8 @@ if (HAVE_VULKAN) list( APPEND PROJECT_LIBRARIES "zvulkan" ) endif() +list( APPEND PROJECT_LIBRARIES "zwidget" ) + # ZMUSIC if( MSVC ) @@ -614,6 +616,7 @@ file( GLOB HEADER_FILES games/sw/src/*.h games/exhumed/src/*.h + launcher/*.h build/src/*.h platform/win32/*.h platform/posix/*.h @@ -1018,6 +1021,7 @@ set (PCH_SOURCES core/console/c_notifybuffer.cpp core/console/d_event.cpp + launcher/launcherwindow.cpp common/audio/sound/i_sound.cpp common/audio/sound/oalsound.cpp @@ -1042,6 +1046,9 @@ set (PCH_SOURCES common/startscreen/startscreen_strife.cpp common/startscreen/startscreen_generic.cpp common/startscreen/endoom.cpp + common/widgets/errorwindow.cpp + common/widgets/netstartwindow.cpp + common/widgets/widgetresourcedata.cpp common/fonts/singlelumpfont.cpp common/fonts/singlepicfont.cpp common/fonts/specialfont.cpp @@ -1377,7 +1384,9 @@ include_directories( common/scripting/interface common/scripting/frontend common/scripting/backend + launcher ../libraries/ZVulkan/include + ../libraries/ZWidget/include ${SYSTEM_SOURCES_DIR} ) @@ -1569,6 +1578,7 @@ source_group("Utility\\Third Party" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_ source_group("Utility\\Third Party Headers" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/include/.+") source_group("Utility\\Third Party Sources" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/src/.+") source_group("Utility\\RapidJSON" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/utility/rapidjson/.+") +source_group("Launcher" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/launcher/.+") source_group("Build Engine" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/build/.+") source_group("Build Engine\\Headers" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/build/include/.+") source_group("Build Engine\\Sources" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/build/src/.+") diff --git a/source/common/engine/st_start.h b/source/common/engine/st_start.h index 8e02987d63..a8b78aaa83 100644 --- a/source/common/engine/st_start.h +++ b/source/common/engine/st_start.h @@ -39,7 +39,7 @@ class FStartupScreen { public: - static FStartupScreen *CreateInstance(int max_progress, bool showprogress); + static FStartupScreen *CreateInstance(int max_progress); FStartupScreen(int max_progress) { @@ -66,7 +66,7 @@ class FStartupScreen class FBasicStartupScreen : public FStartupScreen { public: - FBasicStartupScreen(int max_progress, bool show_bar); + FBasicStartupScreen(int max_progress); ~FBasicStartupScreen(); void Progress(); diff --git a/source/common/platform/posix/cocoa/st_start.mm b/source/common/platform/posix/cocoa/st_start.mm index 8e4ae4bf94..ee6ea26278 100644 --- a/source/common/platform/posix/cocoa/st_start.mm +++ b/source/common/platform/posix/cocoa/st_start.mm @@ -45,7 +45,7 @@ // --------------------------------------------------------------------------- -FBasicStartupScreen::FBasicStartupScreen(int maxProgress, bool showBar) +FBasicStartupScreen::FBasicStartupScreen(int maxProgress) : FStartupScreen(maxProgress) { FConsoleWindow& consoleWindow = FConsoleWindow::GetInstance(); @@ -132,7 +132,7 @@ // --------------------------------------------------------------------------- -FStartupScreen *FStartupScreen::CreateInstance(const int maxProgress, bool showprogress) +FStartupScreen *FStartupScreen::CreateInstance(const int maxProgress) { - return new FBasicStartupScreen(maxProgress, showprogress); + return new FBasicStartupScreen(maxProgress); } diff --git a/source/common/platform/posix/sdl/i_system.cpp b/source/common/platform/posix/sdl/i_system.cpp index d1f4654675..5fd531909d 100644 --- a/source/common/platform/posix/sdl/i_system.cpp +++ b/source/common/platform/posix/sdl/i_system.cpp @@ -65,7 +65,7 @@ #include "palutil.h" #include "st_start.h" #include "printf.h" - +#include "common/widgets/launcherwindow.h" #ifndef NO_GTK bool I_GtkAvailable (); @@ -300,95 +300,16 @@ void I_PrintStr(const char *cp) int I_PickIWad (WadStuff *wads, int numwads, bool showwin, int defaultiwad, int& autoloadflags) { - int i; - if (!showwin) { return defaultiwad; } -#ifndef __APPLE__ - if(I_FileAvailable("kdialog")) - { - FString cmd("kdialog --title \"" GAMENAME " "); - cmd << GetVersionString() << ": Select an IWAD to use\"" - " --menu \"" GAMENAME " found more than one IWAD\n" - "Select from the list below to determine which one to use:\""; - - for(i = 0; i < numwads; ++i) - { - const char *filepart = strrchr(wads[i].Path.GetChars(), '/'); - if(filepart == NULL) - filepart = wads[i].Path.GetChars(); - else - filepart++; - // Menu entries are specified in "tag" "item" pairs, where when a - // particular item is selected (and the Okay button clicked), its - // corresponding tag is printed to stdout for identification. - cmd.AppendFormat(" \"%d\" \"%s (%s)\"", i, wads[i].Name.GetChars(), filepart); - } - - if(defaultiwad >= 0 && defaultiwad < numwads) - { - const char *filepart = strrchr(wads[defaultiwad].Path.GetChars(), '/'); - if(filepart == NULL) - filepart = wads[defaultiwad].Path.GetChars(); - else - filepart++; - cmd.AppendFormat(" --default \"%s (%s)\"", wads[defaultiwad].Name.GetChars(), filepart); - } - - FILE *f = popen(cmd.GetChars(), "r"); - if(f != NULL) - { - char gotstr[16]; - - if(fgets(gotstr, sizeof(gotstr), f) == NULL || - sscanf(gotstr, "%d", &i) != 1) - i = -1; - - // Exit status = 1 means the selection was canceled (either by - // Cancel/Esc or the X button), not that there was an error running - // the program. In that case, nothing was printed so fgets will - // have failed. Other values can indicate an error running the app, - // so fall back to whatever else can be used. - int status = pclose(f); - if(WIFEXITED(status) && (WEXITSTATUS(status) == 0 || WEXITSTATUS(status) == 1)) - return i; - } - } -#endif - -#ifndef NO_GTK - if (I_GtkAvailable()) - { - return I_PickIWad_Gtk (wads, numwads, showwin, defaultiwad, autoloadflags); - } -#endif - #ifdef __APPLE__ return I_PickIWad_Cocoa (wads, numwads, showwin, defaultiwad); +#else + return LauncherWindow::ExecModal(wads, numwads, defaultiwad, &autoloadflags); #endif - - if (!isatty(fileno(stdin))) - { - return defaultiwad; - } - - printf ("Please select a game wad (or 0 to exit):\n"); - for (i = 0; i < numwads; ++i) - { - const char *filepart = strrchr (wads[i].Path.GetChars(), '/'); - if (filepart == NULL) - filepart = wads[i].Path.GetChars(); - else - filepart++; - printf ("%d. %s (%s)\n", i+1, wads[i].Name.GetChars(), filepart); - } - printf ("Which one? "); - if (scanf ("%d", &i) != 1 || i > numwads) - return -1; - return i-1; } void I_PutInClipboard (const char *str) diff --git a/source/common/platform/posix/sdl/st_start.cpp b/source/common/platform/posix/sdl/st_start.cpp index c068e682a3..019a66122b 100644 --- a/source/common/platform/posix/sdl/st_start.cpp +++ b/source/common/platform/posix/sdl/st_start.cpp @@ -85,7 +85,7 @@ static const char SpinnyProgressChars[4] = { '|', '/', '-', '\\' }; // //========================================================================== -FStartupScreen *FStartupScreen::CreateInstance(int max_progress, bool) +FStartupScreen *FStartupScreen::CreateInstance(int max_progress) { return new FTTYStartupScreen(max_progress); } diff --git a/source/common/platform/posix/unix/gtk_dialogs.cpp b/source/common/platform/posix/unix/gtk_dialogs.cpp index 01fb5ee2cb..64afbe7e6d 100644 --- a/source/common/platform/posix/unix/gtk_dialogs.cpp +++ b/source/common/platform/posix/unix/gtk_dialogs.cpp @@ -485,120 +485,6 @@ class ZUIButton : public ZUIWidget } }; -static int PickIWad (WadStuff *wads, int numwads, bool showwin, int defaultiwad, int& autoloadflags) -{ - char caption[100]; - mysnprintf(caption, countof(caption), GAMENAME " %s: Select an IWAD to use", GetVersionString()); - - ZUIWindow window(caption); - - ZUIVBox vbox; - - ZUILabel label(GAMENAME " found more than one IWAD\nSelect from the list below to determine which one to use:"); - ZUIListView listview(wads, numwads, defaultiwad); - ZUIScrolledWindow scroll((ZUIWidget*)&listview); - - ZUIHBox hboxOptions; - - ZUIVBox vboxVideo; - ZUILabel videoSettings("Video settings"); - ZUIRadioButton opengl("OpenGL"); - ZUIRadioButton vulkan(&opengl, "Vulkan"); - ZUIRadioButton openglES(&opengl, "OpenGL ES"); - ZUICheckButton fullscreen("Full screen"); - - ZUIVBox vboxMisc; - ZUICheckButton noautoload("Disable autoload"); - ZUICheckButton dontAskAgain("Don't ask me this again"); - - ZUIVBox vboxExtra; - ZUILabel extraGraphics("Extra graphics"); - ZUICheckButton lights("Lights"); - ZUICheckButton brightmaps("Brightmaps"); - ZUICheckButton widescreen("Widescreen"); - - ZUIHBox hboxButtons; - - ZUIButtonBox bbox; - ZUIButton playButton("Play Game!", true); - ZUIButton exitButton("Exit", false); - - window.AddWidget(&vbox); - vbox.PackStart(&label, false, false, 0); - vbox.PackStart(&scroll, true, true, 0); - vbox.PackEnd(&hboxButtons, false, false, 0); - vbox.PackEnd(&hboxOptions, false, false, 0); - hboxOptions.PackStart(&vboxVideo, false, false, 15); - hboxOptions.PackStart(&vboxMisc, true, false, 15); - hboxOptions.PackStart(&vboxExtra, false, false, 15); - vboxVideo.PackStart(&videoSettings, false, false, 0); - vboxVideo.PackStart(&opengl, false, false, 0); - vboxVideo.PackStart(&vulkan, false, false, 0); - vboxVideo.PackStart(&openglES, false, false, 0); - vboxVideo.PackStart(&fullscreen, false, false, 15); - vboxMisc.PackStart(&noautoload, false, false, 0); - vboxMisc.PackStart(&dontAskAgain, false, false, 0); - vboxExtra.PackStart(&extraGraphics, false, false, 0); - vboxExtra.PackStart(&lights, false, false, 0); - vboxExtra.PackStart(&brightmaps, false, false, 0); - vboxExtra.PackStart(&widescreen, false, false, 0); - hboxButtons.PackStart(&bbox, true, true, 0); - bbox.PackStart(&playButton, false, false, 0); - bbox.PackEnd(&exitButton, false, false, 0); - - dontAskAgain.SetChecked(!showwin); - - switch (vid_preferbackend) - { - case 0: opengl.SetChecked(true); break; - case 1: vulkan.SetChecked(true); break; - case 2: openglES.SetChecked(true); break; - default: break; - } - - if (vid_fullscreen) fullscreen.SetChecked(true); - - if (autoloadflags & 1) noautoload.SetChecked(true); - if (autoloadflags & 2) lights.SetChecked(true); - if (autoloadflags & 4) brightmaps.SetChecked(true); - if (autoloadflags & 8) widescreen.SetChecked(true); - - int close_style = 0; - listview.ConnectButtonPress(&close_style); - listview.ConnectKeyPress(&window); - playButton.ConnectClickedOK(&close_style); - exitButton.ConnectClickedExit(&window); - - playButton.GrabDefault(); - - window.RunModal(); - - if (close_style == 1) - { - int i = listview.GetSelectedIndex(); - - // Set state of queryiwad based on the checkbox. - queryiwad = !dontAskAgain.GetChecked(); - - if (opengl.GetChecked()) vid_preferbackend = 0; - if (vulkan.GetChecked()) vid_preferbackend = 1; - if (openglES.GetChecked()) vid_preferbackend = 2; - - vid_fullscreen = fullscreen.GetChecked(); - - autoloadflags = 0; - if (noautoload.GetChecked()) autoloadflags |= 1; - if (lights.GetChecked()) autoloadflags |= 2; - if (brightmaps.GetChecked()) autoloadflags |= 4; - if (widescreen.GetChecked()) autoloadflags |= 8; - - return i; - } - else - { - return -1; - } -} static void ShowError(const char* errortext) { @@ -621,11 +507,6 @@ static void ShowError(const char* errortext) } // namespace Gtk -int I_PickIWad_Gtk (WadStuff *wads, int numwads, bool showwin, int defaultiwad, int& autoloadflags) -{ - return Gtk::PickIWad (wads, numwads, showwin, defaultiwad, autoloadflags); -} - void I_ShowFatalError_Gtk(const char* errortext) { Gtk::ShowError(errortext); } diff --git a/source/common/platform/win32/i_main.cpp b/source/common/platform/win32/i_main.cpp index c28a3ff4bd..d1f82e59ae 100644 --- a/source/common/platform/win32/i_main.cpp +++ b/source/common/platform/win32/i_main.cpp @@ -132,8 +132,6 @@ bool FancyStdOut, AttachedStdOut; void I_SetIWADInfo() { - // Make the startup banner show itself - mainwindow.UpdateLayout(); } //========================================================================== @@ -266,7 +264,14 @@ int DoMain (HINSTANCE hInstance) atexit ([](){ CoUninitialize(); }); // beware of calling convention. int ret = GameMain (); - mainwindow.CheckForRestart(); + + if (mainwindow.CheckForRestart()) + { + HMODULE hModule = GetModuleHandleW(NULL); + WCHAR path[MAX_PATH]; + GetModuleFileNameW(hModule, path, MAX_PATH); + ShellExecuteW(NULL, L"open", path, GetCommandLineW(), NULL, SW_SHOWNORMAL); + } DestroyCustomCursor(); if (ret == 1337) // special exit code for 'norun'. diff --git a/source/common/platform/win32/i_mainwindow.cpp b/source/common/platform/win32/i_mainwindow.cpp index 64ddaa5da4..fe0ad730a8 100644 --- a/source/common/platform/win32/i_mainwindow.cpp +++ b/source/common/platform/win32/i_mainwindow.cpp @@ -11,17 +11,14 @@ #include "v_font.h" #include "i_net.h" #include "engineerrors.h" +#include "common/widgets/errorwindow.h" +#include "common/widgets/netstartwindow.h" #include #include #include +#include -#ifdef _M_X64 -#define X64 " 64-bit" -#elif _M_ARM64 -#define X64 " ARM-64" -#else -#define X64 "" -#endif +#pragma comment(lib, "dwmapi.lib") MainWindow mainwindow; @@ -55,7 +52,7 @@ void MainWindow::Create(const FString& caption, int x, int y, int width, int hei WS_EX_APPWINDOW, WinClassName, wcaption.c_str(), - WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN, + WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, x, y, width, height, (HWND)NULL, (HMENU)NULL, @@ -67,29 +64,15 @@ void MainWindow::Create(const FString& caption, int x, int y, int width, int hei MessageBoxA(nullptr, "Unable to create main window", "Fatal", MB_ICONEXCLAMATION | MB_OK); exit(-1); } -} -void MainWindow::HideGameTitleWindow() -{ - if (GameTitleWindow != 0) - { - DestroyWindow(GameTitleWindow); - GameTitleWindow = 0; - } -} + uint32_t bordercolor = RGB(51, 51, 51); + uint32_t captioncolor = RGB(33, 33, 33); + uint32_t textcolor = RGB(226, 223, 219); -int MainWindow::GetGameTitleWindowHeight() -{ - if (GameTitleWindow != 0) - { - RECT rect; - GetClientRect(GameTitleWindow, &rect); - return rect.bottom; - } - else - { - return 0; - } + // Don't error check these as they only exist on Windows 11, and if they fail then that is OK. + DwmSetWindowAttribute(Window, 34/*DWMWA_BORDER_COLOR*/, &bordercolor, sizeof(uint32_t)); + DwmSetWindowAttribute(Window, 35/*DWMWA_CAPTION_COLOR*/, &captioncolor, sizeof(uint32_t)); + DwmSetWindowAttribute(Window, 36/*DWMWA_TEXT_COLOR*/, &textcolor, sizeof(uint32_t)); } // Sets the main WndProc, hides all the child windows, and starts up in-game input. @@ -99,10 +82,6 @@ void MainWindow::ShowGameView() { SetWindowLongPtr(Window, GWLP_USERDATA, 1); SetWindowLongPtr(Window, GWLP_WNDPROC, (LONG_PTR)WndProc); - ShowWindow(ConWindow, SW_HIDE); - ShowWindow(ProgressBar, SW_HIDE); - ConWindowHidden = true; - ShowWindow(GameTitleWindow, SW_HIDE); I_InitInput(Window); } } @@ -110,766 +89,88 @@ void MainWindow::ShowGameView() // Returns the main window to its startup state. void MainWindow::RestoreConView() { - HDC screenDC = GetDC(0); - int dpi = GetDeviceCaps(screenDC, LOGPIXELSX); - ReleaseDC(0, screenDC); - int width = (512 * dpi + 96 / 2) / 96; - int height = (384 * dpi + 96 / 2) / 96; - - // Make sure the window has a frame in case it was fullscreened. - SetWindowLongPtr(Window, GWL_STYLE, WS_VISIBLE | WS_OVERLAPPEDWINDOW); - if (GetWindowLong(Window, GWL_EXSTYLE) & WS_EX_TOPMOST) - { - SetWindowPos(Window, HWND_BOTTOM, 0, 0, width, height, SWP_DRAWFRAME | SWP_NOCOPYBITS | SWP_NOMOVE); - SetWindowPos(Window, HWND_TOP, 0, 0, 0, 0, SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOSIZE); - } - else - { - SetWindowPos(Window, NULL, 0, 0, width, height, SWP_DRAWFRAME | SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOZORDER); - } - - SetWindowLongPtr(Window, GWLP_WNDPROC, (LONG_PTR)LConProc); - ShowWindow(ConWindow, SW_SHOW); - ConWindowHidden = false; - ShowWindow(GameTitleWindow, SW_SHOW); I_ShutdownInput(); // Make sure the mouse pointer is available. + ShowWindow(Window, SW_HIDE); + // Make sure the progress bar isn't visible. DeleteStartupScreen(); - - FlushBufferedConsoleStuff(); } // Shows an error message, preferably in the main window, but it can use a normal message box too. void MainWindow::ShowErrorPane(const char* text) { - auto widetext = WideString(text); - if (Window == nullptr || ConWindow == nullptr) - { - if (text != NULL) - { - FStringf caption("Fatal Error - " GAMENAME " %s " X64 " (%s)", GetVersionString(), GetGitTime()); - MessageBoxW(Window, widetext.c_str(),caption.WideString().c_str(), MB_OK | MB_ICONSTOP | MB_TASKMODAL); - } - return; - } - - if (StartWindow != NULL) // Ensure that the network pane is hidden. + if (StartWindow) // Ensure that the network pane is hidden. { I_NetDone(); } - if (text != NULL) - { - FStringf caption("Fatal Error - " GAMENAME " %s " X64 " (%s)", GetVersionString(), GetGitTime()); - auto wcaption = caption.WideString(); - SetWindowTextW(Window, wcaption.c_str()); - ErrorIcon = CreateWindowExW(WS_EX_NOPARENTNOTIFY, L"STATIC", NULL, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | SS_OWNERDRAW, 0, 0, 0, 0, Window, NULL, GetModuleHandle(0), NULL); - if (ErrorIcon != NULL) - { - SetWindowLong(ErrorIcon, GWL_ID, IDC_ICONPIC); - } - } - ErrorPane = CreateDialogParam(GetModuleHandle(0), MAKEINTRESOURCE(IDD_ERRORPANE), Window, ErrorPaneProc, (LONG_PTR)NULL); - - if (text != NULL) - { - CHARRANGE end; - CHARFORMAT2 oldformat, newformat; - PARAFORMAT2 paraformat; - - // Append the error message to the log. - end.cpMax = end.cpMin = GetWindowTextLength(ConWindow); - SendMessage(ConWindow, EM_EXSETSEL, 0, (LPARAM)&end); - - // Remember current charformat. - oldformat.cbSize = sizeof(oldformat); - SendMessage(ConWindow, EM_GETCHARFORMAT, SCF_SELECTION, (LPARAM)&oldformat); - - // Use bigger font and standout colors. - newformat.cbSize = sizeof(newformat); - newformat.dwMask = CFM_BOLD | CFM_COLOR | CFM_SIZE; - newformat.dwEffects = CFE_BOLD; - newformat.yHeight = oldformat.yHeight * 5 / 4; - newformat.crTextColor = RGB(255, 170, 170); - SendMessage(ConWindow, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&newformat); - - // Indent the rest of the text to make the error message stand out a little more. - paraformat.cbSize = sizeof(paraformat); - paraformat.dwMask = PFM_STARTINDENT | PFM_OFFSETINDENT | PFM_RIGHTINDENT; - paraformat.dxStartIndent = paraformat.dxOffset = paraformat.dxRightIndent = 120; - SendMessage(ConWindow, EM_SETPARAFORMAT, 0, (LPARAM)¶format); - SendMessageW(ConWindow, EM_REPLACESEL, FALSE, (LPARAM)L"\n"); - - // Find out where the error lines start for the error icon display control. - SendMessage(ConWindow, EM_EXGETSEL, 0, (LPARAM)&end); - ErrorIconChar = end.cpMax; - - // Now start adding the actual error message. - SendMessageW(ConWindow, EM_REPLACESEL, FALSE, (LPARAM)L"Execution could not continue.\n\n"); - - // Restore old charformat but with light yellow text. - oldformat.crTextColor = RGB(255, 255, 170); - SendMessage(ConWindow, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&oldformat); - - // Add the error text. - SendMessageW(ConWindow, EM_REPLACESEL, FALSE, (LPARAM)widetext.c_str()); - - // Make sure the error text is not scrolled below the window. - SendMessage(ConWindow, EM_LINESCROLL, 0, SendMessage(ConWindow, EM_GETLINECOUNT, 0, 0)); - // The above line scrolled everything off the screen, so pretend to move the scroll - // bar thumb, which clamps to not show any extra lines if it doesn't need to. - SendMessage(ConWindow, EM_SCROLL, SB_PAGEDOWN, 0); - } - - BOOL bRet; - MSG msg; - - while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0) - { - if (bRet == -1) - { - MessageBoxW(Window, widetext.c_str(), WGAMENAME " Fatal Error", MB_OK | MB_ICONSTOP | MB_TASKMODAL); - return; - } - else if (!IsDialogMessage(ErrorPane, &msg)) - { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - } -} -void MainWindow::ShowProgressBar(int maxpos) -{ - if (ProgressBar == 0) - ProgressBar = CreateWindowEx(0, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS, 0, 0, 0, 0, Window, 0, GetModuleHandle(0), 0); - SendMessage(ProgressBar, PBM_SETRANGE, 0, MAKELPARAM(0, maxpos)); - LayoutMainWindow(Window, 0); -} + // PrintStr(text); -void MainWindow::HideProgressBar() -{ - if (ProgressBar != 0) - { - DestroyWindow(ProgressBar); - ProgressBar = 0; - LayoutMainWindow(Window, 0); - } -} + size_t totalsize = 0; + for (const FString& line : bufferedConsoleStuff) + totalsize += line.Len(); -void MainWindow::SetProgressPos(int pos) -{ - if (ProgressBar != 0) - SendMessage(ProgressBar, PBM_SETPOS, pos, 0); -} + std::string alltext; + alltext.reserve(totalsize); + for (const FString& line : bufferedConsoleStuff) + alltext.append(line.GetChars(), line.Len()); -// DialogProc for the network startup pane. It just waits for somebody to click a button, and the only button available is the abort one. -static INT_PTR CALLBACK NetStartPaneProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam) -{ - if (msg == WM_COMMAND && HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDCANCEL) - { - PostMessage(hDlg, WM_COMMAND, 1337, 1337); - return TRUE; - } - return FALSE; + restartrequest = ErrorWindow::ExecModal(text, alltext); } void MainWindow::ShowNetStartPane(const char* message, int maxpos) { - // Create the dialog child window - if (NetStartPane == NULL) - { - NetStartPane = CreateDialogParam(GetModuleHandle(0), MAKEINTRESOURCE(IDD_NETSTARTPANE), Window, NetStartPaneProc, 0); - if (ProgressBar != 0) - { - DestroyWindow(ProgressBar); - ProgressBar = 0; - } - RECT winrect; - GetWindowRect(Window, &winrect); - SetWindowPos(Window, NULL, 0, 0, - winrect.right - winrect.left, winrect.bottom - winrect.top + LayoutNetStartPane(NetStartPane, 0), - SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER); - LayoutMainWindow(Window, NULL); - SetFocus(NetStartPane); - } - - // Set the message text - std::wstring wmessage = WideString(message); - SetDlgItemTextW(NetStartPane, IDC_NETSTARTMESSAGE, wmessage.c_str()); - - // Set the progress bar range - NetStartMaxPos = maxpos; - HWND ctl = GetDlgItem(NetStartPane, IDC_NETSTARTPROGRESS); - if (maxpos == 0) - { - SendMessage(ctl, PBM_SETMARQUEE, TRUE, 100); - SetWindowLong(ctl, GWL_STYLE, GetWindowLong(ctl, GWL_STYLE) | PBS_MARQUEE); - SetDlgItemTextW(NetStartPane, IDC_NETSTARTCOUNT, L""); - } - else - { - SendMessage(ctl, PBM_SETMARQUEE, FALSE, 0); - SetWindowLong(ctl, GWL_STYLE, GetWindowLong(ctl, GWL_STYLE) & (~PBS_MARQUEE)); - - SendMessage(ctl, PBM_SETRANGE, 0, MAKELPARAM(0, maxpos)); - if (maxpos == 1) - { - SendMessage(ctl, PBM_SETPOS, 1, 0); - SetDlgItemTextW(NetStartPane, IDC_NETSTARTCOUNT, L""); - } - } + NetStartWindow::ShowNetStartPane(message, maxpos); } void MainWindow::HideNetStartPane() { - if (NetStartPane != 0) - { - DestroyWindow(NetStartPane); - NetStartPane = 0; - LayoutMainWindow(Window, 0); - } + NetStartWindow::HideNetStartPane(); } void MainWindow::SetNetStartProgress(int pos) { - if (NetStartPane != 0 && NetStartMaxPos > 1) - { - char buf[16]; - mysnprintf(buf, sizeof(buf), "%d/%d", pos, NetStartMaxPos); - SetDlgItemTextA(NetStartPane, IDC_NETSTARTCOUNT, buf); - SendDlgItemMessage(NetStartPane, IDC_NETSTARTPROGRESS, PBM_SETPOS, min(pos, NetStartMaxPos), 0); - } + NetStartWindow::SetNetStartProgress(pos); } bool MainWindow::RunMessageLoop(bool (*timer_callback)(void*), void* userdata) { - BOOL bRet; - MSG msg; - - if (SetTimer(Window, 1337, 500, NULL) == 0) - { - I_FatalError("Could not set network synchronization timer."); - } - - while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0) - { - if (bRet == -1) - { - KillTimer(Window, 1337); - return false; - } - else - { - // This must be outside the window function so that the exception does not cross DLL boundaries. - if (msg.message == WM_COMMAND && msg.wParam == 1337 && msg.lParam == 1337) - { - throw CExitEvent(0); - } - if (msg.message == WM_TIMER && msg.hwnd == Window && msg.wParam == 1337) - { - if (timer_callback(userdata)) - { - KillTimer(Window, 1337); - return true; - } - } - if (!IsDialogMessage(NetStartPane, &msg)) - { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - } - } - KillTimer(Window, 1337); - return false; -} - -void MainWindow::UpdateLayout() -{ - LayoutMainWindow(Window, 0); -} - -// Lays out the main window with the game title and log controls and possibly the error pane and progress bar. -void MainWindow::LayoutMainWindow(HWND hWnd, HWND pane) -{ - RECT rect; - int errorpaneheight = 0; - int bannerheight = 0; - int progressheight = 0; - int netpaneheight = 0; - int leftside = 0; - int w, h; - - GetClientRect(hWnd, &rect); - w = rect.right; - h = rect.bottom; - - if (GameStartupInfo.Name.IsNotEmpty() && GameTitleWindow != NULL) - { - bannerheight = GameTitleFontHeight + 5; - MoveWindow(GameTitleWindow, 0, 0, w, bannerheight, TRUE); - InvalidateRect(GameTitleWindow, NULL, FALSE); - } - if (ProgressBar != NULL) - { - // Base the height of the progress bar on the height of a scroll bar - // arrow, just as in the progress bar example. - progressheight = GetSystemMetrics(SM_CYVSCROLL); - MoveWindow(ProgressBar, 0, h - progressheight, w, progressheight, TRUE); - } - if (NetStartPane != NULL) - { - netpaneheight = LayoutNetStartPane(NetStartPane, w); - SetWindowPos(NetStartPane, HWND_TOP, 0, h - progressheight - netpaneheight, w, netpaneheight, SWP_SHOWWINDOW); - } - if (pane != NULL) - { - errorpaneheight = LayoutErrorPane(pane, w); - SetWindowPos(pane, HWND_TOP, 0, h - progressheight - netpaneheight - errorpaneheight, w, errorpaneheight, 0); - } - if (ErrorIcon != NULL) - { - leftside = GetSystemMetrics(SM_CXICON) + 6; - MoveWindow(ErrorIcon, 0, bannerheight, leftside, h - bannerheight - errorpaneheight - progressheight - netpaneheight, TRUE); - } - // The log window uses whatever space is left. - MoveWindow(ConWindow, leftside, bannerheight, w - leftside, h - bannerheight - errorpaneheight - progressheight - netpaneheight, TRUE); -} - -// Lays out the error pane to the desired width, returning the required height. -int MainWindow::LayoutErrorPane(HWND pane, int w) -{ - HWND ctl, ctl_two; - RECT rectc, rectc_two; - - // Right-align the Quit button. - ctl = GetDlgItem(pane, IDOK); - GetClientRect(ctl, &rectc); // Find out how big it is. - MoveWindow(ctl, w - rectc.right - 1, 1, rectc.right, rectc.bottom, TRUE); - - // Second-right-align the Restart button - ctl_two = GetDlgItem(pane, IDC_BUTTON1); - GetClientRect(ctl_two, &rectc_two); // Find out how big it is. - MoveWindow(ctl_two, w - rectc.right - rectc_two.right - 2, 1, rectc.right, rectc.bottom, TRUE); - - InvalidateRect(ctl, NULL, TRUE); - InvalidateRect(ctl_two, NULL, TRUE); - - // Return the needed height for this layout - return rectc.bottom + 2; -} - -// Lays out the network startup pane to the specified width, returning its required height. -int MainWindow::LayoutNetStartPane(HWND pane, int w) -{ - HWND ctl; - RECT margin, rectc; - int staticheight, barheight; - - // Determine margin sizes. - SetRect(&margin, 7, 7, 0, 0); - MapDialogRect(pane, &margin); - - // Stick the message text in the upper left corner. - ctl = GetDlgItem(pane, IDC_NETSTARTMESSAGE); - GetClientRect(ctl, &rectc); - MoveWindow(ctl, margin.left, margin.top, rectc.right, rectc.bottom, TRUE); - - // Stick the count text in the upper right corner. - ctl = GetDlgItem(pane, IDC_NETSTARTCOUNT); - GetClientRect(ctl, &rectc); - MoveWindow(ctl, w - rectc.right - margin.left, margin.top, rectc.right, rectc.bottom, TRUE); - staticheight = rectc.bottom; - - // Stretch the progress bar to fill the entire width. - ctl = GetDlgItem(pane, IDC_NETSTARTPROGRESS); - barheight = GetSystemMetrics(SM_CYVSCROLL); - MoveWindow(ctl, margin.left, margin.top * 2 + staticheight, w - margin.left * 2, barheight, TRUE); - - // Center the abort button underneath the progress bar. - ctl = GetDlgItem(pane, IDCANCEL); - GetClientRect(ctl, &rectc); - MoveWindow(ctl, (w - rectc.right) / 2, margin.top * 3 + staticheight + barheight, rectc.right, rectc.bottom, TRUE); - - return margin.top * 4 + staticheight + barheight + rectc.bottom; + return NetStartWindow::RunMessageLoop(timer_callback, userdata); } -void MainWindow::CheckForRestart() +bool MainWindow::CheckForRestart() { - if (restartrequest) - { - HMODULE hModule = GetModuleHandleW(NULL); - WCHAR path[MAX_PATH]; - GetModuleFileNameW(hModule, path, MAX_PATH); - ShellExecuteW(NULL, L"open", path, GetCommandLineW(), NULL, SW_SHOWNORMAL); - } + bool result = restartrequest; restartrequest = false; + return result; } // The main window's WndProc during startup. During gameplay, the WndProc in i_input.cpp is used instead. LRESULT MainWindow::LConProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { - return mainwindow.OnMessage(hWnd, msg, wParam, lParam); -} - -INT_PTR MainWindow::ErrorPaneProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam) -{ - switch (msg) - { - case WM_INITDIALOG: - // Appear in the main window. - mainwindow.LayoutMainWindow(GetParent(hDlg), hDlg); - return TRUE; - - case WM_COMMAND: - if (HIWORD(wParam) == BN_CLICKED) - { - if (LOWORD(wParam) == IDC_BUTTON1) // we pressed the restart button, so run GZDoom again - { - mainwindow.restartrequest = true; - } - PostQuitMessage(0); - return TRUE; - } - break; - } - return FALSE; -} - -LRESULT MainWindow::OnMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - switch (msg) - { - case WM_CREATE: return OnCreate(hWnd, msg, wParam, lParam); - case WM_SIZE: return OnSize(hWnd, msg, wParam, lParam); - case WM_DRAWITEM: return OnDrawItem(hWnd, msg, wParam, lParam); - case WM_COMMAND: return OnCommand(hWnd, msg, wParam, lParam); - case WM_CLOSE: return OnClose(hWnd, msg, wParam, lParam); - case WM_DESTROY: return OnDestroy(hWnd, msg, wParam, lParam); - default: return DefWindowProc(hWnd, msg, wParam, lParam); - } -} - -LRESULT MainWindow::OnCreate(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - HWND view; - HDC hdc; - HGDIOBJ oldfont; - LOGFONT lf; - TEXTMETRIC tm; - CHARFORMAT2W format; - - HINSTANCE inst = (HINSTANCE)(LONG_PTR)GetWindowLongPtr(hWnd, GWLP_HINSTANCE); - - // Create game title static control - memset(&lf, 0, sizeof(lf)); - hdc = GetDC(hWnd); - lf.lfHeight = -MulDiv(12, GetDeviceCaps(hdc, LOGPIXELSY), 72); - lf.lfCharSet = ANSI_CHARSET; - lf.lfWeight = FW_BOLD; - lf.lfPitchAndFamily = VARIABLE_PITCH | FF_ROMAN; - wcscpy(lf.lfFaceName, L"Trebuchet MS"); - GameTitleFont = CreateFontIndirect(&lf); - - oldfont = SelectObject(hdc, GetStockObject(DEFAULT_GUI_FONT)); - GetTextMetrics(hdc, &tm); - DefaultGUIFontHeight = tm.tmHeight; - if (GameTitleFont == NULL) - { - GameTitleFontHeight = DefaultGUIFontHeight; - } - else - { - SelectObject(hdc, GameTitleFont); - GetTextMetrics(hdc, &tm); - GameTitleFontHeight = tm.tmHeight; - } - SelectObject(hdc, oldfont); - - // Create log read-only edit control - view = CreateWindowExW(WS_EX_NOPARENTNOTIFY, L"RichEdit20W", nullptr, - WS_CHILD | WS_VISIBLE | WS_VSCROLL | - ES_LEFT | ES_MULTILINE | WS_CLIPSIBLINGS, - 0, 0, 0, 0, - hWnd, NULL, inst, NULL); - HRESULT hr; - hr = GetLastError(); - if (view == NULL) - { - ReleaseDC(hWnd, hdc); - return -1; - } - SendMessage(view, EM_SETREADONLY, TRUE, 0); - SendMessage(view, EM_EXLIMITTEXT, 0, 0x7FFFFFFE); - SendMessage(view, EM_SETBKGNDCOLOR, 0, RGB(70, 70, 70)); - // Setup default font for the log. - //SendMessage (view, WM_SETFONT, (WPARAM)GetStockObject (DEFAULT_GUI_FONT), FALSE); - format.cbSize = sizeof(format); - format.dwMask = CFM_BOLD | CFM_COLOR | CFM_FACE | CFM_SIZE | CFM_CHARSET; - format.dwEffects = 0; - format.yHeight = 200; - format.crTextColor = RGB(223, 223, 223); - format.bCharSet = ANSI_CHARSET; - format.bPitchAndFamily = FF_SWISS | VARIABLE_PITCH; - wcscpy(format.szFaceName, L"DejaVu Sans"); // At least I have it. :p - SendMessageW(view, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&format); - - ConWindow = view; - ReleaseDC(hWnd, hdc); - - view = CreateWindowExW(WS_EX_NOPARENTNOTIFY, L"STATIC", NULL, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | SS_OWNERDRAW, 0, 0, 0, 0, hWnd, nullptr, inst, nullptr); - if (view == nullptr) - { - return -1; - } - SetWindowLong(view, GWL_ID, IDC_STATIC_TITLE); - GameTitleWindow = view; - - return 0; -} - -LRESULT MainWindow::OnSize(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - if (wParam != SIZE_MAXHIDE && wParam != SIZE_MAXSHOW) - { - LayoutMainWindow(hWnd, ErrorPane); - } - return 0; -} - -LRESULT MainWindow::OnDrawItem(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - HGDIOBJ oldfont; - HBRUSH hbr; - DRAWITEMSTRUCT* drawitem; - RECT rect; - SIZE size; - - // Draw title banner. - if (wParam == IDC_STATIC_TITLE && GameStartupInfo.Name.IsNotEmpty()) - { - const PalEntry* c; - - // Draw the game title strip at the top of the window. - drawitem = (LPDRAWITEMSTRUCT)lParam; - - // Draw the background. - rect = drawitem->rcItem; - rect.bottom -= 1; - c = (const PalEntry*)&GameStartupInfo.BkColor; - hbr = CreateSolidBrush(RGB(c->r, c->g, c->b)); - FillRect(drawitem->hDC, &drawitem->rcItem, hbr); - DeleteObject(hbr); - - // Calculate width of the title string. - SetTextAlign(drawitem->hDC, TA_TOP); - oldfont = SelectObject(drawitem->hDC, GameTitleFont != NULL ? GameTitleFont : (HFONT)GetStockObject(DEFAULT_GUI_FONT)); - auto widename = GameStartupInfo.Name.WideString(); - GetTextExtentPoint32W(drawitem->hDC, widename.c_str(), (int)widename.length(), &size); - - // Draw the title. - c = (const PalEntry*)&GameStartupInfo.FgColor; - SetTextColor(drawitem->hDC, RGB(c->r, c->g, c->b)); - SetBkMode(drawitem->hDC, TRANSPARENT); - TextOutW(drawitem->hDC, rect.left + (rect.right - rect.left - size.cx) / 2, 2, widename.c_str(), (int)widename.length()); - SelectObject(drawitem->hDC, oldfont); - return TRUE; - } - // Draw stop icon. - else if (wParam == IDC_ICONPIC) - { - HICON icon; - POINTL char_pos; - drawitem = (LPDRAWITEMSTRUCT)lParam; - - // This background color should match the edit control's. - hbr = CreateSolidBrush(RGB(70, 70, 70)); - FillRect(drawitem->hDC, &drawitem->rcItem, hbr); - DeleteObject(hbr); - - // Draw the icon aligned with the first line of error text. - SendMessage(ConWindow, EM_POSFROMCHAR, (WPARAM)&char_pos, ErrorIconChar); - icon = (HICON)LoadImage(0, IDI_ERROR, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED); - DrawIcon(drawitem->hDC, 6, char_pos.y, icon); - return TRUE; - } - return FALSE; -} - -LRESULT MainWindow::OnCommand(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - if (ErrorIcon != NULL && (HWND)lParam == ConWindow && HIWORD(wParam) == EN_UPDATE) - { - // Be sure to redraw the error icon if the edit control changes. - InvalidateRect(ErrorIcon, NULL, TRUE); - return 0; - } - return DefWindowProc(hWnd, msg, wParam, lParam); -} - -LRESULT MainWindow::OnClose(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - PostQuitMessage(0); - return DefWindowProc(hWnd, msg, wParam, lParam); -} - -LRESULT MainWindow::OnDestroy(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - if (GameTitleFont != NULL) - { - DeleteObject(GameTitleFont); - } return DefWindowProc(hWnd, msg, wParam, lParam); } void MainWindow::PrintStr(const char* cp) { - if (ConWindowHidden) - { - bufferedConsoleStuff.Push(cp); - } - else - { - DoPrintStr(cp); - } -} - -void MainWindow::FlushBufferedConsoleStuff() -{ - for (unsigned i = 0; i < bufferedConsoleStuff.Size(); i++) - { - DoPrintStr(bufferedConsoleStuff[i].GetChars()); - } - bufferedConsoleStuff.Clear(); -} - -void MainWindow::DoPrintStr(const char* cpt) -{ - wchar_t wbuf[256]; - int bpos = 0; - CHARRANGE selection = {}; - CHARRANGE endselection = {}; - LONG lines_before = 0, lines_after = 0; - - // Store the current selection and set it to the end so we can append text. - SendMessage(ConWindow, EM_EXGETSEL, 0, (LPARAM)&selection); - endselection.cpMax = endselection.cpMin = GetWindowTextLength(ConWindow); - SendMessage(ConWindow, EM_EXSETSEL, 0, (LPARAM)&endselection); - - // GetWindowTextLength and EM_EXSETSEL can disagree on where the end of - // the text is. Find out what EM_EXSETSEL thought it was and use that later. - SendMessage(ConWindow, EM_EXGETSEL, 0, (LPARAM)&endselection); - - // Remember how many lines there were before we added text. - lines_before = (LONG)SendMessage(ConWindow, EM_GETLINECOUNT, 0, 0); - - const uint8_t* cptr = (const uint8_t*)cpt; - - auto outputIt = [&]() - { - wbuf[bpos] = 0; - SendMessageW(ConWindow, EM_REPLACESEL, FALSE, (LPARAM)wbuf); - bpos = 0; - }; - - while (int chr = GetCharFromString(cptr)) - { - if ((chr == TEXTCOLOR_ESCAPE && bpos != 0) || bpos == 255) - { - outputIt(); - } - if (chr != TEXTCOLOR_ESCAPE) - { - if (chr >= 0x1D && chr <= 0x1F) - { // The bar characters, most commonly used to indicate map changes - chr = 0x2550; // Box Drawings Double Horizontal - } - wbuf[bpos++] = chr; - } - else - { - EColorRange range = V_ParseFontColor(cptr, CR_UNTRANSLATED, CR_YELLOW); - - if (range != CR_UNDEFINED) - { - // Change the color of future text added to the control. - PalEntry color = V_LogColorFromColorRange(range); - - // Change the color. - CHARFORMAT format; - format.cbSize = sizeof(format); - format.dwMask = CFM_COLOR; - format.dwEffects = 0; - format.crTextColor = RGB(color.r, color.g, color.b); - SendMessage(ConWindow, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&format); - } - } - } - if (bpos != 0) - { - outputIt(); - } - - // If the old selection was at the end of the text, keep it at the end and - // scroll. Don't scroll if the selection is anywhere else. - if (selection.cpMin == endselection.cpMin && selection.cpMax == endselection.cpMax) - { - selection.cpMax = selection.cpMin = GetWindowTextLength(ConWindow); - lines_after = (LONG)SendMessage(ConWindow, EM_GETLINECOUNT, 0, 0); - if (lines_after > lines_before) - { - SendMessage(ConWindow, EM_LINESCROLL, 0, lines_after - lines_before); - } - } - // Restore the previous selection. - SendMessage(ConWindow, EM_EXSETSEL, 0, (LPARAM)&selection); - // Give the edit control a chance to redraw itself. - I_GetEvent(); + bufferedConsoleStuff.Push(cp); } -static DWORD CALLBACK WriteLogFileStreamer(DWORD_PTR cookie, LPBYTE buffer, LONG cb, LONG* pcb) +void MainWindow::GetLog(std::function writeData) { - uint32_t didwrite; - LONG p, pp; - - // Replace gray foreground color with black. - static const char* badfg = "\\red223\\green223\\blue223;"; - // 4321098 765432109 876543210 - // 2 1 0 - for (p = pp = 0; p < cb; ++p) + for (const FString& line : bufferedConsoleStuff) { - if (buffer[p] == badfg[pp]) - { - ++pp; - if (pp == 25) - { - buffer[p - 1] = buffer[p - 2] = buffer[p - 3] = - buffer[p - 9] = buffer[p - 10] = buffer[p - 11] = - buffer[p - 18] = buffer[p - 19] = buffer[p - 20] = '0'; - break; - } - } - else + size_t pos = 0; + size_t len = line.Len(); + while (pos < len) { - pp = 0; + uint32_t size = (uint32_t)std::min(len - pos, 0x0fffffffULL); + uint32_t written = 0; + if (!writeData(&line[pos], size, written)) + return; + pos += written; } } - - auto& writeData = *reinterpret_cast*>(cookie); - if (!writeData((const void*)buffer, cb, didwrite)) - { - return 1; - } - *pcb = didwrite; - return 0; -} - -void MainWindow::GetLog(std::function writeData) -{ - FlushBufferedConsoleStuff(); - - EDITSTREAM streamer = { (DWORD_PTR)&writeData, 0, WriteLogFileStreamer }; - SendMessage(ConWindow, EM_STREAMOUT, SF_RTF, (LPARAM)&streamer); } // each platform has its own specific version of this function. @@ -878,7 +179,7 @@ void MainWindow::SetWindowTitle(const char* caption) std::wstring widecaption; if (!caption) { - FStringf default_caption("" GAMENAME " %s " X64 " (%s)", GetVersionString(), GetGitTime()); + FStringf default_caption("" GAMENAME " %s (%s)", GetVersionString(), GetGitTime()); widecaption = default_caption.WideString(); } else diff --git a/source/common/platform/win32/i_mainwindow.h b/source/common/platform/win32/i_mainwindow.h index 2adea7bfa2..3c0c7e55df 100644 --- a/source/common/platform/win32/i_mainwindow.h +++ b/source/common/platform/win32/i_mainwindow.h @@ -20,20 +20,11 @@ class MainWindow void RestoreConView(); void ShowErrorPane(const char* text); - void CheckForRestart(); - - void HideGameTitleWindow(); - int GetGameTitleWindowHeight(); + bool CheckForRestart(); void PrintStr(const char* cp); void GetLog(std::function writeFile); - void UpdateLayout(); - - void ShowProgressBar(int maxpos); - void HideProgressBar(); - void SetProgressPos(int pos); - void ShowNetStartPane(const char* message, int maxpos); void SetNetStartProgress(int pos); bool RunMessageLoop(bool (*timer_callback)(void*), void* userdata); @@ -44,47 +35,11 @@ class MainWindow HWND GetHandle() { return Window; } private: - void LayoutMainWindow(HWND hWnd, HWND pane); - int LayoutErrorPane(HWND pane, int w); - int LayoutNetStartPane(HWND pane, int w); - - void DoPrintStr(const char* cpt); - void FlushBufferedConsoleStuff(); - - LRESULT OnMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); - LRESULT OnCreate(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); - LRESULT OnSize(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); - LRESULT OnDrawItem(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); - LRESULT OnCommand(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); - LRESULT OnClose(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); - LRESULT OnDestroy(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); - static LRESULT CALLBACK LConProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); - static INT_PTR CALLBACK ErrorPaneProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam); HWND Window = 0; - - HFONT GameTitleFont = 0; - LONG GameTitleFontHeight = 0; - LONG DefaultGUIFontHeight = 0; - LONG ErrorIconChar = 0; - bool restartrequest = false; - - HWND GameTitleWindow = 0; - HWND ErrorPane = 0; - HWND ErrorIcon = 0; - - bool ConWindowHidden = false; - HWND ConWindow = 0; TArray bufferedConsoleStuff; - - HWND ProgressBar = 0; - - HWND NetStartPane = 0; - int NetStartMaxPos = 0; - - HWND StartupScreen = 0; }; extern MainWindow mainwindow; diff --git a/source/common/platform/win32/i_system.cpp b/source/common/platform/win32/i_system.cpp index 568a18a21e..b6ea656e78 100644 --- a/source/common/platform/win32/i_system.cpp +++ b/source/common/platform/win32/i_system.cpp @@ -87,6 +87,8 @@ #include "i_interface.h" #include "i_mainwindow.h" +#include "common/widgets/launcherwindow.h" + // MACROS ------------------------------------------------------------------ #ifdef _MSC_VER @@ -342,131 +344,6 @@ static void SetQueryIWad(HWND dialog) queryiwad = query; } -//========================================================================== -// -// IWADBoxCallback -// -// Dialog proc for the IWAD selector. -// -//========================================================================== -static int* pAutoloadflags; - -BOOL CALLBACK IWADBoxCallback(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) -{ - int& flags = *pAutoloadflags;; - - HWND ctrl; - int i; - - switch (message) - { - case WM_INITDIALOG: - // Add our program name to the window title - { - WCHAR label[256]; - FString newlabel; - - GetWindowTextW(hDlg, label, countof(label)); - FString alabel(label); - newlabel.Format(GAMENAME " %s: %s", GetVersionString(), alabel.GetChars()); - auto wlabel = newlabel.WideString(); - SetWindowTextW(hDlg, wlabel.c_str()); - } - - // [SP] Upstreamed from Zandronum - char szString[256]; - - // Check the current video settings. - SendDlgItemMessage( hDlg, IDC_WELCOME_FULLSCREEN, BM_SETCHECK, vid_fullscreen ? BST_CHECKED : BST_UNCHECKED, 0 ); - switch (vid_preferbackend) - { - case 1: - SendDlgItemMessage( hDlg, IDC_WELCOME_VULKAN2, BM_SETCHECK, BST_CHECKED, 0 ); - break; -#ifdef HAVE_GLES2 - case 2: - case 3: - SendDlgItemMessage( hDlg, IDC_WELCOME_VULKAN4, BM_SETCHECK, BST_CHECKED, 0 ); - break; -#endif - default: - SendDlgItemMessage( hDlg, IDC_WELCOME_VULKAN1, BM_SETCHECK, BST_CHECKED, 0 ); - break; - } - - - // [SP] This is our's - SendDlgItemMessage( hDlg, IDC_WELCOME_NOAUTOLOAD, BM_SETCHECK, (flags & 1) ? BST_CHECKED : BST_UNCHECKED, 0); - SendDlgItemMessage( hDlg, IDC_WELCOME_LIGHTS, BM_SETCHECK, (flags & 2) ? BST_CHECKED : BST_UNCHECKED, 0 ); - SendDlgItemMessage( hDlg, IDC_WELCOME_BRIGHTMAPS, BM_SETCHECK, (flags & 4) ? BST_CHECKED : BST_UNCHECKED, 0 ); - SendDlgItemMessage( hDlg, IDC_WELCOME_WIDESCREEN, BM_SETCHECK, (flags & 8) ? BST_CHECKED : BST_UNCHECKED, 0 ); - - // Set up our version string. - snprintf(szString, sizeof(szString), "Version %s.", GetVersionString()); - SetDlgItemTextA (hDlg, IDC_WELCOME_VERSION, szString); - - // Populate the list with all the IWADs found - ctrl = GetDlgItem(hDlg, IDC_IWADLIST); - for (i = 0; i < NumWads; i++) - { - const char *filepart = strrchr(WadList[i].Path.GetChars(), '/'); - if (filepart == NULL) - filepart = WadList[i].Path.GetChars(); - else - filepart++; - - FString work; - if (*filepart) work.Format("%s (%s)", WadList[i].Name.GetChars(), filepart); - else work = WadList[i].Name.GetChars(); - std::wstring wide = work.WideString(); - SendMessage(ctrl, LB_ADDSTRING, 0, (LPARAM)wide.c_str()); - SendMessage(ctrl, LB_SETITEMDATA, i, (LPARAM)i); - } - SendMessage(ctrl, LB_SETCURSEL, DefaultWad, 0); - SetFocus(ctrl); - // Set the state of the "Don't ask me again" checkbox - ctrl = GetDlgItem(hDlg, IDC_DONTASKIWAD); - SendMessage(ctrl, BM_SETCHECK, queryiwad ? BST_UNCHECKED : BST_CHECKED, 0); - // Make sure the dialog is in front. If SHIFT was pressed to force it visible, - // then the other window will normally be on top. - SetForegroundWindow(hDlg); - break; - - case WM_COMMAND: - if (LOWORD(wParam) == IDCANCEL) - { - EndDialog (hDlg, -1); - } - else if (LOWORD(wParam) == IDOK || - (LOWORD(wParam) == IDC_IWADLIST && HIWORD(wParam) == LBN_DBLCLK)) - { - SetQueryIWad(hDlg); - // [SP] Upstreamed from Zandronum - vid_fullscreen = SendDlgItemMessage( hDlg, IDC_WELCOME_FULLSCREEN, BM_GETCHECK, 0, 0 ) == BST_CHECKED; -#ifdef HAVE_GLES2 - if (SendDlgItemMessage(hDlg, IDC_WELCOME_VULKAN4, BM_GETCHECK, 0, 0) == BST_CHECKED) - vid_preferbackend = 2; - else -#endif - if (SendDlgItemMessage(hDlg, IDC_WELCOME_VULKAN2, BM_GETCHECK, 0, 0) == BST_CHECKED) - vid_preferbackend = 1; - else if (SendDlgItemMessage(hDlg, IDC_WELCOME_VULKAN1, BM_GETCHECK, 0, 0) == BST_CHECKED) - vid_preferbackend = 0; - - // [SP] This is our's. - flags = 0; - if (SendDlgItemMessage(hDlg, IDC_WELCOME_NOAUTOLOAD, BM_GETCHECK, 0, 0) == BST_CHECKED) flags |= 1; - if (SendDlgItemMessage(hDlg, IDC_WELCOME_LIGHTS, BM_GETCHECK, 0, 0) == BST_CHECKED) flags |= 2; - if (SendDlgItemMessage(hDlg, IDC_WELCOME_BRIGHTMAPS, BM_GETCHECK, 0, 0) == BST_CHECKED) flags |= 4; - if (SendDlgItemMessage(hDlg, IDC_WELCOME_WIDESCREEN, BM_GETCHECK, 0, 0) == BST_CHECKED) flags |= 8; - ctrl = GetDlgItem (hDlg, IDC_IWADLIST); - EndDialog(hDlg, SendMessage (ctrl, LB_GETCURSEL, 0, 0)); - } - break; - } - return FALSE; -} - //========================================================================== // // I_PickIWad @@ -478,7 +355,6 @@ BOOL CALLBACK IWADBoxCallback(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPa int I_PickIWad(WadStuff *wads, int numwads, bool showwin, int defaultiwad, int& autoloadflags) { int vkey; - pAutoloadflags = &autoloadflags; if (stricmp(queryiwad_key, "shift") == 0) { vkey = VK_SHIFT; @@ -493,12 +369,7 @@ int I_PickIWad(WadStuff *wads, int numwads, bool showwin, int defaultiwad, int& } if (showwin || (vkey != 0 && GetAsyncKeyState(vkey))) { - WadList = wads; - NumWads = numwads; - DefaultWad = defaultiwad; - - return (int)DialogBox(g_hInst, MAKEINTRESOURCE(IDD_IWADDIALOG), - (HWND)mainwindow.GetHandle(), (DLGPROC)IWADBoxCallback); + return LauncherWindow::ExecModal(wads, numwads, defaultiwad, &autoloadflags); } return defaultiwad; } diff --git a/source/common/platform/win32/st_start.cpp b/source/common/platform/win32/st_start.cpp index 07039add27..8bc48d26e5 100644 --- a/source/common/platform/win32/st_start.cpp +++ b/source/common/platform/win32/st_start.cpp @@ -76,9 +76,9 @@ extern HINSTANCE g_hInst; // //========================================================================== -FStartupScreen *FStartupScreen::CreateInstance(int max_progress, bool showprogress) +FStartupScreen *FStartupScreen::CreateInstance(int max_progress) { - return new FBasicStartupScreen(max_progress, showprogress); + return new FBasicStartupScreen(max_progress); } //========================================================================== @@ -89,13 +89,9 @@ FStartupScreen *FStartupScreen::CreateInstance(int max_progress, bool showprogre // //========================================================================== -FBasicStartupScreen::FBasicStartupScreen(int max_progress, bool show_bar) +FBasicStartupScreen::FBasicStartupScreen(int max_progress) : FStartupScreen(max_progress) { - if (show_bar) - { - mainwindow.ShowProgressBar(MaxPos); - } NetMaxPos = 0; NetCurPos = 0; } @@ -111,8 +107,6 @@ FBasicStartupScreen::FBasicStartupScreen(int max_progress, bool show_bar) FBasicStartupScreen::~FBasicStartupScreen() { - mainwindow.HideProgressBar(); - KillTimer(mainwindow.GetHandle(), 1337); } //========================================================================== @@ -128,7 +122,6 @@ void FBasicStartupScreen::Progress() if (CurPos < MaxPos) { CurPos++; - mainwindow.SetProgressPos(CurPos); } } diff --git a/source/common/widgets/errorwindow.cpp b/source/common/widgets/errorwindow.cpp new file mode 100644 index 0000000000..6b6b71035c --- /dev/null +++ b/source/common/widgets/errorwindow.cpp @@ -0,0 +1,280 @@ + +#include "errorwindow.h" +#include "version.h" +#include "v_font.h" +#include "printf.h" +#include +#include +#include + +bool ErrorWindow::ExecModal(const std::string& text, const std::string& log) +{ + Size screenSize = GetScreenSize(); + double windowWidth = 1200.0; + double windowHeight = 700.0; + + auto window = std::make_unique(); + window->SetText(text, log); + window->SetFrameGeometry((screenSize.width - windowWidth) * 0.5, (screenSize.height - windowHeight) * 0.5, windowWidth, windowHeight); + window->Show(); + + DisplayWindow::RunLoop(); + + return window->Restart; +} + +ErrorWindow::ErrorWindow() : Widget(nullptr, WidgetType::Window) +{ + FStringf caption("Fatal Error - " GAMENAME " %s (%s)", GetVersionString(), GetGitTime()); + SetWindowTitle(caption.GetChars()); + SetWindowBackground(Colorf::fromRgba8(51, 51, 51)); + SetWindowBorderColor(Colorf::fromRgba8(51, 51, 51)); + SetWindowCaptionColor(Colorf::fromRgba8(33, 33, 33)); + SetWindowCaptionTextColor(Colorf::fromRgba8(226, 223, 219)); + + LogView = new LogViewer(this); + ClipboardButton = new PushButton(this); + RestartButton = new PushButton(this); + + ClipboardButton->OnClick = [=]() { OnClipboardButtonClicked(); }; + RestartButton->OnClick = [=]() { OnRestartButtonClicked(); }; + + ClipboardButton->SetText("Copy to clipboard"); + RestartButton->SetText("Restart"); + + LogView->SetFocus(); +} + +void ErrorWindow::SetText(const std::string& text, const std::string& log) +{ + LogView->SetText(text, log); + + clipboardtext.clear(); + clipboardtext.reserve(log.size() + text.size() + 100); + + // Strip the color escapes from the log + const uint8_t* cptr = (const uint8_t*)log.data(); + while (int chr = GetCharFromString(cptr)) + { + if (chr != TEXTCOLOR_ESCAPE) + { + // The bar characters, most commonly used to indicate map changes + if (chr >= 0x1D && chr <= 0x1F) + { + chr = 0x2550; // Box Drawings Double Horizontal + } + clipboardtext += MakeUTF8(chr); + } + } + + clipboardtext += "\nExecution could not continue.\n"; + clipboardtext += text; + clipboardtext += "\n"; +} + +void ErrorWindow::OnClipboardButtonClicked() +{ + SetClipboardText(clipboardtext); +} + +void ErrorWindow::OnRestartButtonClicked() +{ + Restart = true; + DisplayWindow::ExitLoop(); +} + +void ErrorWindow::OnClose() +{ + Restart = false; + DisplayWindow::ExitLoop(); +} + +void ErrorWindow::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + + double y = GetHeight() - 15.0 - ClipboardButton->GetPreferredHeight(); + ClipboardButton->SetFrameGeometry(20.0, y, 170.0, ClipboardButton->GetPreferredHeight()); + RestartButton->SetFrameGeometry(GetWidth() - 20.0 - 100.0, y, 100.0, RestartButton->GetPreferredHeight()); + y -= 20.0; + + LogView->SetFrameGeometry(Rect::xywh(0.0, 0.0, w, y)); +} + +///////////////////////////////////////////////////////////////////////////// + +LogViewer::LogViewer(Widget* parent) : Widget(parent) +{ + SetNoncontentSizes(8.0, 8.0, 3.0, 8.0); + + scrollbar = new Scrollbar(this); + scrollbar->FuncScroll = [=]() { OnScrollbarScroll(); }; +} + +void LogViewer::SetText(const std::string& text, const std::string& log) +{ + lines.clear(); + + std::string::size_type start = 0; + std::string::size_type end = log.find('\n'); + while (end != std::string::npos) + { + lines.push_back(CreateLineLayout(log.substr(start, end - start))); + start = end + 1; + end = log.find('\n', start); + } + + lines.push_back(CreateLineLayout(log.substr(start))); + + // Add an empty line as a bit of spacing + lines.push_back(CreateLineLayout({})); + + SpanLayout layout; + //layout.AddImage(Image::LoadResource("widgets/erroricon.svg"), -8.0); + layout.AddText("Execution could not continue.", largefont, Colorf::fromRgba8(255, 170, 170)); + lines.push_back(layout); + + layout.Clear(); + layout.AddText(text, largefont, Colorf::fromRgba8(255, 255, 170)); + lines.push_back(layout); + + scrollbar->SetRanges(0.0, (double)lines.size(), 1.0, 100.0); + scrollbar->SetPosition((double)lines.size() - 1.0); + + Update(); +} + +SpanLayout LogViewer::CreateLineLayout(const std::string& text) +{ + SpanLayout layout; + + Colorf curcolor = Colorf::fromRgba8(255, 255, 255); + std::string curtext; + + const uint8_t* cptr = (const uint8_t*)text.data(); + while (int chr = GetCharFromString(cptr)) + { + if (chr != TEXTCOLOR_ESCAPE) + { + // The bar characters, most commonly used to indicate map changes + if (chr >= 0x1D && chr <= 0x1F) + { + chr = 0x2550; // Box Drawings Double Horizontal + } + curtext += MakeUTF8(chr); + } + else + { + EColorRange range = V_ParseFontColor(cptr, CR_UNTRANSLATED, CR_YELLOW); + if (range != CR_UNDEFINED) + { + if (!curtext.empty()) + layout.AddText(curtext, font, curcolor); + curtext.clear(); + + PalEntry color = V_LogColorFromColorRange(range); + curcolor = Colorf::fromRgba8(color.r, color.g, color.b); + } + } + } + + curtext.push_back(' '); + layout.AddText(curtext, font, curcolor); + + return layout; +} + +void LogViewer::OnPaintFrame(Canvas* canvas) +{ + double w = GetFrameGeometry().width; + double h = GetFrameGeometry().height; + Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); + //canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); + //canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); + //canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); + //canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); +} + +void LogViewer::OnPaint(Canvas* canvas) +{ + double width = GetWidth() - scrollbar->GetFrameGeometry().width; + double y = GetHeight(); + size_t start = std::min((size_t)std::round(scrollbar->GetPosition() + 1.0), lines.size()); + for (size_t i = start; i > 0 && y > 0.0; i--) + { + SpanLayout& layout = lines[i - 1]; + layout.Layout(canvas, width); + layout.SetPosition(Point(0.0, y - layout.GetSize().height)); + layout.DrawLayout(canvas); + y -= layout.GetSize().height; + } +} + +void LogViewer::OnMouseWheel(const Point& pos, EInputKey key) +{ + if (key == IK_MouseWheelUp) + { + ScrollUp(4); + } + else if (key == IK_MouseWheelDown) + { + ScrollDown(4); + } +} + +void LogViewer::OnKeyDown(EInputKey key) +{ + if (key == IK_Home) + { + scrollbar->SetPosition(0.0f); + Update(); + } + if (key == IK_End) + { + scrollbar->SetPosition(scrollbar->GetMax()); + Update(); + } + else if (key == IK_PageUp) + { + ScrollUp(20); + } + else if (key == IK_PageDown) + { + ScrollDown(20); + } + else if (key == IK_Up) + { + ScrollUp(4); + } + else if (key == IK_Down) + { + ScrollDown(4); + } +} + +void LogViewer::OnScrollbarScroll() +{ + Update(); +} + +void LogViewer::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + double sw = scrollbar->GetPreferredWidth(); + scrollbar->SetFrameGeometry(Rect::xywh(w - sw, 0.0, sw, h)); +} + +void LogViewer::ScrollUp(int lines) +{ + scrollbar->SetPosition(std::max(scrollbar->GetPosition() - (double)lines, 0.0)); + Update(); +} + +void LogViewer::ScrollDown(int lines) +{ + scrollbar->SetPosition(std::min(scrollbar->GetPosition() + (double)lines, scrollbar->GetMax())); + Update(); +} diff --git a/source/common/widgets/errorwindow.h b/source/common/widgets/errorwindow.h new file mode 100644 index 0000000000..63304b33f1 --- /dev/null +++ b/source/common/widgets/errorwindow.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +class LogViewer; +class PushButton; +class Scrollbar; + +class ErrorWindow : public Widget +{ +public: + static bool ExecModal(const std::string& text, const std::string& log); + + ErrorWindow(); + + bool Restart = false; + +protected: + void OnClose() override; + void OnGeometryChanged() override; + +private: + void SetText(const std::string& text, const std::string& log); + + void OnClipboardButtonClicked(); + void OnRestartButtonClicked(); + + LogViewer* LogView = nullptr; + PushButton* ClipboardButton = nullptr; + PushButton* RestartButton = nullptr; + + std::string clipboardtext; +}; + +class LogViewer : public Widget +{ +public: + LogViewer(Widget* parent); + + void SetText(const std::string& text, const std::string& log); + +protected: + void OnPaintFrame(Canvas* canvas) override; + void OnPaint(Canvas* canvas) override; + void OnMouseWheel(const Point& pos, EInputKey key) override; + void OnKeyDown(EInputKey key) override; + void OnGeometryChanged() override; + +private: + void OnScrollbarScroll(); + void ScrollUp(int lines); + void ScrollDown(int lines); + + SpanLayout CreateLineLayout(const std::string& text); + + Scrollbar* scrollbar = nullptr; + + std::shared_ptr largefont = Font::Create("Poppins", 16.0); + std::shared_ptr font = Font::Create("Poppins", 12.0); + std::vector lines; +}; diff --git a/source/common/widgets/netstartwindow.cpp b/source/common/widgets/netstartwindow.cpp new file mode 100644 index 0000000000..c45f300988 --- /dev/null +++ b/source/common/widgets/netstartwindow.cpp @@ -0,0 +1,151 @@ + +#include "netstartwindow.h" +#include "version.h" +#include "engineerrors.h" +#include +#include +#include + +NetStartWindow* NetStartWindow::Instance = nullptr; + +void NetStartWindow::ShowNetStartPane(const char* message, int maxpos) +{ + Size screenSize = GetScreenSize(); + double windowWidth = 300.0; + double windowHeight = 150.0; + + if (!Instance) + { + Instance = new NetStartWindow(); + Instance->SetFrameGeometry((screenSize.width - windowWidth) * 0.5, (screenSize.height - windowHeight) * 0.5, windowWidth, windowHeight); + Instance->Show(); + } + + Instance->SetMessage(message, maxpos); +} + +void NetStartWindow::HideNetStartPane() +{ + delete Instance; + Instance = nullptr; +} + +void NetStartWindow::SetNetStartProgress(int pos) +{ + if (Instance) + Instance->SetProgress(pos); +} + +bool NetStartWindow::RunMessageLoop(bool (*newtimer_callback)(void*), void* newuserdata) +{ + if (!Instance) + return false; + + Instance->timer_callback = newtimer_callback; + Instance->userdata = newuserdata; + Instance->CallbackException = {}; + + DisplayWindow::RunLoop(); + + Instance->timer_callback = nullptr; + Instance->userdata = nullptr; + + if (Instance->CallbackException) + std::rethrow_exception(Instance->CallbackException); + + // Even though the comment in FBasicStartupScreen::NetLoop says we should return false, the old code actually throws an exception! + // This effectively also means the function always returns true... + if (!Instance->exitreason) + { + throw CExitEvent(0); + } + + return Instance->exitreason; +} + +NetStartWindow::NetStartWindow() : Widget(nullptr, WidgetType::Window) +{ + SetWindowBackground(Colorf::fromRgba8(51, 51, 51)); + SetWindowBorderColor(Colorf::fromRgba8(51, 51, 51)); + SetWindowCaptionColor(Colorf::fromRgba8(33, 33, 33)); + SetWindowCaptionTextColor(Colorf::fromRgba8(226, 223, 219)); + SetWindowTitle(GAMENAME); + + MessageLabel = new TextLabel(this); + ProgressLabel = new TextLabel(this); + AbortButton = new PushButton(this); + + MessageLabel->SetTextAlignment(TextLabelAlignment::Center); + ProgressLabel->SetTextAlignment(TextLabelAlignment::Center); + + AbortButton->OnClick = [=]() { OnClose(); }; + + AbortButton->SetText("Abort Network Game"); + + CallbackTimer = new Timer(this); + CallbackTimer->FuncExpired = [=]() { OnCallbackTimerExpired(); }; + CallbackTimer->Start(500); +} + +void NetStartWindow::SetMessage(const std::string& message, int newmaxpos) +{ + MessageLabel->SetText(message); + maxpos = newmaxpos; +} + +void NetStartWindow::SetProgress(int newpos) +{ + if (pos != newpos && maxpos > 1) + { + pos = newpos; + FString message; + message.Format("%d/%d", pos, maxpos); + ProgressLabel->SetText(message.GetChars()); + } +} + +void NetStartWindow::OnClose() +{ + exitreason = false; + DisplayWindow::ExitLoop(); +} + +void NetStartWindow::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + + double y = 15.0; + double labelheight = MessageLabel->GetPreferredHeight(); + MessageLabel->SetFrameGeometry(Rect::xywh(5.0, y, w - 10.0, labelheight)); + y += labelheight; + + labelheight = ProgressLabel->GetPreferredHeight(); + ProgressLabel->SetFrameGeometry(Rect::xywh(5.0, y, w - 10.0, labelheight)); + y += labelheight; + + y = GetHeight() - 15.0 - AbortButton->GetPreferredHeight(); + AbortButton->SetFrameGeometry((w - 200.0) * 0.5, y, 200.0, AbortButton->GetPreferredHeight()); +} + +void NetStartWindow::OnCallbackTimerExpired() +{ + if (timer_callback) + { + bool result = false; + try + { + result = timer_callback(userdata); + } + catch (...) + { + CallbackException = std::current_exception(); + } + + if (result) + { + exitreason = true; + DisplayWindow::ExitLoop(); + } + } +} diff --git a/source/common/widgets/netstartwindow.h b/source/common/widgets/netstartwindow.h new file mode 100644 index 0000000000..5d27b8ebfc --- /dev/null +++ b/source/common/widgets/netstartwindow.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +class TextLabel; +class PushButton; +class Timer; + +class NetStartWindow : public Widget +{ +public: + static void ShowNetStartPane(const char* message, int maxpos); + static void HideNetStartPane(); + static void SetNetStartProgress(int pos); + static bool RunMessageLoop(bool (*timer_callback)(void*), void* userdata); + +private: + NetStartWindow(); + + void SetMessage(const std::string& message, int maxpos); + void SetProgress(int pos); + +protected: + void OnClose() override; + void OnGeometryChanged() override; + +private: + void OnCallbackTimerExpired(); + + TextLabel* MessageLabel = nullptr; + TextLabel* ProgressLabel = nullptr; + PushButton* AbortButton = nullptr; + + Timer* CallbackTimer = nullptr; + + int pos = 0; + int maxpos = 1; + bool (*timer_callback)(void*) = nullptr; + void* userdata = nullptr; + + bool exitreason = false; + + std::exception_ptr CallbackException; + + static NetStartWindow* Instance; +}; diff --git a/source/common/widgets/widgetresourcedata.cpp b/source/common/widgets/widgetresourcedata.cpp new file mode 100644 index 0000000000..84822fc868 --- /dev/null +++ b/source/common/widgets/widgetresourcedata.cpp @@ -0,0 +1,54 @@ + +#include +#include "filesystem.h" +#include "printf.h" + +FResourceFile* WidgetResources; + +void InitWidgetResources(const char* filename) +{ + WidgetResources = FResourceFile::OpenResourceFile(filename); + if (!WidgetResources) + I_FatalError("Unable to open %s", filename); +} + +static std::vector LoadFile(const std::string& name) +{ + auto lump = WidgetResources->FindEntry(name.c_str()); + if (lump == -1) + I_FatalError("Unable to find %s", name.c_str()); + + auto reader = WidgetResources->GetEntryReader(lump, FileSys::READER_SHARED); + std::vector buffer(reader.GetLength()); + reader.Read(buffer.data(), buffer.size()); + return buffer; +} + +// This interface will later require some significant redesign. +std::vector LoadWidgetFontData(const std::string& name) +{ + std::vector returnv; + if (!stricmp(name.c_str(), "notosans")) + { + returnv.resize(3); + returnv[2].fontdata = LoadFile("widgets/noto/notosans-regular.ttf"); + returnv[0].fontdata = LoadFile("widgets/noto/notosansarmenian-regular.ttf"); + returnv[1].fontdata = LoadFile("widgets/noto/notosansgeorgian-regular.ttf"); + returnv[0].ranges.push_back(std::make_pair(0x531, 0x58f)); + returnv[1].ranges.push_back(std::make_pair(0x10a0, 0x10ff)); + returnv[1].ranges.push_back(std::make_pair(0x1c90, 0x1cbf)); + returnv[1].ranges.push_back(std::make_pair(0x2d00, 0x2d2f)); + // todo: load system CJK fonts here + return returnv; + + } + returnv.resize(1); + std::string fn = "widgets/font/" +name + ".ttf"; + returnv[0].fontdata = LoadFile(fn); + return returnv; +} + +std::vector LoadWidgetImageData(const std::string& name) +{ + return LoadFile(name); +} diff --git a/source/core/gamecontrol.cpp b/source/core/gamecontrol.cpp index 62241abb05..b7ed7c05ac 100644 --- a/source/core/gamecontrol.cpp +++ b/source/core/gamecontrol.cpp @@ -89,6 +89,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #include "buildtiles.h" void LoadHexFont(const char* filename); +void InitWidgetResources(const char* basewad); CVAR(Bool, autoloadlights, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR(Bool, autoloadbrightmaps, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) @@ -1029,6 +1030,7 @@ int RunGame() I_FatalError("Cannot find " ENGINERES_FILE); } LoadHexFont(wad); // load hex font early so we have it during startup. + InitWidgetResources(wad); // Set up the console before anything else so that it can receive text. C_InitConsole(1024, 768, true); @@ -1109,7 +1111,7 @@ int RunGame() V_InitScreenSize(); V_InitScreen(); - StartWindow = FStartupScreen::CreateInstance(8, true); + StartWindow = FStartupScreen::CreateInstance(8); StartWindow->Progress(); if (!GameConfig->IsInitialized()) diff --git a/source/core/rendering/scene/hw_walls.cpp b/source/core/rendering/scene/hw_walls.cpp index b00df04a7d..86aa273896 100644 --- a/source/core/rendering/scene/hw_walls.cpp +++ b/source/core/rendering/scene/hw_walls.cpp @@ -420,7 +420,7 @@ void HWWall::PutWall(HWDrawInfo *di, bool translucent) if (seg && (seg->cstat & CSTAT_WALL_ROTATE_90)) { float f; - // rotate 90° clockwise. The coordinates have already been set up in rotated space, so all we need to do here is swap u and v and then negate the new v. + // rotate 90 degrees clockwise. The coordinates have already been set up in rotated space, so all we need to do here is swap u and v and then negate the new v. f = tcs[UPLFT].u; tcs[UPLFT].u = tcs[UPLFT].v; tcs[UPLFT].v = 1.f - f; f = tcs[LOLFT].u; tcs[LOLFT].u = tcs[LOLFT].v; tcs[LOLFT].v = 1.f - f; f = tcs[UPRGT].u; tcs[UPRGT].u = tcs[UPRGT].v; tcs[UPRGT].v = 1.f - f; diff --git a/source/launcher/launcherwindow.cpp b/source/launcher/launcherwindow.cpp new file mode 100644 index 0000000000..663eb03a75 --- /dev/null +++ b/source/launcher/launcherwindow.cpp @@ -0,0 +1,268 @@ +#include "launcherwindow.h" +#include "v_video.h" +#include "version.h" +#include "i_interface.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef RENDER_BACKENDS +EXTERN_CVAR(Int, vid_preferbackend); +#endif + +EXTERN_CVAR(Bool, queryiwad); + +int LauncherWindow::ExecModal(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags) +{ + Size screenSize = GetScreenSize(); + double windowWidth = 615.0; + double windowHeight = 668.0; + + auto launcher = std::make_unique(wads, numwads, defaultiwad, autoloadflags); + launcher->SetFrameGeometry((screenSize.width - windowWidth) * 0.5, (screenSize.height - windowHeight) * 0.5, windowWidth, windowHeight); + launcher->Show(); + + DisplayWindow::RunLoop(); + + return launcher->ExecResult; +} + +LauncherWindow::LauncherWindow(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags) : Widget(nullptr, WidgetType::Window), AutoloadFlags(autoloadflags) +{ + SetWindowBackground(Colorf::fromRgba8(51, 51, 51)); + SetWindowBorderColor(Colorf::fromRgba8(51, 51, 51)); + SetWindowCaptionColor(Colorf::fromRgba8(33, 33, 33)); + SetWindowCaptionTextColor(Colorf::fromRgba8(226, 223, 219)); + SetWindowTitle(GAMENAME); + + Logo = new ImageBox(this); + WelcomeLabel = new TextLabel(this); + VersionLabel = new TextLabel(this); + SelectLabel = new TextLabel(this); + GeneralLabel = new TextLabel(this); + ExtrasLabel = new TextLabel(this); + ParametersLabel = new TextLabel(this); + FullscreenCheckbox = new CheckboxLabel(this); + DisableAutoloadCheckbox = new CheckboxLabel(this); + DontAskAgainCheckbox = new CheckboxLabel(this); + LightsCheckbox = new CheckboxLabel(this); + BrightmapsCheckbox = new CheckboxLabel(this); + WidescreenCheckbox = new CheckboxLabel(this); + PlayButton = new PushButton(this); + ExitButton = new PushButton(this); + GamesList = new ListView(this); + ParametersEdit = new LineEdit(this); + + PlayButton->OnClick = [=]() { OnPlayButtonClicked(); }; + ExitButton->OnClick = [=]() { OnExitButtonClicked(); }; + GamesList->OnActivated = [=]() { OnGamesListActivated(); }; + + SelectLabel->SetText("Select which game file (IWAD) to run."); + PlayButton->SetText("Play Game"); + ExitButton->SetText("Exit"); + + GeneralLabel->SetText("General"); + ExtrasLabel->SetText("Extra Graphics"); + FullscreenCheckbox->SetText("Fullscreen"); + DisableAutoloadCheckbox->SetText("Disable autoload"); + DontAskAgainCheckbox->SetText("Don't ask me again"); + LightsCheckbox->SetText("Lights"); + BrightmapsCheckbox->SetText("Brightmaps"); + WidescreenCheckbox->SetText("Widescreen"); + ParametersLabel->SetText("Additional Parameters:"); + +#ifdef RENDER_BACKENDS + BackendLabel = new TextLabel(this); + VulkanCheckbox = new CheckboxLabel(this); + OpenGLCheckbox = new CheckboxLabel(this); + GLESCheckbox = new CheckboxLabel(this); + BackendLabel->SetText("Render Backend"); + VulkanCheckbox->SetText("Vulkan"); + OpenGLCheckbox->SetText("OpenGL"); + GLESCheckbox->SetText("OpenGL ES"); +#endif + + FString welcomeText, versionText; + welcomeText.Format("Welcome to %s!", GAMENAME); + versionText.Format("Version %s.", GetVersionString()); + WelcomeLabel->SetText(welcomeText.GetChars()); + VersionLabel->SetText(versionText.GetChars()); + + FullscreenCheckbox->SetChecked(vid_fullscreen); + DontAskAgainCheckbox->SetChecked(!queryiwad); + + int flags = *autoloadflags; + DisableAutoloadCheckbox->SetChecked(flags & 1); + LightsCheckbox->SetChecked(flags & 2); + BrightmapsCheckbox->SetChecked(flags & 4); + WidescreenCheckbox->SetChecked(flags & 8); + +#ifdef RENDER_BACKENDS + OpenGLCheckbox->SetRadioStyle(true); + VulkanCheckbox->SetRadioStyle(true); + GLESCheckbox->SetRadioStyle(true); + OpenGLCheckbox->FuncChanged = [this](bool on) { if (on) { VulkanCheckbox->SetChecked(false); GLESCheckbox->SetChecked(false); }}; + VulkanCheckbox->FuncChanged = [this](bool on) { if (on) { OpenGLCheckbox->SetChecked(false); GLESCheckbox->SetChecked(false); }}; + GLESCheckbox->FuncChanged = [this](bool on) { if (on) { VulkanCheckbox->SetChecked(false); OpenGLCheckbox->SetChecked(false); }}; + switch (vid_preferbackend) + { + case 0: + OpenGLCheckbox->SetChecked(true); + break; + case 1: + VulkanCheckbox->SetChecked(true); + break; + case 2: + GLESCheckbox->SetChecked(true); + break; + } +#endif + + for (int i = 0; i < numwads; i++) + { + const char* filepart = strrchr(wads[i].Path.GetChars(), '/'); + if (filepart == NULL) + filepart = wads[i].Path.GetChars(); + else + filepart++; + + FString work; + if (*filepart) work.Format("%s (%s)", wads[i].Name.GetChars(), filepart); + else work = wads[i].Name.GetChars(); + + GamesList->AddItem(work.GetChars()); + } + + Logo->SetImage(Image::LoadResource("widgets/banner.png")); + + GamesList->SetFocus(); +} + +void LauncherWindow::OnClose() +{ + OnExitButtonClicked(); +} + +void LauncherWindow::OnPlayButtonClicked() +{ + vid_fullscreen = FullscreenCheckbox->GetChecked(); + queryiwad = !DontAskAgainCheckbox->GetChecked(); + + int flags = 0; + if (DisableAutoloadCheckbox->GetChecked()) flags |= 1; + if (LightsCheckbox->GetChecked()) flags |= 2; + if (BrightmapsCheckbox->GetChecked()) flags |= 4; + if (WidescreenCheckbox->GetChecked()) flags |= 8; + *AutoloadFlags = flags; + +#ifdef RENDER_BACKENDS + int v = 1; + if (OpenGLCheckbox->GetChecked()) v = 0; + else if (VulkanCheckbox->GetChecked()) v = 1; + else if (GLESCheckbox->GetChecked()) v = 2; + if (v != vid_preferbackend) vid_preferbackend = v; +#endif + + std::string extraargs = ParametersEdit->GetText(); + if (!extraargs.empty()) + { + // To do: restart the process like the cocoa backend is doing? + } + + ExecResult = GamesList->GetSelectedItem(); + DisplayWindow::ExitLoop(); +} + +void LauncherWindow::OnExitButtonClicked() +{ + ExecResult = -1; + DisplayWindow::ExitLoop(); +} + +void LauncherWindow::OnGamesListActivated() +{ + OnPlayButtonClicked(); +} + +void LauncherWindow::OnGeometryChanged() +{ + double y = 0.0; + + Logo->SetFrameGeometry(0.0, y, GetWidth(), Logo->GetPreferredHeight()); + y += Logo->GetPreferredHeight(); + + y += 10.0; + + WelcomeLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0, WelcomeLabel->GetPreferredHeight()); + y += WelcomeLabel->GetPreferredHeight(); + + VersionLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0, VersionLabel->GetPreferredHeight()); + y += VersionLabel->GetPreferredHeight(); + + y += 10.0; + + SelectLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0, SelectLabel->GetPreferredHeight()); + y += SelectLabel->GetPreferredHeight(); + + double listViewTop = y + 10.0; + + y = GetHeight() - 15.0 - PlayButton->GetPreferredHeight(); + PlayButton->SetFrameGeometry(20.0, y, 120.0, PlayButton->GetPreferredHeight()); + ExitButton->SetFrameGeometry(GetWidth() - 20.0 - 120.0, y, 120.0, PlayButton->GetPreferredHeight()); + + y -= 20.0; + + double editHeight = 24.0; + y -= editHeight; + ParametersEdit->SetFrameGeometry(20.0, y, GetWidth() - 40.0, editHeight); + y -= 5.0; + + double labelHeight = ParametersLabel->GetPreferredHeight(); + y -= labelHeight; + ParametersLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0, labelHeight); + y -= 10.0; + + double panelWidth = 150.0; + +#ifdef RENDER_BACKENDS + auto yy = y; + y -= GLESCheckbox->GetPreferredHeight(); + double x = GetWidth() / 2 - panelWidth / 2; + GLESCheckbox->SetFrameGeometry(x, y, 190.0, GLESCheckbox->GetPreferredHeight()); + + y -= OpenGLCheckbox->GetPreferredHeight(); + OpenGLCheckbox->SetFrameGeometry(x, y, 190.0, OpenGLCheckbox->GetPreferredHeight()); + + y -= VulkanCheckbox->GetPreferredHeight(); + VulkanCheckbox->SetFrameGeometry(x, y, 190.0, VulkanCheckbox->GetPreferredHeight()); + + y -= BackendLabel->GetPreferredHeight(); + BackendLabel->SetFrameGeometry(x, y, 190.0, BackendLabel->GetPreferredHeight()); + + y = yy; +#endif + y -= DontAskAgainCheckbox->GetPreferredHeight(); + DontAskAgainCheckbox->SetFrameGeometry(20.0, y, 190.0, DontAskAgainCheckbox->GetPreferredHeight()); + WidescreenCheckbox->SetFrameGeometry(GetWidth() - 20.0 - panelWidth, y, panelWidth, WidescreenCheckbox->GetPreferredHeight()); + + y -= DisableAutoloadCheckbox->GetPreferredHeight(); + DisableAutoloadCheckbox->SetFrameGeometry(20.0, y, 190.0, DisableAutoloadCheckbox->GetPreferredHeight()); + BrightmapsCheckbox->SetFrameGeometry(GetWidth() - 20.0 - panelWidth, y, panelWidth, BrightmapsCheckbox->GetPreferredHeight()); + + y -= FullscreenCheckbox->GetPreferredHeight(); + FullscreenCheckbox->SetFrameGeometry(20.0, y, 190.0, FullscreenCheckbox->GetPreferredHeight()); + LightsCheckbox->SetFrameGeometry(GetWidth() - 20.0 - panelWidth, y, panelWidth, LightsCheckbox->GetPreferredHeight()); + + y -= GeneralLabel->GetPreferredHeight(); + GeneralLabel->SetFrameGeometry(20.0, y, 190.0, GeneralLabel->GetPreferredHeight()); + ExtrasLabel->SetFrameGeometry(GetWidth() - 20.0 - panelWidth, y, panelWidth, ExtrasLabel->GetPreferredHeight()); + + double listViewBottom = y - 10.0; + GamesList->SetFrameGeometry(20.0, listViewTop, GetWidth() - 40.0, std::max(listViewBottom - listViewTop, 0.0)); +} diff --git a/source/launcher/launcherwindow.h b/source/launcher/launcherwindow.h new file mode 100644 index 0000000000..6f48a05254 --- /dev/null +++ b/source/launcher/launcherwindow.h @@ -0,0 +1,57 @@ +#pragma once + +#include + + +#define RENDER_BACKENDS + +class ImageBox; +class TextLabel; +class CheckboxLabel; +class PushButton; +class ListView; +class LineEdit; +struct WadStuff; + +class LauncherWindow : public Widget +{ +public: + static int ExecModal(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags); + + LauncherWindow(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags); + +private: + void OnPlayButtonClicked(); + void OnExitButtonClicked(); + void OnGamesListActivated(); + + void OnClose() override; + void OnGeometryChanged() override; + + ImageBox* Logo = nullptr; + TextLabel* WelcomeLabel = nullptr; + TextLabel* VersionLabel = nullptr; + TextLabel* SelectLabel = nullptr; + TextLabel* GeneralLabel = nullptr; + TextLabel* ExtrasLabel = nullptr; + TextLabel* ParametersLabel = nullptr; + CheckboxLabel* FullscreenCheckbox = nullptr; + CheckboxLabel* DisableAutoloadCheckbox = nullptr; + CheckboxLabel* DontAskAgainCheckbox = nullptr; + CheckboxLabel* LightsCheckbox = nullptr; + CheckboxLabel* BrightmapsCheckbox = nullptr; + CheckboxLabel* WidescreenCheckbox = nullptr; +#ifdef RENDER_BACKENDS + TextLabel* BackendLabel = nullptr; + CheckboxLabel* VulkanCheckbox = nullptr; + CheckboxLabel* OpenGLCheckbox = nullptr; + CheckboxLabel* GLESCheckbox = nullptr; +#endif + PushButton* PlayButton = nullptr; + PushButton* ExitButton = nullptr; + ListView* GamesList = nullptr; + LineEdit* ParametersEdit = nullptr; + + int* AutoloadFlags = nullptr; + int ExecResult = -1; +}; diff --git a/wadsrc/static/widgets/banner.png b/wadsrc/static/widgets/banner.png new file mode 100644 index 0000000000..8b792e51a1 Binary files /dev/null and b/wadsrc/static/widgets/banner.png differ diff --git a/wadsrc/static/widgets/noto/NotoSans-Regular.ttf b/wadsrc/static/widgets/noto/NotoSans-Regular.ttf new file mode 100644 index 0000000000..fa4cff505e Binary files /dev/null and b/wadsrc/static/widgets/noto/NotoSans-Regular.ttf differ diff --git a/wadsrc/static/widgets/noto/NotoSansArmenian-Regular.ttf b/wadsrc/static/widgets/noto/NotoSansArmenian-Regular.ttf new file mode 100644 index 0000000000..097432059b Binary files /dev/null and b/wadsrc/static/widgets/noto/NotoSansArmenian-Regular.ttf differ diff --git a/wadsrc/static/widgets/noto/NotoSansGeorgian-Regular.ttf b/wadsrc/static/widgets/noto/NotoSansGeorgian-Regular.ttf new file mode 100644 index 0000000000..49163cc37e Binary files /dev/null and b/wadsrc/static/widgets/noto/NotoSansGeorgian-Regular.ttf differ diff --git a/wadsrc/static/widgets/noto/NotoSansJP-Medium.ttf b/wadsrc/static/widgets/noto/NotoSansJP-Medium.ttf new file mode 100644 index 0000000000..1d89aefe9c Binary files /dev/null and b/wadsrc/static/widgets/noto/NotoSansJP-Medium.ttf differ diff --git a/wadsrc/static/widgets/noto/OFL.txt b/wadsrc/static/widgets/noto/OFL.txt new file mode 100644 index 0000000000..76df3b5656 --- /dev/null +++ b/wadsrc/static/widgets/noto/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE.