diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..8b04ae1 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,99 @@ +name: build + +on: + push: + pull_request: + +env: + BUILD_TYPE: Release + +jobs: + linux: + runs-on: ubuntu-20.04 + name: 🐧 Ubuntu 20.04 + steps: + - name: 🧰 Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: true + + - name: ⬇️ Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + gcc-10 \ + g++-10 \ + libsdl2-dev \ + libsdl2-mixer-dev + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure CMake + shell: bash + env: + CC: gcc-10 + CXX: g++-10 + working-directory: ${{runner.workspace}}/build + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE + + - name: Build + working-directory: ${{runner.workspace}}/build + shell: bash + env: + CC: gcc-10 + CXX: g++-10 + run: cmake --build . --config $BUILD_TYPE + + win: + runs-on: windows-latest + name: 🟦 Windows x64 + steps: + + - name: 🧰 Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: true + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure CMake Windows + shell: bash + working-directory: ${{runner.workspace}}/build + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_TOOLCHAIN_FILE="C:/vcpkg/scripts/buildsystems/vcpkg.cmake" -DVCPKG_BIN_DIR="c:/vcpkg/installed/x64-windows/bin" + + - name: Build + working-directory: ${{runner.workspace}}/build + shell: bash + run: cmake --build . --config $BUILD_TYPE + +# macos-build: +# runs-on: macos-10.15 +# name: 🍎 macOS 10.15 +# steps: + +# - name: 🧰 Checkout +# uses: actions/checkout@v2 +# with: +# fetch-depth: 0 +# submodules: true + +# - name: ⬇️ Install dependencies +# run: brew install sdl2 sdl2_mixer + +# - name: Create Build Environment +# run: cmake -E make_directory ${{runner.workspace}}/build + +# - name: Configure CMake +# shell: bash +# working-directory: ${{runner.workspace}}/build +# run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE + +# - name: Build +# working-directory: ${{runner.workspace}}/build +# shell: bash +# run: | +# cmake --build . --config $BUILD_TYPE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..30887f7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,118 @@ +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/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/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/window/window.h +) + +set(ZWIDGET_WIN32_SOURCES + src/window/win32/win32window.cpp + src/window/win32/win32window.h +) + +set(ZWIDGET_UNIX_SOURCES +) + +set(EXAMPLE_SOURCES + example/example.cpp +) + +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\\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\\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\\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\\unix" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/unix/.+") + +source_group("example" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/example/.+") + +include_directories(include include/zwidget src) + +if(WIN32) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_WIN32_SOURCES}) + add_definitions(-DUNICODE -D_UNICODE) +else() + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_UNIX_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) + +add_executable(zwidget_example WIN32 MACOSX_BUNDLE ${EXAMPLE_SOURCES}) +target_link_libraries(zwidget_example zwidget) +set_target_properties(zwidget_example PROPERTIES CXX_STANDARD 17) + +if(MSVC) + set_property(TARGET zwidget PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + set_property(TARGET zwidget_example PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fdc0622 --- /dev/null +++ b/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/README.md b/README.md index f6f91ef..c3bb8e2 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# ZWidget \ No newline at end of file +# ZWidget +A framework for building user interface applications diff --git a/example/example.cpp b/example/example.cpp new file mode 100644 index 0000000..beb5720 --- /dev/null +++ b/example/example.cpp @@ -0,0 +1,98 @@ + +#include +#include +#include +#include +#include + +#ifdef WIN32 + +#include +#include + +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + +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; +} + +static std::vector ReadAllBytes(const std::string& filename) +{ + HANDLE handle = CreateFile(to_utf16(filename).c_str(), FILE_READ_ACCESS, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + if (handle == INVALID_HANDLE_VALUE) + throw std::runtime_error("Could not open " + filename); + + LARGE_INTEGER fileSize; + BOOL result = GetFileSizeEx(handle, &fileSize); + if (result == FALSE) + { + CloseHandle(handle); + throw std::runtime_error("GetFileSizeEx failed"); + } + + std::vector buffer(fileSize.QuadPart); + + DWORD bytesRead = 0; + result = ReadFile(handle, buffer.data(), (DWORD)buffer.size(), &bytesRead, nullptr); + if (result == FALSE || bytesRead != buffer.size()) + { + CloseHandle(handle); + throw std::runtime_error("ReadFile failed"); + } + + CloseHandle(handle); + + return buffer; +} + +std::vector LoadWidgetFontData(const std::string& name) +{ + return ReadAllBytes("C:\\Windows\\Fonts\\segoeui.ttf"); +} + +int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow) +{ + auto mainwindow = new MainWindow(); + auto textedit = new TextEdit(mainwindow); + textedit->SetText(R"( +#version 460 + +in vec4 AttrPos; +in vec4 AttrColor; +out vec4 Color; + +void main() +{ + gl_Position = AttrPos; + Color = AttrColor; +} +)"); + mainwindow->SetWindowTitle("ZWidget Example"); + mainwindow->SetFrameGeometry(100.0, 100.0, 1700.0, 900.0); + mainwindow->SetCentralWidget(textedit); + textedit->SetFocus(); + mainwindow->Show(); + + DisplayWindow::RunLoop(); + + return 0; +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/include/zwidget/core/canvas.h b/include/zwidget/core/canvas.h new file mode 100644 index 0000000..a546e62 --- /dev/null +++ b/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/include/zwidget/core/colorf.h b/include/zwidget/core/colorf.h new file mode 100644 index 0000000..0c74cd1 --- /dev/null +++ b/include/zwidget/core/colorf.h @@ -0,0 +1,23 @@ +#pragma once + +#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 }; + } + + float r = 0.0f; + float g = 0.0f; + float b = 0.0f; + float a = 1.0f; +}; diff --git a/include/zwidget/core/font.h b/include/zwidget/core/font.h new file mode 100644 index 0000000..d4af360 --- /dev/null +++ b/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/include/zwidget/core/image.h b/include/zwidget/core/image.h new file mode 100644 index 0000000..12393bd --- /dev/null +++ b/include/zwidget/core/image.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +enum class ImageFormat +{ + R8G8B8A8 +}; + +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); +}; diff --git a/include/zwidget/core/rect.h b/include/zwidget/core/rect.h new file mode 100644 index 0000000..f5ab17c --- /dev/null +++ b/include/zwidget/core/rect.h @@ -0,0 +1,74 @@ +#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); } + + 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/include/zwidget/core/resourcedata.h b/include/zwidget/core/resourcedata.h new file mode 100644 index 0000000..1c60ee1 --- /dev/null +++ b/include/zwidget/core/resourcedata.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include + +std::vector LoadWidgetFontData(const std::string& name); diff --git a/include/zwidget/core/span_layout.h b/include/zwidget/core/span_layout.h new file mode 100644 index 0000000..4a1ddd1 --- /dev/null +++ b/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(153 / 255.0f, 201 / 255.0f, 239 / 255.0f); + + 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/include/zwidget/core/timer.h b/include/zwidget/core/timer.h new file mode 100644 index 0000000..0cbfc8d --- /dev/null +++ b/include/zwidget/core/timer.h @@ -0,0 +1,24 @@ +#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; + + friend class Widget; +}; diff --git a/include/zwidget/core/utf8reader.h b/include/zwidget/core/utf8reader.h new file mode 100644 index 0000000..f9c186b --- /dev/null +++ b/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/include/zwidget/core/widget.h b/include/zwidget/core/widget.h new file mode 100644 index 0000000..0151405 --- /dev/null +++ b/include/zwidget/core/widget.h @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include "canvas.h" +#include "rect.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 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 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); } + +protected: + 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 Geometry = Rect::xywh(-1.0, -1.0, 0.0, 0.0); + + std::string WindowTitle; + std::unique_ptr DispWindow; + std::unique_ptr DispCanvas; + Widget* FocusWidget = nullptr; + Widget* CaptureWidget = nullptr; + + Widget(const Widget&) = delete; + Widget& operator=(const Widget&) = delete; + + friend class Timer; +}; diff --git a/include/zwidget/widgets/lineedit/lineedit.h b/include/zwidget/widgets/lineedit/lineedit.h new file mode 100644 index 0000000..464dcdc --- /dev/null +++ b/include/zwidget/widgets/lineedit/lineedit.h @@ -0,0 +1,159 @@ + +#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 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/include/zwidget/widgets/mainwindow/mainwindow.h b/include/zwidget/widgets/mainwindow/mainwindow.h new file mode 100644 index 0000000..abba23d --- /dev/null +++ b/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/include/zwidget/widgets/menubar/menubar.h b/include/zwidget/widgets/menubar/menubar.h new file mode 100644 index 0000000..9c13283 --- /dev/null +++ b/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/include/zwidget/widgets/scrollbar/scrollbar.h b/include/zwidget/widgets/scrollbar/scrollbar.h new file mode 100644 index 0000000..f582618 --- /dev/null +++ b/include/zwidget/widgets/scrollbar/scrollbar.h @@ -0,0 +1,97 @@ + +#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; + int GetMin() const; + int GetMax() const; + int GetLineStep() const; + int GetPageStep() const; + int GetPosition() const; + + void SetVertical(); + void SetHorizontal(); + + void SetMin(int scroll_min); + void SetMax(int scroll_max); + void SetLineStep(int step); + void SetPageStep(int step); + + void SetRanges(int scroll_min, int scroll_max, int line_step, int page_step); + void SetRanges(int view_size, int total_size); + + void SetPosition(int pos); + + 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(); + int CalculateThumbSize(int track_size); + int CalculateThumbPosition(int thumb_size, int track_size); + Rect CreateRect(int start, int end); + void InvokeScrollEvent(std::function* event_ptr); + void OnTimerExpired(); + + bool vertical = false; + int scroll_min = 0; + int scroll_max = 1; + int line_step = 1; + int page_step = 10; + int position = 0; + + 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; + + int thumb_start_position = 0; + Point mouse_drag_start_pos; + int thumb_start_pixel_position = 0; + + Timer* mouse_down_timer = nullptr; + int last_step_size = 0; + + Rect rect_button_decrement; + Rect rect_track_decrement; + Rect rect_thumb; + Rect rect_track_increment; + Rect rect_button_increment; + + std::function* FuncScrollOnMouseDown = nullptr; + + static const int decr_height = 16; + static const int incr_height = 16; +}; diff --git a/include/zwidget/widgets/statusbar/statusbar.h b/include/zwidget/widgets/statusbar/statusbar.h new file mode 100644 index 0000000..c03a3e2 --- /dev/null +++ b/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/include/zwidget/widgets/textedit/textedit.h b/include/zwidget/widgets/textedit/textedit.h new file mode 100644 index 0000000..8b0632e --- /dev/null +++ b/include/zwidget/widgets/textedit/textedit.h @@ -0,0 +1,155 @@ + +#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 OnPaint(Canvas* canvas); + 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("Segoe UI", 12.0); + + template + static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } +}; diff --git a/include/zwidget/widgets/toolbar/toolbar.h b/include/zwidget/widgets/toolbar/toolbar.h new file mode 100644 index 0000000..ca0a89c --- /dev/null +++ b/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/include/zwidget/widgets/toolbar/toolbarbutton.h b/include/zwidget/widgets/toolbar/toolbarbutton.h new file mode 100644 index 0000000..933a651 --- /dev/null +++ b/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/include/zwidget/window/window.h b/include/zwidget/window/window.h new file mode 100644 index 0000000..1714bf6 --- /dev/null +++ b/include/zwidget/window/window.h @@ -0,0 +1,169 @@ +#pragma once + +#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(); + + 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 Update() = 0; + virtual bool GetKeyState(EInputKey key) = 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 PresentBitmap(int width, int height, const uint32_t* pixels) = 0; +}; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp new file mode 100644 index 0000000..1a26293 --- /dev/null +++ b/src/core/canvas.cpp @@ -0,0 +1,666 @@ + +#include "core/canvas.h" +#include "core/rect.h" +#include "core/colorf.h" +#include "core/utf8reader.h" +#include "core/resourcedata.h" +#include "window/window.h" +#include "schrift/schrift.h" +#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) : fontname(fontname), height(height) + { + data = LoadWidgetFontData(fontname); + 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 = 2; x < w; x += 3) + { + uint32_t red = sline[x - 2]; + uint32_t green = sline[x - 1]; + uint32_t blue = sline[x]; + uint32_t alpha = (red | green | blue) ? 255 : 0; + + uint32_t maxval = std::max(std::max(red, green), blue); + red = std::max(red, maxval / 5); + green = std::max(green, maxval / 5); + blue = std::max(blue, maxval / 5); + + dline[x / 3] = (alpha << 24) | (red << 16) | (green << 8) | blue; + } + if (w % 3 == 1) + { + uint32_t red = sline[w - 1]; + uint32_t green = 0; + uint32_t blue = 0; + uint32_t alpha = (red | green | blue) ? 255 : 0; + + uint32_t maxval = std::max(std::max(red, green), blue); + red = std::max(red, maxval / 5); + green = std::max(green, maxval / 5); + blue = std::max(blue, maxval / 5); + + dline[(w - 1) / 3] = (alpha << 24) | (red << 16) | (green << 8) | blue; + } + else if (w % 3 == 2) + { + uint32_t red = sline[w - 2]; + uint32_t green = sline[w - 1]; + uint32_t blue = 0; + uint32_t alpha = (red | green | blue) ? 255 : 0; + + uint32_t maxval = std::max(std::max(red, green), blue); + red = std::max(red, maxval / 5); + green = std::max(green, maxval / 5); + blue = std::max(blue, maxval / 5); + + dline[(w - 1) / 3] = (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 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); + + 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; +}; + +BitmapCanvas::BitmapCanvas(DisplayWindow* window) : window(window) +{ + uint32_t white = 0xffffffff; + whiteTexture = createTexture(1, 1, &white); + font = std::make_unique("Segoe UI", 13.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::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 = origin.x + std::round(pos.x * uiscale); + double y = origin.y + std::round(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->textmetrics.ascender - font->textmetrics.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, y); +} + +VerticalTextPosition BitmapCanvas::verticalTextAlign() +{ + VerticalTextPosition align; + align.top = 0.0f; + align.baseline = font->textmetrics.ascender; + align.bottom = font->textmetrics.ascender - font->textmetrics.descender; + return align; +} + +std::unique_ptr BitmapCanvas::createTexture(int width, int height, const void* pixels) +{ + auto texture = std::make_unique(); + texture->Width = width; + texture->Height = height; + texture->Data.resize(width * height); + memcpy(texture->Data.data(), pixels, width * height * sizeof(uint32_t)); + 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, 0.0); +} + +int BitmapCanvas::getClipMinY() const +{ + return clipStack.empty() ? 0 : (int)std::max(clipStack.back().y, 0.0); +} + +int BitmapCanvas::getClipMaxX() const +{ + return clipStack.empty() ? width : (int)std::min(clipStack.back().x + clipStack.back().width, (double)width); +} + +int BitmapCanvas::getClipMaxY() const +{ + return clipStack.empty() ? height : (int)std::min(clipStack.back().y + clipStack.back().height, (double)height); +} + +void BitmapCanvas::drawTile(CanvasTexture* texture, float x, float y, 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)x; + int x1 = (int)(x + width); + int y0 = (int)y; + int y1 = (int)(y + 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 - y0); + 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 - x0); + 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 x, float y, 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)x; + int x1 = (int)(x + width); + int y0 = (int)y; + int y1 = (int)(y + 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 - y0); + 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 - x0); + 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) +{ + 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/src/core/font.cpp b/src/core/font.cpp new file mode 100644 index 0000000..f646912 --- /dev/null +++ b/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/src/core/image.cpp b/src/core/image.cpp new file mode 100644 index 0000000..7869e26 --- /dev/null +++ b/src/core/image.cpp @@ -0,0 +1,43 @@ + +#include "core/image.h" +#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); + 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); +} diff --git a/src/core/schrift/LICENSE.txt b/src/core/schrift/LICENSE.txt new file mode 100644 index 0000000..6bb186f --- /dev/null +++ b/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/src/core/schrift/schrift.cpp b/src/core/schrift/schrift.cpp new file mode 100644 index 0000000..0febb86 --- /dev/null +++ b/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/src/core/schrift/schrift.h b/src/core/schrift/schrift.h new file mode 100644 index 0000000..93d2051 --- /dev/null +++ b/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/src/core/span_layout.cpp b/src/core/span_layout.cpp new file mode 100644 index 0000000..1b68f6e --- /dev/null +++ b/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/src/core/timer.cpp b/src/core/timer.cpp new file mode 100644 index 0000000..9e57551 --- /dev/null +++ b/src/core/timer.cpp @@ -0,0 +1,29 @@ + +#include "core/timer.h" +#include "core/widget.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) +{ +} + +void Timer::Stop() +{ +} diff --git a/src/core/utf8reader.cpp b/src/core/utf8reader.cpp new file mode 100644 index 0000000..200da9f --- /dev/null +++ b/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/src/core/widget.cpp b/src/core/widget.cpp new file mode 100644 index 0000000..22b78d5 --- /dev/null +++ b/src/core/widget.cpp @@ -0,0 +1,530 @@ + +#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() +{ + 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 Geometry.size(); +} + +Rect Widget::GetFrameGeometry() const +{ + if (Type == WidgetType::Child) + { + return Geometry; + } + else + { + return DispWindow->GetWindowFrame(); + } +} + +void Widget::SetFrameGeometry(const Rect& geometry) +{ + if (Type == WidgetType::Child) + { + Geometry = geometry; + 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::Update() +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->Update(); + } +} + +void Widget::Repaint() +{ + Widget* w = Window(); + w->DispCanvas->begin(Colorf(240/255.0f, 240/255.0f, 240/255.0f)); + w->Paint(DispCanvas.get()); + w->DispCanvas->end(); +} + +void Widget::Paint(Canvas* canvas) +{ + Point oldOrigin = canvas->getOrigin(); + canvas->pushClip(Geometry); + canvas->setOrigin(oldOrigin + Geometry.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) +{ +} + +void Widget::CaptureMouse() +{ +} + +void Widget::ReleaseMouseCapture() +{ +} + +std::string Widget::GetClipboardText() +{ + return {}; +} + +void Widget::SetClipboardText(const std::string& 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->Geometry.contains(pos)) + { + Widget* cur2 = cur->ChildAt(pos - cur->Geometry.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->Geometry.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->Geometry.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->Geometry.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->Geometry.topLeft(); + } + throw std::runtime_error("MapFromGlobal: no window widget found"); +} + +void Widget::OnWindowPaint() +{ + Repaint(); +} + +void Widget::OnWindowMouseMove(const Point& pos) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseMove(CaptureWidget->MapFrom(this, pos)); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + 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(); + Geometry = Rect::xywh(0.0, 0.0, size.width, size.height); + OnGeometryChanged(); +} + +void Widget::OnWindowClose() +{ + Close(); +} + +void Widget::OnWindowActivated() +{ +} + +void Widget::OnWindowDeactivated() +{ +} + +void Widget::OnWindowDpiScaleChanged() +{ +} diff --git a/src/widgets/lineedit/lineedit.cpp b/src/widgets/lineedit/lineedit.cpp new file mode 100644 index 0000000..e03e185 --- /dev/null +++ b/src/widgets/lineedit/lineedit.cpp @@ -0,0 +1,1184 @@ + +#include "widgets/lineedit/lineedit.h" +#include "core/utf8reader.h" +#include "core/colorf.h" + +LineEdit::LineEdit(Widget* parent) : Widget(parent) +{ + 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::OnPaint(Canvas* canvas) +{ + // To do: draw frame elsewhere, maybe in a OnPaintFrame function? + double w = GetWidth(); + double h = GetHeight(); + Colorf bordercolor(200 / 255.0f, 200 / 255.0f, 200 / 255.0f); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf(1.0f, 1.0f, 1.0f, 1.0f)); + 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); + + 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(153 / 255.0f, 201 / 255.0f, 239 / 255.0f) : Colorf(229 / 255.0f, 235 / 255.0f, 241 / 255.0f)); + } + + // Draw text before selection + if (!txt_before.empty()) + { + canvas->drawText(Point(0.0, canvas->verticalTextAlign().baseline), Colorf(0.0f, 0.0f, 0.0f), txt_before); + } + if (!txt_selected.empty()) + { + canvas->drawText(Point(size_before.width, canvas->verticalTextAlign().baseline), Colorf(0.0f, 0.0f, 0.0f), txt_selected); + } + if (!txt_after.empty()) + { + canvas->drawText(Point(size_before.width + size_selected.width, canvas->verticalTextAlign().baseline), Colorf(0.0f, 0.0f, 0.0f), txt_after); + } + + // draw cursor + if (HasFocus()) + { + if (cursor_blink_visible) + { + Rect cursor_rect = GetCursorRect(); + canvas->fillRect(cursor_rect, Colorf(0.0f, 0.0f, 0.0f)); + } + } +} + +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/src/widgets/mainwindow/mainwindow.cpp b/src/widgets/mainwindow/mainwindow.cpp new file mode 100644 index 0000000..7a98a2c --- /dev/null +++ b/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/src/widgets/menubar/menubar.cpp b/src/widgets/menubar/menubar.cpp new file mode 100644 index 0000000..feb9092 --- /dev/null +++ b/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(0.0f, 0.0f, 0.0f), "File Edit View Tools Window Help"); +} diff --git a/src/widgets/scrollbar/scrollbar.cpp b/src/widgets/scrollbar/scrollbar.cpp new file mode 100644 index 0000000..eea72fe --- /dev/null +++ b/src/widgets/scrollbar/scrollbar.cpp @@ -0,0 +1,400 @@ + +#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; +} + +int Scrollbar::GetMin() const +{ + return scroll_min; +} + +int Scrollbar::GetMax() const +{ + return scroll_max; +} + +int Scrollbar::GetLineStep() const +{ + return line_step; +} + +int Scrollbar::GetPageStep() const +{ + return page_step; +} + +int Scrollbar::GetPosition() const +{ + return position; +} + +void Scrollbar::SetVertical() +{ + vertical = true; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetHorizontal() +{ + vertical = false; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetMin(int new_scroll_min) +{ + SetRanges(new_scroll_min, scroll_max, line_step, page_step); +} + +void Scrollbar::SetMax(int new_scroll_max) +{ + SetRanges(scroll_min, new_scroll_max, line_step, page_step); +} + +void Scrollbar::SetLineStep(int step) +{ + SetRanges(scroll_min, scroll_max, step, page_step); +} + +void Scrollbar::SetPageStep(int step) +{ + SetRanges(scroll_min, scroll_max, line_step, step); +} + +void Scrollbar::SetRanges(int scroll_min, int scroll_max, int line_step, int page_step) +{ + if (scroll_min >= scroll_max || line_step <= 0 || page_step <= 0) + throw std::runtime_error("Scrollbar ranges out of bounds!"); + scroll_min = scroll_min; + scroll_max = scroll_max; + line_step = line_step; + page_step = page_step; + if (position >= scroll_max) + position = scroll_max - 1; + if (position < scroll_min) + position = scroll_min; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetRanges(int view_size, int total_size) +{ + if (view_size <= 0 || total_size <= 0) + { + SetRanges(0, 1, 1, 1); + } + else + { + int scroll_max = std::max(1, total_size - view_size + 1); + int page_step = std::max(1, view_size); + SetRanges(0, scroll_max, 1, page_step); + } +} + +void Scrollbar::SetPosition(int pos) +{ + position = pos; + if (pos >= scroll_max) + position = scroll_max - 1; + if (pos < scroll_min) + position = scroll_min; + + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::OnMouseMove(const Point& pos) +{ + if (mouse_down_mode == mouse_down_thumb_drag) + { + int last_position = position; + + if (pos.x < -100 || pos.x > GetWidth() + 100 || pos.y < -100 || pos.y > GetHeight() + 100) + { + position = thumb_start_position; + } + else + { + int delta = (int)(vertical ? (pos.y - mouse_drag_start_pos.y) : (pos.x - mouse_drag_start_pos.x)); + int position_pixels = thumb_start_pixel_position + delta; + + int track_height = 0; + if (vertical) + track_height = (int)(rect_track_decrement.height + rect_track_increment.height); + else + track_height = (int)(rect_track_decrement.width + rect_track_increment.width); + + if (track_height != 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; + + int last_position = position; + + position -= line_step; + last_step_size = -line_step; + if (position >= scroll_max) + position = scroll_max - 1; + 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; + + int last_position = position; + + position += line_step; + last_step_size = line_step; + if (position >= scroll_max) + position = scroll_max - 1; + 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 = (int)(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; + + int last_position = position; + + position -= page_step; + last_step_size = -page_step; + if (position >= scroll_max) + position = scroll_max - 1; + 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; + + int last_position = position; + + position += page_step; + last_step_size = page_step; + if (position >= scroll_max) + position = scroll_max - 1; + 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); + */ +} + +// Calculates positions of all parts. Returns true if thumb position was changed compared to previously, false otherwise. +bool Scrollbar::UpdatePartPositions() +{ + int total_height = (int)(vertical ? GetHeight() : GetWidth()); + int track_height = std::max(0, total_height - decr_height - incr_height); + int thumb_height = CalculateThumbSize(track_height); + + int thumb_offset = decr_height + CalculateThumbPosition(thumb_height, track_height); + + Rect previous_rect_thumb = rect_thumb; + + rect_button_decrement = CreateRect(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); +} + +int Scrollbar::CalculateThumbSize(int track_size) +{ + int minimum_thumb_size = 20; + int range = scroll_max - scroll_min; + int length = range + page_step - 1; + int 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; +} + +int Scrollbar::CalculateThumbPosition(int thumb_size, int track_size) +{ + int relative_pos = position - scroll_min; + int range = scroll_max - scroll_min - 1; + if (range != 0) + { + int available_area = std::max(0, track_size - thumb_size); + return relative_pos * available_area / range; + } + else + { + return 0; + } +} + +Rect Scrollbar::CreateRect(int start, int 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); + + int 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)(); +} diff --git a/src/widgets/statusbar/statusbar.cpp b/src/widgets/statusbar/statusbar.cpp new file mode 100644 index 0000000..41593af --- /dev/null +++ b/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(0.0f, 0.0f, 0.0f), "Command:"); +} diff --git a/src/widgets/textedit/textedit.cpp b/src/widgets/textedit/textedit.cpp new file mode 100644 index 0000000..37f1da7 --- /dev/null +++ b/src/widgets/textedit/textedit.cpp @@ -0,0 +1,1042 @@ + +#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) +{ + 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() - vert_scrollbar->GetWidth(), + 0.0, + 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()); + else + line.layout.AddText(" ", font, Colorf()); // 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.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::OnPaint(Canvas* canvas) +{ + // To do: draw frame elsewhere, maybe in a OnPaintFrame function? + double w = GetWidth(); + double h = GetHeight(); + Colorf bordercolor(200 / 255.0f, 200 / 255.0f, 200 / 255.0f); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf(1.0f, 1.0f, 1.0f, 1.0f)); + 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); + + 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/src/widgets/toolbar/toolbar.cpp b/src/widgets/toolbar/toolbar.cpp new file mode 100644 index 0000000..d028dd2 --- /dev/null +++ b/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/src/widgets/toolbar/toolbarbutton.cpp b/src/widgets/toolbar/toolbarbutton.cpp new file mode 100644 index 0000000..adb6d08 --- /dev/null +++ b/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/src/window/win32/win32window.cpp b/src/window/win32/win32window.cpp new file mode 100644 index 0000000..fb869ba --- /dev/null +++ b/src/window/win32/win32window.cpp @@ -0,0 +1,447 @@ + +#include "win32window.h" +#include +#include +#include +#include + +#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; + classdesc.lpszClassName = L"ZWidgetWindow"; + classdesc.lpfnWndProc = &Win32Window::WndProc; + RegisterClassEx(&classdesc); + + CreateWindowEx(WS_EX_APPWINDOW | WS_EX_OVERLAPPEDWINDOW, L"ZWidgetWindow", L"", WS_OVERLAPPEDWINDOW, 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::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::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.) +} + +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; +} + +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 = GetDC(WindowHandle); + 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) +{ + 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) + { + ValidateRect(WindowHandle, nullptr); + WindowHost->OnWindowPaint(); + } + 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 + { + SetCursor((HCURSOR)LoadImage(0, IDC_ARROW, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_DEFAULTSIZE, LR_SHARED)); + } + + WindowHost->OnWindowMouseMove(GetLParamPos(lparam)); + } + else if (msg == WM_LBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(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_MBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_MiddleMouse); + } + else if (msg == WM_RBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(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; + WindowHost->OnWindowMouseWheel(GetLParamPos(lparam), 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 DefWindowProc(WindowHandle, msg, wparam, lparam); +} + +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; +} + +std::list Win32Window::Windows; +bool Win32Window::ExitRunLoop; diff --git a/src/window/win32/win32window.h b/src/window/win32/win32window.h new file mode 100644 index 0000000..39ba0e5 --- /dev/null +++ b/src/window/win32/win32window.h @@ -0,0 +1,60 @@ +#pragma once + +#define NOMINMAX +#define WIN32_MEAN_AND_LEAN +#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 Update() override; + bool GetKeyState(EInputKey key) 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; + + Point GetLParamPos(LPARAM lparam) const; + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + + static bool ExitRunLoop; + static std::list Windows; + std::list::iterator WindowsIterator; + + 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 = {}; +}; diff --git a/src/window/window.cpp b/src/window/window.cpp new file mode 100644 index 0000000..e6f82ac --- /dev/null +++ b/src/window/window.cpp @@ -0,0 +1,50 @@ + +#include "window/window.h" + +#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(); +} + +#else + +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"); +} + +#endif