diff --git a/libraries/ZWidget/.github/workflows/build.yaml b/libraries/ZWidget/.github/workflows/build.yaml new file mode 100644 index 000000000..396970061 --- /dev/null +++ b/libraries/ZWidget/.github/workflows/build.yaml @@ -0,0 +1,93 @@ +name: build + +on: + push: + pull_request: + +env: + BUILD_TYPE: Release + +jobs: + linux: + runs-on: ubuntu-latest + name: 🐧 Ubuntu x64 + steps: + - name: 🧰 Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: true + + - name: ⬇️ Install dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get -qq update + sudo apt-get -qq install \ + build-essential \ + libsdl2-dev \ + libsdl2-mixer-dev + - 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 + + 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/libraries/ZWidget/.gitignore b/libraries/ZWidget/.gitignore new file mode 100644 index 000000000..6c41b1f58 --- /dev/null +++ b/libraries/ZWidget/.gitignore @@ -0,0 +1,6 @@ +# build artifacts +/build + +# misc cache/backup/swap files +.cache +*~ \ No newline at end of file diff --git a/libraries/ZWidget/CMakeLists.txt b/libraries/ZWidget/CMakeLists.txt new file mode 100644 index 000000000..894da17e2 --- /dev/null +++ b/libraries/ZWidget/CMakeLists.txt @@ -0,0 +1,305 @@ +cmake_minimum_required(VERSION 3.11) +project(zwidget) + +if (UNIX AND NOT APPLE) + include(FindPkgConfig) + pkg_check_modules(DBUS dbus-1) + if (!DBUS_FOUND) + pkg_check_modules(DBUS REQUIRED dbus) # Fedora Linux looks for dbus instead + endif() + pkg_check_modules(WAYLANDPP + wayland-client + wayland-client++ + wayland-client-extra++ + wayland-client-unstable++ + wayland-cursor++ + xkbcommon>=0.5.0 + ) +endif() + +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/theme.cpp + src/core/utf8reader.cpp + src/core/pathfill.cpp + src/core/truetypefont.cpp + src/core/truetypefont.h + src/core/picopng/picopng.cpp + src/core/picopng/picopng.h + src/core/nanosvg/nanosvg.cpp + src/core/nanosvg/nanosvg.h + src/core/nanosvg/nanosvgrast.h + src/widgets/lineedit/lineedit.cpp + src/widgets/mainwindow/mainwindow.cpp + src/widgets/menubar/menubar.cpp + src/widgets/scrollbar/scrollbar.cpp + src/widgets/statusbar/statusbar.cpp + src/widgets/textedit/textedit.cpp + src/widgets/toolbar/toolbar.cpp + src/widgets/toolbar/toolbarbutton.cpp + src/widgets/imagebox/imagebox.cpp + src/widgets/textlabel/textlabel.cpp + src/widgets/pushbutton/pushbutton.cpp + src/widgets/checkboxlabel/checkboxlabel.cpp + src/widgets/listview/listview.cpp + src/widgets/tabwidget/tabwidget.cpp + src/window/window.cpp + src/window/stub/stub_open_folder_dialog.cpp + src/window/stub/stub_open_folder_dialog.h + src/window/stub/stub_open_file_dialog.cpp + src/window/stub/stub_open_file_dialog.h + src/window/stub/stub_save_file_dialog.cpp + src/window/stub/stub_save_file_dialog.h + src/window/ztimer/ztimer.h + src/window/ztimer/ztimer.cpp + src/systemdialogs/open_folder_dialog.cpp + src/systemdialogs/open_file_dialog.cpp + src/systemdialogs/save_file_dialog.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/pathfill.h + include/zwidget/core/span_layout.h + include/zwidget/core/timer.h + include/zwidget/core/widget.h + include/zwidget/core/theme.h + include/zwidget/core/utf8reader.h + include/zwidget/core/resourcedata.h + include/zwidget/widgets/lineedit/lineedit.h + include/zwidget/widgets/mainwindow/mainwindow.h + include/zwidget/widgets/menubar/menubar.h + include/zwidget/widgets/scrollbar/scrollbar.h + include/zwidget/widgets/statusbar/statusbar.h + include/zwidget/widgets/textedit/textedit.h + include/zwidget/widgets/toolbar/toolbar.h + include/zwidget/widgets/toolbar/toolbarbutton.h + include/zwidget/widgets/imagebox/imagebox.h + include/zwidget/widgets/textlabel/textlabel.h + include/zwidget/widgets/pushbutton/pushbutton.h + include/zwidget/widgets/checkboxlabel/checkboxlabel.h + include/zwidget/widgets/listview/listview.h + include/zwidget/widgets/tabwidget/tabwidget.h + include/zwidget/window/window.h + include/zwidget/window/x11nativehandle.h + include/zwidget/window/waylandnativehandle.h + include/zwidget/window/win32nativehandle.h + include/zwidget/window/sdl2nativehandle.h + include/zwidget/systemdialogs/open_folder_dialog.h + include/zwidget/systemdialogs/open_file_dialog.h + include/zwidget/systemdialogs/save_file_dialog.h +) + +set(ZWIDGET_WIN32_SOURCES + src/window/win32/win32_display_backend.cpp + src/window/win32/win32_display_backend.h + src/window/win32/win32_display_window.cpp + src/window/win32/win32_display_window.h + src/window/win32/win32_open_folder_dialog.cpp + src/window/win32/win32_open_folder_dialog.h + src/window/win32/win32_open_file_dialog.cpp + src/window/win32/win32_open_file_dialog.h + src/window/win32/win32_save_file_dialog.cpp + src/window/win32/win32_save_file_dialog.h + src/window/win32/win32_util.h +) + +set(ZWIDGET_DBUS_SOURCES + src/window/dbus/dbus_open_folder_dialog.cpp + src/window/dbus/dbus_open_folder_dialog.h + src/window/dbus/dbus_open_file_dialog.cpp + src/window/dbus/dbus_open_file_dialog.h + src/window/dbus/dbus_save_file_dialog.cpp + src/window/dbus/dbus_save_file_dialog.h +) + +set(ZWIDGET_COCOA_SOURCES +) + +set(ZWIDGET_SDL2_SOURCES + src/window/sdl2/sdl2_display_backend.cpp + src/window/sdl2/sdl2_display_backend.h + src/window/sdl2/sdl2_display_window.cpp + src/window/sdl2/sdl2_display_window.h +) + +set(ZWIDGET_X11_SOURCES + src/window/x11/x11_display_backend.cpp + src/window/x11/x11_display_backend.h + src/window/x11/x11_display_window.cpp + src/window/x11/x11_display_window.h +) + +set(ZWIDGET_WAYLAND_SOURCES + src/window/wayland/wayland_display_backend.cpp + src/window/wayland/wayland_display_backend.h + src/window/wayland/wayland_display_window.cpp + src/window/wayland/wayland_display_window.h + src/window/wayland/wl_fractional_scaling_protocol.cpp + src/window/wayland/wl_fractional_scaling_protocol.hpp +) + +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\\picopng" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/core/picopng/.+") +source_group("src\\core\\nanosvg" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/core/nanosvg/.+") +source_group("src\\widgets" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/.+") +source_group("src\\widgets\\lineedit" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/lineedit/.+") +source_group("src\\widgets\\mainwindow" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/mainwindow/.+") +source_group("src\\widgets\\menubar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/menubar/.+") +source_group("src\\widgets\\scrollbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/scrollbar/.+") +source_group("src\\widgets\\statusbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/statusbar/.+") +source_group("src\\widgets\\textedit" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/textedit/.+") +source_group("src\\widgets\\toolbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/toolbar/.+") +source_group("src\\widgets\\imagebox" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/imagebox/.+") +source_group("src\\widgets\\textlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/textlabel/.+") +source_group("src\\widgets\\pushbutton" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/pushbutton/.+") +source_group("src\\widgets\\checkboxlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/checkboxlabel/.+") +source_group("src\\widgets\\listview" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/listview/.+") +source_group("src\\widgets\\tabwidget" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/tabwidget/.+") +source_group("src\\window" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/.+") +source_group("src\\window\\stub" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/stub/.+") +source_group("src\\window\\win32" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/win32/.+") +source_group("src\\window\\sdl2" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/sdl2/.+") +source_group("src\\window\\x11" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/x11/.+") +source_group("src\\window\\wayland" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/wayland/.+") +source_group("src\\window\\dbus" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/dbus/.+") +source_group("src\\systemdialogs" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/systemdialogs/.+") +source_group("include" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/.+") +source_group("include\\core" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/core/.+") +source_group("include\\widgets" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/.+") +source_group("include\\widgets\\lineedit" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/lineedit/.+") +source_group("include\\widgets\\mainwindow" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/mainwindow/.+") +source_group("include\\widgets\\menubar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/menubar/.+") +source_group("include\\widgets\\scrollbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/scrollbar/.+") +source_group("include\\widgets\\statusbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/statusbar/.+") +source_group("include\\widgets\\textedit" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/textedit/.+") +source_group("include\\widgets\\toolbar" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/toolbar/.+") +source_group("include\\widgets\\imagebox" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/imagebox/.+") +source_group("include\\widgets\\textlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/textlabel/.+") +source_group("include\\widgets\\pushbutton" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/pushbutton/.+") +source_group("include\\widgets\\checkboxlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/checkboxlabel/.+") +source_group("include\\widgets\\listview" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/listview/.+") +source_group("include\\widgets\\tabwidget" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/tabwidget/.+") +source_group("include\\window" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/.+") +source_group("include\\systemdialogs" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/systemdialogs/.+") + +# Include directory to external projects using zwidget +include_directories(include) + +# Internal include dirs for building zwidget +set(ZWIDGET_INCLUDE_DIRS include/zwidget src) + +set(ZWIDGET_COMPILE_OPTIONS) + +if(WIN32) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_WIN32_SOURCES}) + set(ZWIDGET_DEFINES -DUNICODE -D_UNICODE) + set(ZWIDGET_LINK_OPTIONS) + if(MSVC) + # Use all cores for compilation + set(ZWIDGET_COMPILE_OPTIONS ${ZWIDGET_COMPILE_OPTIONS} /MP) + + # Ignore specific warnings + #set(ZWIDGET_COMPILE_OPTIONS ${ZWIDGET_COMPILE_OPTIONS} /wd4244 /wd4267 /wd4005 /wd4018) + + # Don't slow down std containers in debug builds + set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES}) + + # Ignore warning about legacy CRT functions + #set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -D_CRT_SECURE_NO_WARNINGS) + endif() +elseif(APPLE) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_COCOA_SOURCES}) + set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl) + set(ZWIDGET_DEFINES -DUNIX -D_UNIX) + set(ZWIDGET_LINK_OPTIONS -pthread) +else() + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES} ${ZWIDGET_X11_SOURCES}) + set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl -lX11) + set(ZWIDGET_DEFINES -DUNIX -D_UNIX -DUSE_SDL2 -DUSE_X11) + set(ZWIDGET_LINK_OPTIONS -pthread) + if (DBUS_FOUND) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_DBUS_SOURCES}) + set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${DBUS_INCLUDE_DIRS}) + set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${DBUS_LDFLAGS}) + set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_DBUS) + endif() + if (WAYLANDPP_FOUND) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_WAYLAND_SOURCES}) + set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${WAYLANDPP_INCLUDE_DIRS}) + set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${WAYLANDPP_LDFLAGS}) + set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_WAYLAND) + endif() +endif() + +if(MSVC) + set(CXX_WARNING_FLAGS /W3) +else() + set(CXX_WARNING_FLAGS -Wall -Wpedantic) +endif() + +add_library(zwidget STATIC ${ZWIDGET_SOURCES} ${ZWIDGET_INCLUDES}) +target_compile_options(zwidget PRIVATE ${ZWIDGET_COMPILE_OPTIONS}) +target_compile_definitions(zwidget PRIVATE ${ZWIDGET_DEFINES}) +target_include_directories(zwidget PRIVATE ${ZWIDGET_INCLUDE_DIRS}) +target_link_options(zwidget PRIVATE ${ZWIDGET_LINK_OPTIONS}) +target_link_libraries(zwidget ${ZWIDGET_LIBS}) +set_target_properties(zwidget PROPERTIES CXX_STANDARD 17) +target_compile_options(zwidget PRIVATE ${CXX_WARNING_FLAGS}) + +if(MSVC) + set_property(TARGET zwidget PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() + +option(ZWIDGET_BUILD_EXAMPLE "Build the zwidget example application" ON) + +if(ZWIDGET_BUILD_EXAMPLE) + if (UNIX AND NOT APPLE) + find_package(SDL2 REQUIRED) + endif() + + add_executable(zwidget_example WIN32 + example/example.cpp + example/picopng.cpp + example/picopng.h + ) + target_compile_options(zwidget_example PRIVATE ${CXX_WARNING_FLAGS}) + + add_custom_command( + TARGET zwidget_example POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/example/banner.png" + "${CMAKE_CURRENT_BINARY_DIR}/banner.png" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/example/OpenSans.ttf" + "${CMAKE_CURRENT_BINARY_DIR}/OpenSans.ttf" + ) + + target_include_directories(zwidget_example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/example) + target_link_libraries(zwidget_example PRIVATE zwidget) + + if(WIN32) + target_compile_definitions(zwidget_example PRIVATE UNICODE _UNICODE) + target_link_libraries(zwidget_example PRIVATE gdi32 user32 shell32 comdlg32) + elseif(APPLE) + target_link_libraries(zwidget_example PRIVATE "-framework Cocoa -framework OpenGL") + else() + target_link_libraries(zwidget_example PRIVATE ${ZWIDGET_LIBS} SDL2::SDL2) + endif() + + set_target_properties(zwidget_example PROPERTIES CXX_STANDARD 17) + + if(MSVC) + set_property(TARGET zwidget_example PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + endif() +endif() diff --git a/libraries/ZWidget/LICENSE.md b/libraries/ZWidget/LICENSE.md new file mode 100644 index 000000000..fdc06221c --- /dev/null +++ b/libraries/ZWidget/LICENSE.md @@ -0,0 +1,21 @@ +# License information + +## License for ZWidget itself + + // Copyright (c) 2023 Magnus Norddahl + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. diff --git a/libraries/ZWidget/README.md b/libraries/ZWidget/README.md new file mode 100644 index 000000000..c3bb8e219 --- /dev/null +++ b/libraries/ZWidget/README.md @@ -0,0 +1,2 @@ +# ZWidget +A framework for building user interface applications diff --git a/libraries/ZWidget/example/OpenSans.ttf b/libraries/ZWidget/example/OpenSans.ttf new file mode 100644 index 000000000..9db85693b Binary files /dev/null and b/libraries/ZWidget/example/OpenSans.ttf differ diff --git a/libraries/ZWidget/example/banner.png b/libraries/ZWidget/example/banner.png new file mode 100644 index 000000000..f9beb9d89 Binary files /dev/null and b/libraries/ZWidget/example/banner.png differ diff --git a/libraries/ZWidget/example/example.cpp b/libraries/ZWidget/example/example.cpp new file mode 100644 index 000000000..ec4eed91b --- /dev/null +++ b/libraries/ZWidget/example/example.cpp @@ -0,0 +1,283 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "picopng.h" + +static std::vector ReadAllBytes(const std::string& filename); + +class LauncherWindow : public Widget +{ +public: + LauncherWindow() : Widget(nullptr, WidgetType::Window) + { + Logo = new ImageBox(this); + WelcomeLabel = new TextLabel(this); + VersionLabel = new TextLabel(this); + SelectLabel = new TextLabel(this); + GeneralLabel = new TextLabel(this); + ExtrasLabel = new TextLabel(this); + FullscreenCheckbox = new CheckboxLabel(this); + DisableAutoloadCheckbox = new CheckboxLabel(this); + DontAskAgainCheckbox = new CheckboxLabel(this); + LightsCheckbox = new CheckboxLabel(this); + BrightmapsCheckbox = new CheckboxLabel(this); + WidescreenCheckbox = new CheckboxLabel(this); + PlayButton = new PushButton(this); + ExitButton = new PushButton(this); + GamesList = new ListView(this); + + SetWindowBackground(Colorf::fromRgba8(51, 51, 51)); + SetWindowBorderColor(Colorf::fromRgba8(51, 51, 51)); + SetWindowCaptionColor(Colorf::fromRgba8(33, 33, 33)); + SetWindowCaptionTextColor(Colorf::fromRgba8(226, 223, 219)); + SetWindowTitle("VKDoom Launcher"); + + WelcomeLabel->SetText("Welcome to VKDoom"); + VersionLabel->SetText("Version 0xdeadbabe."); + SelectLabel->SetText("Select which game file (IWAD) to run."); + PlayButton->SetText("Play Game"); + ExitButton->SetText("Exit"); + + ExitButton->OnClick = []{ + DisplayWindow::ExitLoop(); + }; + + GeneralLabel->SetText("General"); + ExtrasLabel->SetText("Extra Graphics"); + FullscreenCheckbox->SetText("Fullscreen"); + DisableAutoloadCheckbox->SetText("Disable autoload"); + DontAskAgainCheckbox->SetText("Don't ask me again"); + LightsCheckbox->SetText("Lights"); + BrightmapsCheckbox->SetText("Brightmaps"); + WidescreenCheckbox->SetText("Widescreen"); + + try + { + auto filedata = ReadAllBytes("banner.png"); + std::vector pixels; + unsigned long width = 0, height = 0; + int result = decodePNG(pixels, width, height, (const unsigned char*)filedata.data(), filedata.size(), true); + if (result == 0) + { + Logo->SetImage(Image::Create(width, height, ImageFormat::R8G8B8A8, pixels.data())); + } + } + catch (...) + { + } + } + + void OnGeometryChanged() override + { + double y = 0.0; + + Logo->SetFrameGeometry(0.0, y, GetWidth(), Logo->GetPreferredHeight()); + y += Logo->GetPreferredHeight(); + + y += 10.0; + + WelcomeLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0, WelcomeLabel->GetPreferredHeight()); + y += WelcomeLabel->GetPreferredHeight(); + + VersionLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0, VersionLabel->GetPreferredHeight()); + y += VersionLabel->GetPreferredHeight(); + + y += 10.0; + + SelectLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0, SelectLabel->GetPreferredHeight()); + y += SelectLabel->GetPreferredHeight(); + + double listViewTop = y + 10.0; + + y = GetHeight() - 15.0 - PlayButton->GetPreferredHeight(); + PlayButton->SetFrameGeometry(20.0, y, 120.0, PlayButton->GetPreferredHeight()); + ExitButton->SetFrameGeometry(GetWidth() - 20.0 - 120.0, y, 120.0, PlayButton->GetPreferredHeight()); + + y -= 20.0; + + double panelWidth = 150.0; + y -= DontAskAgainCheckbox->GetPreferredHeight(); + DontAskAgainCheckbox->SetFrameGeometry(20.0, y, 190.0, DontAskAgainCheckbox->GetPreferredHeight()); + WidescreenCheckbox->SetFrameGeometry(GetWidth() - 20.0 - panelWidth, y, panelWidth, WidescreenCheckbox->GetPreferredHeight()); + + y -= DisableAutoloadCheckbox->GetPreferredHeight(); + DisableAutoloadCheckbox->SetFrameGeometry(20.0, y, 190.0, DisableAutoloadCheckbox->GetPreferredHeight()); + BrightmapsCheckbox->SetFrameGeometry(GetWidth() - 20.0 - panelWidth, y, panelWidth, BrightmapsCheckbox->GetPreferredHeight()); + + y -= FullscreenCheckbox->GetPreferredHeight(); + FullscreenCheckbox->SetFrameGeometry(20.0, y, 190.0, FullscreenCheckbox->GetPreferredHeight()); + LightsCheckbox->SetFrameGeometry(GetWidth() - 20.0 - panelWidth, y, panelWidth, LightsCheckbox->GetPreferredHeight()); + + y -= GeneralLabel->GetPreferredHeight(); + GeneralLabel->SetFrameGeometry(20.0, y, 190.0, GeneralLabel->GetPreferredHeight()); + ExtrasLabel->SetFrameGeometry(GetWidth() - 20.0 - panelWidth, y, panelWidth, ExtrasLabel->GetPreferredHeight()); + + double listViewBottom = y - 10.0; + GamesList->SetFrameGeometry(20.0, listViewTop, GetWidth() - 40.0, std::max(listViewBottom - listViewTop, 0.0)); + } + + ImageBox* Logo = nullptr; + TextLabel* WelcomeLabel = nullptr; + TextLabel* VersionLabel = nullptr; + TextLabel* SelectLabel = nullptr; + TextLabel* GeneralLabel = nullptr; + TextLabel* ExtrasLabel = nullptr; + CheckboxLabel* FullscreenCheckbox = nullptr; + CheckboxLabel* DisableAutoloadCheckbox = nullptr; + CheckboxLabel* DontAskAgainCheckbox = nullptr; + CheckboxLabel* LightsCheckbox = nullptr; + CheckboxLabel* BrightmapsCheckbox = nullptr; + CheckboxLabel* WidescreenCheckbox = nullptr; + PushButton* PlayButton = nullptr; + PushButton* ExitButton = nullptr; + ListView* GamesList = nullptr; +}; + +static std::vector ReadAllBytes(const std::string& filename); + +std::vector LoadWidgetFontData(const std::string& name) +{ + return { + {std::move(ReadAllBytes("OpenSans.ttf")), ""} + }; +} + +std::vector LoadWidgetData(const std::string& name) +{ + return ReadAllBytes(name); +} + +int example() +{ + WidgetTheme::SetTheme(std::make_unique()); + + // DisplayBackend::Set(DisplayBackend::TryCreateWin32()); + // DisplayBackend::Set(DisplayBackend::TryCreateSDL2()); + DisplayBackend::Set(DisplayBackend::TryCreateBackend()); + +#if 1 + auto launcher = new LauncherWindow(); + launcher->SetFrameGeometry(100.0, 100.0, 615.0, 668.0); + launcher->Show(); +#else + 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(); +#endif + + DisplayWindow::RunLoop(); + + return 0; +} + +#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; +} + + +int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow) +{ + example(); +} + +#else + +#include +#include +#include +#include + +static std::vector ReadAllBytes(const std::string& filename) +{ + std::ifstream file(filename, std::ios::binary | std::ios::ate); + if (!file) + throw std::runtime_error("ReadFile failed"); + + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector buffer(size); + if (!file.read(reinterpret_cast(buffer.data()), size)) + throw std::runtime_error("ReadFile failed "); + + return buffer; +} + +int main() +{ + example(); +} + +#endif diff --git a/libraries/ZWidget/example/picopng.cpp b/libraries/ZWidget/example/picopng.cpp new file mode 100644 index 000000000..c2943eb14 --- /dev/null +++ b/libraries/ZWidget/example/picopng.cpp @@ -0,0 +1,593 @@ +#include + +/* +decodePNG: The picoPNG function, decodes a PNG file buffer in memory, into a raw pixel buffer. +out_image: output parameter, this will contain the raw pixels after decoding. + By default the output is 32-bit RGBA color. + The std::vector is automatically resized to the correct size. +image_width: output_parameter, this will contain the width of the image in pixels. +image_height: output_parameter, this will contain the height of the image in pixels. +in_png: pointer to the buffer of the PNG file in memory. To get it from a file on + disk, load it and store it in a memory buffer yourself first. +in_size: size of the input PNG file in bytes. +convert_to_rgba32: optional parameter, true by default. + Set to true to get the output in RGBA 32-bit (8 bit per channel) color format + no matter what color type the original PNG image had. This gives predictable, + useable data from any random input PNG. + Set to false to do no color conversion at all. The result then has the same data + type as the PNG image, which can range from 1 bit to 64 bits per pixel. + Information about the color type or palette colors are not provided. You need + to know this information yourself to be able to use the data so this only + works for trusted PNG files. Use LodePNG instead of picoPNG if you need this information. +return: 0 if success, not 0 if some error occured. +*/ +int decodePNG(std::vector& out_image, unsigned long& image_width, unsigned long& image_height, const unsigned char* in_png, size_t in_size, bool convert_to_rgba32 = true) +{ + // picoPNG version 20101224 + // Copyright (c) 2005-2010 Lode Vandevenne + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + // picoPNG is a PNG decoder in one C++ function of around 500 lines. Use picoPNG for + // programs that need only 1 .cpp file. Since it's a single function, it's very limited, + // it can convert a PNG to raw pixel data either converted to 32-bit RGBA color or + // with no color conversion at all. For anything more complex, another tiny library + // is available: LodePNG (lodepng.c(pp)), which is a single source and header file. + // Apologies for the compact code style, it's to make this tiny. + + static const unsigned long LENBASE[29] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258}; + static const unsigned long LENEXTRA[29] = {0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; + static const unsigned long DISTBASE[30] = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577}; + static const unsigned long DISTEXTRA[30] = {0,0,0,0,1,1,2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; + static const unsigned long CLCL[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; //code length code lengths + struct Zlib //nested functions for zlib decompression + { + static unsigned long readBitFromStream(size_t& bitp, const unsigned char* bits) { unsigned long result = (bits[bitp >> 3] >> (bitp & 0x7)) & 1; bitp++; return result;} + static unsigned long readBitsFromStream(size_t& bitp, const unsigned char* bits, size_t nbits) + { + unsigned long result = 0; + for(size_t i = 0; i < nbits; i++) result += (readBitFromStream(bitp, bits)) << i; + return result; + } + struct HuffmanTree + { + int makeFromLengths(const std::vector& bitlen, unsigned long maxbitlen) + { //make tree given the lengths + unsigned long numcodes = (unsigned long)(bitlen.size()), treepos = 0, nodefilled = 0; + std::vector tree1d(numcodes), blcount(maxbitlen + 1, 0), nextcode(maxbitlen + 1, 0); + for(unsigned long bits = 0; bits < numcodes; bits++) blcount[bitlen[bits]]++; //count number of instances of each code length + for(unsigned long bits = 1; bits <= maxbitlen; bits++) nextcode[bits] = (nextcode[bits - 1] + blcount[bits - 1]) << 1; + for(unsigned long n = 0; n < numcodes; n++) if(bitlen[n] != 0) tree1d[n] = nextcode[bitlen[n]]++; //generate all the codes + tree2d.clear(); tree2d.resize(numcodes * 2, 32767); //32767 here means the tree2d isn't filled there yet + for(unsigned long n = 0; n < numcodes; n++) //the codes + for(unsigned long i = 0; i < bitlen[n]; i++) //the bits for this code + { + unsigned long bit = (tree1d[n] >> (bitlen[n] - i - 1)) & 1; + if(treepos > numcodes - 2) return 55; + if(tree2d[2 * treepos + bit] == 32767) //not yet filled in + { + if(i + 1 == bitlen[n]) { tree2d[2 * treepos + bit] = n; treepos = 0; } //last bit + else { tree2d[2 * treepos + bit] = ++nodefilled + numcodes; treepos = nodefilled; } //addresses are encoded as values > numcodes + } + else treepos = tree2d[2 * treepos + bit] - numcodes; //subtract numcodes from address to get address value + } + return 0; + } + int decode(bool& decoded, unsigned long& result, size_t& treepos, unsigned long bit) const + { //Decodes a symbol from the tree + unsigned long numcodes = (unsigned long)tree2d.size() / 2; + if(treepos >= numcodes) return 11; //error: you appeared outside the codetree + result = tree2d[2 * treepos + bit]; + decoded = (result < numcodes); + treepos = decoded ? 0 : result - numcodes; + return 0; + } + std::vector tree2d; //2D representation of a huffman tree: The one dimension is "0" or "1", the other contains all nodes and leaves of the tree. + }; + struct Inflator + { + int error; + void inflate(std::vector& out, const std::vector& in, size_t inpos = 0) + { + size_t bp = 0, pos = 0; //bit pointer and byte pointer + error = 0; + unsigned long BFINAL = 0; + while(!BFINAL && !error) + { + if(bp >> 3 >= in.size()) { error = 52; return; } //error, bit pointer will jump past memory + BFINAL = readBitFromStream(bp, &in[inpos]); + unsigned long BTYPE = readBitFromStream(bp, &in[inpos]); BTYPE += 2 * readBitFromStream(bp, &in[inpos]); + if(BTYPE == 3) { error = 20; return; } //error: invalid BTYPE + else if(BTYPE == 0) inflateNoCompression(out, &in[inpos], bp, pos, in.size()); + else inflateHuffmanBlock(out, &in[inpos], bp, pos, in.size(), BTYPE); + } + if(!error) out.resize(pos); //Only now we know the true size of out, resize it to that + } + void generateFixedTrees(HuffmanTree& tree, HuffmanTree& treeD) //get the tree of a deflated block with fixed tree + { + std::vector bitlen(288, 8), bitlenD(32, 5);; + for(size_t i = 144; i <= 255; i++) bitlen[i] = 9; + for(size_t i = 256; i <= 279; i++) bitlen[i] = 7; + tree.makeFromLengths(bitlen, 15); + treeD.makeFromLengths(bitlenD, 15); + } + HuffmanTree codetree, codetreeD, codelengthcodetree; //the code tree for Huffman codes, dist codes, and code length codes + unsigned long huffmanDecodeSymbol(const unsigned char* in, size_t& bp, const HuffmanTree& codetree, size_t inlength) + { //decode a single symbol from given list of bits with given code tree. return value is the symbol + bool decoded; unsigned long ct; + for(size_t treepos = 0;;) + { + if((bp & 0x07) == 0 && (bp >> 3) > inlength) { error = 10; return 0; } //error: end reached without endcode + error = codetree.decode(decoded, ct, treepos, readBitFromStream(bp, in)); if(error) return 0; //stop, an error happened + if(decoded) return ct; + } + } + void getTreeInflateDynamic(HuffmanTree& tree, HuffmanTree& treeD, const unsigned char* in, size_t& bp, size_t inlength) + { //get the tree of a deflated block with dynamic tree, the tree itself is also Huffman compressed with a known tree + std::vector bitlen(288, 0), bitlenD(32, 0); + if(bp >> 3 >= inlength - 2) { error = 49; return; } //the bit pointer is or will go past the memory + size_t HLIT = readBitsFromStream(bp, in, 5) + 257; //number of literal/length codes + 257 + size_t HDIST = readBitsFromStream(bp, in, 5) + 1; //number of dist codes + 1 + size_t HCLEN = readBitsFromStream(bp, in, 4) + 4; //number of code length codes + 4 + std::vector codelengthcode(19); //lengths of tree to decode the lengths of the dynamic tree + for(size_t i = 0; i < 19; i++) codelengthcode[CLCL[i]] = (i < HCLEN) ? readBitsFromStream(bp, in, 3) : 0; + error = codelengthcodetree.makeFromLengths(codelengthcode, 7); if(error) return; + size_t i = 0, replength; + while(i < HLIT + HDIST) + { + unsigned long code = huffmanDecodeSymbol(in, bp, codelengthcodetree, inlength); if(error) return; + if(code <= 15) { if(i < HLIT) bitlen[i++] = code; else bitlenD[i++ - HLIT] = code; } //a length code + else if(code == 16) //repeat previous + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 3 + readBitsFromStream(bp, in, 2); + unsigned long value; //set value to the previous code + if((i - 1) < HLIT) value = bitlen[i - 1]; + else value = bitlenD[i - HLIT - 1]; + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 13; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = value; else bitlenD[i++ - HLIT] = value; + } + } + else if(code == 17) //repeat "0" 3-10 times + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 3 + readBitsFromStream(bp, in, 3); + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 14; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = 0; else bitlenD[i++ - HLIT] = 0; + } + } + else if(code == 18) //repeat "0" 11-138 times + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 11 + readBitsFromStream(bp, in, 7); + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 15; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = 0; else bitlenD[i++ - HLIT] = 0; + } + } + else { error = 16; return; } //error: somehow an unexisting code appeared. This can never happen. + } + if(bitlen[256] == 0) { error = 64; return; } //the length of the end code 256 must be larger than 0 + error = tree.makeFromLengths(bitlen, 15); if(error) return; //now we've finally got HLIT and HDIST, so generate the code trees, and the function is done + error = treeD.makeFromLengths(bitlenD, 15); if(error) return; + } + void inflateHuffmanBlock(std::vector& out, const unsigned char* in, size_t& bp, size_t& pos, size_t inlength, unsigned long btype) + { + if(btype == 1) { generateFixedTrees(codetree, codetreeD); } + else if(btype == 2) { getTreeInflateDynamic(codetree, codetreeD, in, bp, inlength); if(error) return; } + for(;;) + { + unsigned long code = huffmanDecodeSymbol(in, bp, codetree, inlength); if(error) return; + if(code == 256) return; //end code + else if(code <= 255) //literal symbol + { + if(pos >= out.size()) out.resize((pos + 1) * 2); //reserve more room + out[pos++] = (unsigned char)(code); + } + else if(code >= 257 && code <= 285) //length code + { + size_t length = LENBASE[code - 257], numextrabits = LENEXTRA[code - 257]; + if((bp >> 3) >= inlength) { error = 51; return; } //error, bit pointer will jump past memory + length += readBitsFromStream(bp, in, numextrabits); + unsigned long codeD = huffmanDecodeSymbol(in, bp, codetreeD, inlength); if(error) return; + if(codeD > 29) { error = 18; return; } //error: invalid dist code (30-31 are never used) + unsigned long dist = DISTBASE[codeD], numextrabitsD = DISTEXTRA[codeD]; + if((bp >> 3) >= inlength) { error = 51; return; } //error, bit pointer will jump past memory + dist += readBitsFromStream(bp, in, numextrabitsD); + size_t start = pos, back = start - dist; //backwards + if(pos + length >= out.size()) out.resize((pos + length) * 2); //reserve more room + for(size_t i = 0; i < length; i++) { out[pos++] = out[back++]; if(back >= start) back = start - dist; } + } + } + } + void inflateNoCompression(std::vector& out, const unsigned char* in, size_t& bp, size_t& pos, size_t inlength) + { + while((bp & 0x7) != 0) bp++; //go to first boundary of byte + size_t p = bp / 8; + if(p >= inlength - 4) { error = 52; return; } //error, bit pointer will jump past memory + unsigned long LEN = in[p] + 256 * in[p + 1], NLEN = in[p + 2] + 256 * in[p + 3]; p += 4; + if(LEN + NLEN != 65535) { error = 21; return; } //error: NLEN is not one's complement of LEN + if(pos + LEN >= out.size()) out.resize(pos + LEN); + if(p + LEN > inlength) { error = 23; return; } //error: reading outside of in buffer + for(unsigned long n = 0; n < LEN; n++) out[pos++] = in[p++]; //read LEN bytes of literal data + bp = p * 8; + } + }; + int decompress(std::vector& out, const std::vector& in) //returns error value + { + Inflator inflator; + if(in.size() < 2) { return 53; } //error, size of zlib data too small + if((in[0] * 256 + in[1]) % 31 != 0) { return 24; } //error: 256 * in[0] + in[1] must be a multiple of 31, the FCHECK value is supposed to be made that way + unsigned long CM = in[0] & 15, CINFO = (in[0] >> 4) & 15, FDICT = (in[1] >> 5) & 1; + if(CM != 8 || CINFO > 7) { return 25; } //error: only compression method 8: inflate with sliding window of 32k is supported by the PNG spec + if(FDICT != 0) { return 26; } //error: the specification of PNG says about the zlib stream: "The additional flags shall not specify a preset dictionary." + inflator.inflate(out, in, 2); + return inflator.error; //note: adler32 checksum was skipped and ignored + } + }; + struct PNG //nested functions for PNG decoding + { + struct Info + { + unsigned long width, height, colorType, bitDepth, compressionMethod, filterMethod, interlaceMethod, key_r, key_g, key_b; + bool key_defined; //is a transparent color key given? + std::vector palette; + } info; + int error; + void decode(std::vector& out, const unsigned char* in, size_t size, bool convert_to_rgba32) + { + error = 0; + if(size == 0 || in == 0) { error = 48; return; } //the given data is empty + readPngHeader(&in[0], size); if(error) return; + size_t pos = 33; //first byte of the first chunk after the header + std::vector idat; //the data from idat chunks + bool IEND = false, known_type = true; + info.key_defined = false; + while(!IEND) //loop through the chunks, ignoring unknown chunks and stopping at IEND chunk. IDAT data is put at the start of the in buffer + { + if(pos + 8 >= size) { error = 30; return; } //error: size of the in buffer too small to contain next chunk + size_t chunkLength = read32bitInt(&in[pos]); pos += 4; + if(chunkLength > 2147483647) { error = 63; return; } + if(pos + chunkLength >= size) { error = 35; return; } //error: size of the in buffer too small to contain next chunk + if(in[pos + 0] == 'I' && in[pos + 1] == 'D' && in[pos + 2] == 'A' && in[pos + 3] == 'T') //IDAT chunk, containing compressed image data + { + idat.insert(idat.end(), &in[pos + 4], &in[pos + 4 + chunkLength]); + pos += (4 + chunkLength); + } + else if(in[pos + 0] == 'I' && in[pos + 1] == 'E' && in[pos + 2] == 'N' && in[pos + 3] == 'D') { pos += 4; IEND = true; } + else if(in[pos + 0] == 'P' && in[pos + 1] == 'L' && in[pos + 2] == 'T' && in[pos + 3] == 'E') //palette chunk (PLTE) + { + pos += 4; //go after the 4 letters + info.palette.resize(4 * (chunkLength / 3)); + if(info.palette.size() > (4 * 256)) { error = 38; return; } //error: palette too big + for(size_t i = 0; i < info.palette.size(); i += 4) + { + for(size_t j = 0; j < 3; j++) info.palette[i + j] = in[pos++]; //RGB + info.palette[i + 3] = 255; //alpha + } + } + else if(in[pos + 0] == 't' && in[pos + 1] == 'R' && in[pos + 2] == 'N' && in[pos + 3] == 'S') //palette transparency chunk (tRNS) + { + pos += 4; //go after the 4 letters + if(info.colorType == 3) + { + if(4 * chunkLength > info.palette.size()) { error = 39; return; } //error: more alpha values given than there are palette entries + for(size_t i = 0; i < chunkLength; i++) info.palette[4 * i + 3] = in[pos++]; + } + else if(info.colorType == 0) + { + if(chunkLength != 2) { error = 40; return; } //error: this chunk must be 2 bytes for greyscale image + info.key_defined = 1; info.key_r = info.key_g = info.key_b = 256 * in[pos] + in[pos + 1]; pos += 2; + } + else if(info.colorType == 2) + { + if(chunkLength != 6) { error = 41; return; } //error: this chunk must be 6 bytes for RGB image + info.key_defined = 1; + info.key_r = 256 * in[pos] + in[pos + 1]; pos += 2; + info.key_g = 256 * in[pos] + in[pos + 1]; pos += 2; + info.key_b = 256 * in[pos] + in[pos + 1]; pos += 2; + } + else { error = 42; return; } //error: tRNS chunk not allowed for other color models + } + else //it's not an implemented chunk type, so ignore it: skip over the data + { + if(!(in[pos + 0] & 32)) { error = 69; return; } //error: unknown critical chunk (5th bit of first byte of chunk type is 0) + pos += (chunkLength + 4); //skip 4 letters and uninterpreted data of unimplemented chunk + known_type = false; + } + pos += 4; //step over CRC (which is ignored) + } + unsigned long bpp = getBpp(info); + std::vector scanlines(((info.width * (info.height * bpp + 7)) / 8) + info.height); //now the out buffer will be filled + Zlib zlib; //decompress with the Zlib decompressor + error = zlib.decompress(scanlines, idat); if(error) return; //stop if the zlib decompressor returned an error + size_t bytewidth = (bpp + 7) / 8, outlength = (info.height * info.width * bpp + 7) / 8; + out.resize(outlength); //time to fill the out buffer + unsigned char* out_ = outlength ? &out[0] : 0; //use a regular pointer to the std::vector for faster code if compiled without optimization + if(info.interlaceMethod == 0) //no interlace, just filter + { + size_t linestart = 0, linelength = (info.width * bpp + 7) / 8; //length in bytes of a scanline, excluding the filtertype byte + if(bpp >= 8) //byte per byte + for(unsigned long y = 0; y < info.height; y++) + { + unsigned long filterType = scanlines[linestart]; + const unsigned char* prevline = (y == 0) ? 0 : &out_[(y - 1) * info.width * bytewidth]; + unFilterScanline(&out_[linestart - y], &scanlines[linestart + 1], prevline, bytewidth, filterType, linelength); if(error) return; + linestart += (1 + linelength); //go to start of next scanline + } + else //less than 8 bits per pixel, so fill it up bit per bit + { + std::vector templine((info.width * bpp + 7) >> 3); //only used if bpp < 8 + for(size_t y = 0, obp = 0; y < info.height; y++) + { + unsigned long filterType = scanlines[linestart]; + const unsigned char* prevline = (y == 0) ? 0 : &out_[(y - 1) * info.width * bytewidth]; + unFilterScanline(&templine[0], &scanlines[linestart + 1], prevline, bytewidth, filterType, linelength); if(error) return; + for(size_t bp = 0; bp < info.width * bpp;) setBitOfReversedStream(obp, out_, readBitFromReversedStream(bp, &templine[0])); + linestart += (1 + linelength); //go to start of next scanline + } + } + } + else //interlaceMethod is 1 (Adam7) + { + size_t passw[7] = { (info.width + 7) / 8, (info.width + 3) / 8, (info.width + 3) / 4, (info.width + 1) / 4, (info.width + 1) / 2, (info.width + 0) / 2, (info.width + 0) / 1 }; + size_t passh[7] = { (info.height + 7) / 8, (info.height + 7) / 8, (info.height + 3) / 8, (info.height + 3) / 4, (info.height + 1) / 4, (info.height + 1) / 2, (info.height + 0) / 2 }; + size_t passstart[7] = {0}; + size_t pattern[28] = {0,4,0,2,0,1,0,0,0,4,0,2,0,1,8,8,4,4,2,2,1,8,8,8,4,4,2,2}; //values for the adam7 passes + for(int i = 0; i < 6; i++) passstart[i + 1] = passstart[i] + passh[i] * ((passw[i] ? 1 : 0) + (passw[i] * bpp + 7) / 8); + std::vector scanlineo((info.width * bpp + 7) / 8), scanlinen((info.width * bpp + 7) / 8); //"old" and "new" scanline + for(int i = 0; i < 7; i++) + adam7Pass(&out_[0], &scanlinen[0], &scanlineo[0], &scanlines[passstart[i]], info.width, pattern[i], pattern[i + 7], pattern[i + 14], pattern[i + 21], passw[i], passh[i], bpp); + } + if(convert_to_rgba32 && (info.colorType != 6 || info.bitDepth != 8)) //conversion needed + { + std::vector data = out; + error = convert(out, &data[0], info, info.width, info.height); + } + } + void readPngHeader(const unsigned char* in, size_t inlength) //read the information from the header and store it in the Info + { + if(inlength < 29) { error = 27; return; } //error: the data length is smaller than the length of the header + if(in[0] != 137 || in[1] != 80 || in[2] != 78 || in[3] != 71 || in[4] != 13 || in[5] != 10 || in[6] != 26 || in[7] != 10) { error = 28; return; } //no PNG signature + if(in[12] != 'I' || in[13] != 'H' || in[14] != 'D' || in[15] != 'R') { error = 29; return; } //error: it doesn't start with a IHDR chunk! + info.width = read32bitInt(&in[16]); info.height = read32bitInt(&in[20]); + info.bitDepth = in[24]; info.colorType = in[25]; + info.compressionMethod = in[26]; if(in[26] != 0) { error = 32; return; } //error: only compression method 0 is allowed in the specification + info.filterMethod = in[27]; if(in[27] != 0) { error = 33; return; } //error: only filter method 0 is allowed in the specification + info.interlaceMethod = in[28]; if(in[28] > 1) { error = 34; return; } //error: only interlace methods 0 and 1 exist in the specification + error = checkColorValidity(info.colorType, info.bitDepth); + } + void unFilterScanline(unsigned char* recon, const unsigned char* scanline, const unsigned char* precon, size_t bytewidth, unsigned long filterType, size_t length) + { + switch(filterType) + { + case 0: for(size_t i = 0; i < length; i++) recon[i] = scanline[i]; break; + case 1: + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + recon[i - bytewidth]; + break; + case 2: + if(precon) for(size_t i = 0; i < length; i++) recon[i] = scanline[i] + precon[i]; + else for(size_t i = 0; i < length; i++) recon[i] = scanline[i]; + break; + case 3: + if(precon) + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i] + precon[i] / 2; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + ((recon[i - bytewidth] + precon[i]) / 2); + } + else + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + recon[i - bytewidth] / 2; + } + break; + case 4: + if(precon) + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i] + paethPredictor(0, precon[i], 0); + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + paethPredictor(recon[i - bytewidth], precon[i], precon[i - bytewidth]); + } + else + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + paethPredictor(recon[i - bytewidth], 0, 0); + } + break; + default: error = 36; return; //error: unexisting filter type given + } + } + void adam7Pass(unsigned char* out, unsigned char* linen, unsigned char* lineo, const unsigned char* in, unsigned long w, size_t passleft, size_t passtop, size_t spacex, size_t spacey, size_t passw, size_t passh, unsigned long bpp) + { //filter and reposition the pixels into the output when the image is Adam7 interlaced. This function can only do it after the full image is already decoded. The out buffer must have the correct allocated memory size already. + if(passw == 0) return; + size_t bytewidth = (bpp + 7) / 8, linelength = 1 + ((bpp * passw + 7) / 8); + for(unsigned long y = 0; y < passh; y++) + { + unsigned char filterType = in[y * linelength], *prevline = (y == 0) ? 0 : lineo; + unFilterScanline(linen, &in[y * linelength + 1], prevline, bytewidth, filterType, (w * bpp + 7) / 8); if(error) return; + if(bpp >= 8) for(size_t i = 0; i < passw; i++) for(size_t b = 0; b < bytewidth; b++) //b = current byte of this pixel + out[bytewidth * w * (passtop + spacey * y) + bytewidth * (passleft + spacex * i) + b] = linen[bytewidth * i + b]; + else for(size_t i = 0; i < passw; i++) + { + size_t obp = bpp * w * (passtop + spacey * y) + bpp * (passleft + spacex * i), bp = i * bpp; + for(size_t b = 0; b < bpp; b++) setBitOfReversedStream(obp, out, readBitFromReversedStream(bp, &linen[0])); + } + unsigned char* temp = linen; linen = lineo; lineo = temp; //swap the two buffer pointers "line old" and "line new" + } + } + static unsigned long readBitFromReversedStream(size_t& bitp, const unsigned char* bits) { unsigned long result = (bits[bitp >> 3] >> (7 - (bitp & 0x7))) & 1; bitp++; return result;} + static unsigned long readBitsFromReversedStream(size_t& bitp, const unsigned char* bits, unsigned long nbits) + { + unsigned long result = 0; + for(size_t i = nbits - 1; i < nbits; i--) result += ((readBitFromReversedStream(bitp, bits)) << i); + return result; + } + void setBitOfReversedStream(size_t& bitp, unsigned char* bits, unsigned long bit) { bits[bitp >> 3] |= (bit << (7 - (bitp & 0x7))); bitp++; } + unsigned long read32bitInt(const unsigned char* buffer) { return (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]; } + int checkColorValidity(unsigned long colorType, unsigned long bd) //return type is a LodePNG error code + { + if((colorType == 2 || colorType == 4 || colorType == 6)) { if(!(bd == 8 || bd == 16)) return 37; else return 0; } + else if(colorType == 0) { if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 || bd == 16)) return 37; else return 0; } + else if(colorType == 3) { if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 )) return 37; else return 0; } + else return 31; //unexisting color type + } + unsigned long getBpp(const Info& info) + { + if(info.colorType == 2) return (3 * info.bitDepth); + else if(info.colorType >= 4) return (info.colorType - 2) * info.bitDepth; + else return info.bitDepth; + } + int convert(std::vector& out, const unsigned char* in, Info& infoIn, unsigned long w, unsigned long h) + { //converts from any color type to 32-bit. return value = LodePNG error code + size_t numpixels = w * h, bp = 0; + out.resize(numpixels * 4); + unsigned char* out_ = out.empty() ? 0 : &out[0]; //faster if compiled without optimization + if(infoIn.bitDepth == 8 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[i]; + out_[4 * i + 3] = (infoIn.key_defined && in[i] == infoIn.key_r) ? 0 : 255; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 2) //RGB color + for(size_t i = 0; i < numpixels; i++) + { + for(size_t c = 0; c < 3; c++) out_[4 * i + c] = in[3 * i + c]; + out_[4 * i + 3] = (infoIn.key_defined == 1 && in[3 * i + 0] == infoIn.key_r && in[3 * i + 1] == infoIn.key_g && in[3 * i + 2] == infoIn.key_b) ? 0 : 255; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 3) //indexed color (palette) + for(size_t i = 0; i < numpixels; i++) + { + if(4U * in[i] >= infoIn.palette.size()) return 46; + for(size_t c = 0; c < 4; c++) out_[4 * i + c] = infoIn.palette[4 * in[i] + c]; //get rgb colors from the palette + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 4) //greyscale with alpha + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[2 * i + 0]; + out_[4 * i + 3] = in[2 * i + 1]; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 6) for(size_t i = 0; i < numpixels; i++) for(size_t c = 0; c < 4; c++) out_[4 * i + c] = in[4 * i + c]; //RGB with alpha + else if(infoIn.bitDepth == 16 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[2 * i]; + out_[4 * i + 3] = (infoIn.key_defined && 256U * in[i] + in[i + 1] == infoIn.key_r) ? 0 : 255; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 2) //RGB color + for(size_t i = 0; i < numpixels; i++) + { + for(size_t c = 0; c < 3; c++) out_[4 * i + c] = in[6 * i + 2 * c]; + out_[4 * i + 3] = (infoIn.key_defined && 256U*in[6*i+0]+in[6*i+1] == infoIn.key_r && 256U*in[6*i+2]+in[6*i+3] == infoIn.key_g && 256U*in[6*i+4]+in[6*i+5] == infoIn.key_b) ? 0 : 255; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 4) //greyscale with alpha + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[4 * i]; //most significant byte + out_[4 * i + 3] = in[4 * i + 2]; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 6) for(size_t i = 0; i < numpixels; i++) for(size_t c = 0; c < 4; c++) out_[4 * i + c] = in[8 * i + 2 * c]; //RGB with alpha + else if(infoIn.bitDepth < 8 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + unsigned long value = (readBitsFromReversedStream(bp, in, infoIn.bitDepth) * 255) / ((1 << infoIn.bitDepth) - 1); //scale value from 0 to 255 + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = (unsigned char)(value); + out_[4 * i + 3] = (infoIn.key_defined && value && ((1U << infoIn.bitDepth) - 1U) == infoIn.key_r && ((1U << infoIn.bitDepth) - 1U)) ? 0 : 255; + } + else if(infoIn.bitDepth < 8 && infoIn.colorType == 3) //palette + for(size_t i = 0; i < numpixels; i++) + { + unsigned long value = readBitsFromReversedStream(bp, in, infoIn.bitDepth); + if(4 * value >= infoIn.palette.size()) return 47; + for(size_t c = 0; c < 4; c++) out_[4 * i + c] = infoIn.palette[4 * value + c]; //get rgb colors from the palette + } + return 0; + } + unsigned char paethPredictor(short a, short b, short c) //Paeth predicter, used by PNG filter type 4 + { + short p = a + b - c, pa = p > a ? (p - a) : (a - p), pb = p > b ? (p - b) : (b - p), pc = p > c ? (p - c) : (c - p); + return (unsigned char)((pa <= pb && pa <= pc) ? a : pb <= pc ? b : c); + } + }; + PNG decoder; decoder.decode(out_image, in_png, in_size, convert_to_rgba32); + image_width = decoder.info.width; image_height = decoder.info.height; + return decoder.error; +} + +#if 0 + + + +//an example using the PNG loading function: + +#include +#include + +void loadFile(std::vector& buffer, const std::string& filename) //designed for loading files from hard disk in an std::vector +{ + std::ifstream file(filename.c_str(), std::ios::in|std::ios::binary|std::ios::ate); + + //get filesize + std::streamsize size = 0; + if(file.seekg(0, std::ios::end).good()) size = file.tellg(); + if(file.seekg(0, std::ios::beg).good()) size -= file.tellg(); + + //read contents of the file into the vector + if(size > 0) + { + buffer.resize((size_t)size); + file.read((char*)(&buffer[0]), size); + } + else buffer.clear(); +} + +int main(int argc, char *argv[]) +{ + const char* filename = argc > 1 ? argv[1] : "test.png"; + + //load and decode + std::vector buffer, image; + loadFile(buffer, filename); + unsigned long w, h; + int error = decodePNG(image, w, h, buffer.empty() ? 0 : &buffer[0], (unsigned long)buffer.size()); + + //if there's an error, display it + if(error != 0) std::cout << "error: " << error << std::endl; + + //the pixels are now in the vector "image", use it as texture, draw it, ... + + if(image.size() > 4) std::cout << "width: " << w << " height: " << h << " first pixel: " << std::hex << int(image[0]) << int(image[1]) << int(image[2]) << int(image[3]) << std::endl; +} + +/* + //this is test code, it displays the pixels of a 1 bit PNG. To use it, set the flag convert_to_rgba32 to false and load a 1-bit PNG image with a small size (so that its ASCII representation can fit in a console window) + for(int y = 0; y < h; y++) + { + for(int x = 0; x < w; x++) + { + int i = y * h + x; + std::cout << (((image[i/8] >> (7-i%8)) & 1) ? '.' : '#'); + } + std::cout << std::endl; + } +*/ + +#endif diff --git a/libraries/ZWidget/example/picopng.h b/libraries/ZWidget/example/picopng.h new file mode 100644 index 000000000..87dc961be --- /dev/null +++ b/libraries/ZWidget/example/picopng.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +int decodePNG(std::vector& out_image, unsigned long& image_width, unsigned long& image_height, const unsigned char* in_png, size_t in_size, bool convert_to_rgba32 = true); diff --git a/libraries/ZWidget/include/zwidget/core/canvas.h b/libraries/ZWidget/include/zwidget/core/canvas.h new file mode 100644 index 000000000..c3ae7fea8 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/canvas.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include +#include "image.h" +#include "rect.h" +#include + +class Font; +class Point; +class Rect; +class Colorf; +class DisplayWindow; +class CanvasFontGroup; + +class CanvasTexture +{ +public: + virtual ~CanvasTexture() = default; + + int Width = 0; + int Height = 0; +}; + +class FontMetrics +{ +public: + double ascent = 0.0; + double descent = 0.0; + double external_leading = 0.0; + double height = 0.0; +}; + +struct VerticalTextPosition +{ + double top = 0.0; + double baseline = 0.0; + double bottom = 0.0; +}; + +class Canvas +{ +public: + static std::unique_ptr create(); + + Canvas(); + virtual ~Canvas(); + + virtual void attach(DisplayWindow* window); + virtual void detach(); + + virtual void begin(const Colorf& color); + virtual void end() { } + + virtual void begin3d() { } + virtual void end3d() { } + + Point getOrigin(); + void setOrigin(const Point& origin); + + void pushClip(const Rect& box); + void popClip(); + + void fillRect(const Rect& box, const Colorf& color); + void line(const Point& p0, const Point& p1, const Colorf& color); + + void drawText(const Point& pos, const Colorf& color, const std::string& text); + Rect measureText(const std::string& text); + VerticalTextPosition verticalTextAlign(); + + void drawText(const std::shared_ptr& font, const Point& pos, const std::string& text, const Colorf& color); + void drawTextEllipsis(const std::shared_ptr& font, const Point& pos, const Rect& clipBox, const std::string& text, const Colorf& color); + Rect measureText(const std::shared_ptr& font, const std::string& text); + FontMetrics getFontMetrics(const std::shared_ptr& font); + int getCharacterIndex(const std::shared_ptr& font, const std::string& text, const Point& hitPoint); + + void drawImage(const std::shared_ptr& image, const Point& pos); + void drawImage(const std::shared_ptr& image, const Rect& box); + +protected: + virtual std::unique_ptr createTexture(int width, int height, const void* pixels, ImageFormat format = ImageFormat::B8G8R8A8) = 0; + virtual void drawLineAntialiased(float x0, float y0, float x1, float y1, Colorf color) = 0; + virtual void fillTile(float x, float y, float width, float height, Colorf color) = 0; + virtual void drawTile(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) = 0; + virtual void drawGlyph(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) = 0; + + int getClipMinX() const; + int getClipMinY() const; + int getClipMaxX() const; + int getClipMaxY() const; + + template + static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } + + DisplayWindow* window = nullptr; + int width = 0; + int height = 0; + double uiscale = 1.0f; + + std::unique_ptr whiteTexture; + +private: + void setLanguage(const char* lang) { language = lang; } + void drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color); + + std::unique_ptr font; + + Point origin; + std::vector clipStack; + + std::unordered_map, std::unique_ptr> imageTextures; + std::string language; + + friend class CanvasFont; +}; diff --git a/libraries/ZWidget/include/zwidget/core/colorf.h b/libraries/ZWidget/include/zwidget/core/colorf.h new file mode 100644 index 000000000..935214799 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/colorf.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +class Colorf +{ +public: + Colorf() = default; + Colorf(float r, float g, float b, float a = 1.0f) : r(r), g(g), b(b), a(a) { } + + static Colorf transparent() { return { 0.0f, 0.0f, 0.0f, 0.0f }; } + + static Colorf fromRgba8(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) + { + float s = 1.0f / 255.0f; + return { r * s, g * s, b * s, a * s }; + } + + uint32_t toBgra8() const + { + uint32_t cr = (int)(std::max(std::min(r * 255.0f, 255.0f), 0.0f)); + uint32_t cg = (int)(std::max(std::min(g * 255.0f, 255.0f), 0.0f)); + uint32_t cb = (int)(std::max(std::min(b * 255.0f, 255.0f), 0.0f)); + uint32_t ca = (int)(std::max(std::min(a * 255.0f, 255.0f), 0.0f)); + return (ca << 24) | (cr << 16) | (cg << 8) | cb; + } + + bool operator==(const Colorf& v) const { return r == v.r && g == v.g && b == v.b && a == v.a; } + bool operator!=(const Colorf& v) const { return r != v.r || g != v.g || b != v.b || a != v.a; } + + float r = 0.0f; + float g = 0.0f; + float b = 0.0f; + float a = 1.0f; +}; diff --git a/libraries/ZWidget/include/zwidget/core/font.h b/libraries/ZWidget/include/zwidget/core/font.h new file mode 100644 index 000000000..d4af36061 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/font.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class Font +{ +public: + virtual ~Font() = default; + + virtual const std::string& GetName() const = 0; + virtual double GetHeight() const = 0; + + static std::shared_ptr Create(const std::string& name, double height); +}; diff --git a/libraries/ZWidget/include/zwidget/core/image.h b/libraries/ZWidget/include/zwidget/core/image.h new file mode 100644 index 000000000..4cffc195a --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/image.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +enum class ImageFormat +{ + R8G8B8A8, + B8G8R8A8 +}; + +class Image +{ +public: + virtual ~Image() = default; + + virtual int GetWidth() const = 0; + virtual int GetHeight() const = 0; + virtual ImageFormat GetFormat() const = 0; + virtual void* GetData() const = 0; + + static std::shared_ptr Create(int width, int height, ImageFormat format, const void* data); + static std::shared_ptr LoadResource(const std::string& resourcename, double dpiscale = 1.0); +}; diff --git a/libraries/ZWidget/include/zwidget/core/pathfill.h b/libraries/ZWidget/include/zwidget/core/pathfill.h new file mode 100644 index 000000000..a6d749806 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/pathfill.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include +#include "core/rect.h" + +/* +// 3x3 transform matrix +class PathFillTransform +{ +public: + PathFillTransform() { for (int i = 0; i < 9; i++) matrix[i] = 0.0f; matrix[0] = matrix[4] = matrix[8] = 1.0; } + PathFillTransform(const float* mat3x3) { for (int i = 0; i < 9; i++) matrix[i] = (double)mat3x3[i]; } + PathFillTransform(const double* mat3x3) { for (int i = 0; i < 9; i++) matrix[i] = mat3x3[i]; } + + double matrix[9]; +}; +*/ + +enum class PathFillMode +{ + alternate, + winding +}; + +enum class PathFillCommand +{ + line, + quadradic, + cubic +}; + +class PathFillSubpath +{ +public: + PathFillSubpath() : points(1) { } + + std::vector points; + std::vector commands; + bool closed = false; +}; + +class PathFillDesc +{ +public: + PathFillDesc() : subpaths(1) { } + + PathFillMode fill_mode = PathFillMode::alternate; + std::vector subpaths; + + void MoveTo(const Point& point) + { + if (!subpaths.back().commands.empty()) + { + subpaths.push_back(PathFillSubpath()); + } + subpaths.back().points.front() = point; + } + + void LineTo(const Point& point) + { + auto& subpath = subpaths.back(); + subpath.points.push_back(point); + subpath.commands.push_back(PathFillCommand::line); + } + + void BezierTo(const Point& control, const Point& point) + { + auto& subpath = subpaths.back(); + subpath.points.push_back(control); + subpath.points.push_back(point); + subpath.commands.push_back(PathFillCommand::quadradic); + } + + void BezierTo(const Point& control1, const Point& control2, const Point& point) + { + auto& subpath = subpaths.back(); + subpath.points.push_back(control1); + subpath.points.push_back(control2); + subpath.points.push_back(point); + subpath.commands.push_back(PathFillCommand::cubic); + } + + void Close() + { + if (!subpaths.back().commands.empty()) + { + subpaths.back().closed = true; + subpaths.push_back(PathFillSubpath()); + } + } + + void Rasterize(uint8_t* dest, int width, int height, bool blend = false); +}; diff --git a/libraries/ZWidget/include/zwidget/core/rect.h b/libraries/ZWidget/include/zwidget/core/rect.h new file mode 100644 index 000000000..3b655cd2e --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/rect.h @@ -0,0 +1,85 @@ +#pragma once + +#include + +class Point +{ +public: + Point() = default; + Point(double x, double y) : x(x), y(y) { } + + double x = 0; + double y = 0; + + Point& operator+=(const Point& p) { x += p.x; y += p.y; return *this; } + Point& operator-=(const Point& p) { x -= p.x; y -= p.y; return *this; } +}; + +class Size +{ +public: + Size() = default; + Size(double width, double height) : width(width), height(height) { } + + double width = 0; + double height = 0; +}; + +class Rect +{ +public: + Rect() = default; + Rect(const Point& p, const Size& s) : x(p.x), y(p.y), width(s.width), height(s.height) { } + Rect(double x, double y, double width, double height) : x(x), y(y), width(width), height(height) { } + + Point pos() const { return { x, y }; } + Size size() const { return { width, height }; } + + Point topLeft() const { return { x, y }; } + Point topRight() const { return { x + width, y }; } + Point bottomLeft() const { return { x, y + height }; } + Point bottomRight() const { return { x + width, y + height }; } + + double left() const { return x; } + double top() const { return y; } + double right() const { return x + width; } + double bottom() const { return y + height; } + + static Rect xywh(double x, double y, double width, double height) { return Rect(x, y, width, height); } + static Rect ltrb(double left, double top, double right, double bottom) { return Rect(left, top, right - left, bottom - top); } + + static Rect shrink(Rect box, double left, double top, double right, double bottom) + { + box.x += left; + box.y += top; + box.width = std::max(box.width - left - right, 0.0); + box.height = std::max(box.height - bottom - top, 0.0); + return box; + } + + bool contains(const Point& p) const { return (p.x >= x && p.x < x + width) && (p.y >= y && p.y < y + height); } + + double x = 0; + double y = 0; + double width = 0; + double height = 0; +}; + +inline Point operator+(const Point& a, const Point& b) { return Point(a.x + b.x, a.y + b.y); } +inline Point operator-(const Point& a, const Point& b) { return Point(a.x - b.x, a.y - b.y); } +inline bool operator==(const Point& a, const Point& b) { return a.x == b.x && a.y == b.y; } +inline bool operator!=(const Point& a, const Point& b) { return a.x != b.x || a.y != b.y; } +inline bool operator==(const Size& a, const Size& b) { return a.width == b.width && a.height == b.height; } +inline bool operator!=(const Size& a, const Size& b) { return a.width != b.width || a.height != b.height; } +inline bool operator==(const Rect& a, const Rect& b) { return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height; } +inline bool operator!=(const Rect& a, const Rect& b) { return a.x != b.x || a.y != b.y || a.width != b.width || a.height != b.height; } + +inline Point operator+(const Point& a, double b) { return Point(a.x + b, a.y + b); } +inline Point operator-(const Point& a, double b) { return Point(a.x - b, a.y - b); } +inline Point operator*(const Point& a, double b) { return Point(a.x * b, a.y * b); } +inline Point operator/(const Point& a, double b) { return Point(a.x / b, a.y / b); } + +inline Size operator+(const Size& a, double b) { return Size(a.width + b, a.height + b); } +inline Size operator-(const Size& a, double b) { return Size(a.width - b, a.height - b); } +inline Size operator*(const Size& a, double b) { return Size(a.width * b, a.height * b); } +inline Size operator/(const Size& a, double b) { return Size(a.width / b, a.height / b); } diff --git a/libraries/ZWidget/include/zwidget/core/resourcedata.h b/libraries/ZWidget/include/zwidget/core/resourcedata.h new file mode 100644 index 000000000..f29ab261e --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/resourcedata.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include + +struct SingleFontData +{ + std::vector fontdata; + std::string language; +}; + +std::vector LoadWidgetFontData(const std::string& name); +std::vector LoadWidgetData(const std::string& name); diff --git a/libraries/ZWidget/include/zwidget/core/span_layout.h b/libraries/ZWidget/include/zwidget/core/span_layout.h new file mode 100644 index 000000000..974feca40 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/span_layout.h @@ -0,0 +1,238 @@ +#pragma once + +#include +#include + +#include "colorf.h" +#include "rect.h" +#include "font.h" +#include "canvas.h" + +class Widget; +class Image; +class Canvas; + +enum SpanAlign +{ + span_left, + span_right, + span_center, + span_justify +}; + +class SpanLayout +{ +public: + SpanLayout(); + ~SpanLayout(); + + struct HitTestResult + { + enum Type + { + no_objects_available, + outside_top, + outside_left, + outside_right, + outside_bottom, + inside + }; + + Type type = {}; + int object_id = -1; + size_t offset = 0; + }; + + void Clear(); + + void AddText(const std::string& text, std::shared_ptr font, const Colorf& color = Colorf(), int id = -1); + void AddImage(const std::shared_ptr image, double baseline_offset = 0, int id = -1); + void AddWidget(Widget* component, double baseline_offset = 0, int id = -1); + + void Layout(Canvas* canvas, double max_width); + + void SetPosition(const Point& pos); + + Size GetSize() const; + Rect GetRect() const; + + std::vector GetRectById(int id) const; + + HitTestResult HitTest(Canvas* canvas, const Point& pos); + + void DrawLayout(Canvas* canvas); + + /// Draw layout generating ellipsis for clipped text + void DrawLayoutEllipsis(Canvas* canvas, const Rect& content_rect); + + void SetComponentGeometry(); + + Size FindPreferredSize(Canvas* canvas); + + void SetSelectionRange(std::string::size_type start, std::string::size_type end); + void SetSelectionColors(const Colorf& foreground, const Colorf& background); + + void ShowCursor(); + void HideCursor(); + + void SetCursorPos(std::string::size_type pos); + void SetCursorOverwriteMode(bool enable); + void SetCursorColor(const Colorf& color); + + std::string GetCombinedText() const; + + void SetAlign(SpanAlign align); + + double GetFirstBaselineOffset(); + double GetLastBaselineOffset(); + +private: + struct TextBlock + { + size_t start = 0; + size_t end = 0; + }; + + enum ObjectType + { + object_text, + object_image, + object_component + }; + + enum FloatType + { + float_none, + float_left, + float_right + }; + + struct SpanObject + { + ObjectType type = object_text; + FloatType float_type = float_none; + + std::shared_ptr font; + Colorf color; + size_t start = 0, end = 0; + + std::shared_ptr image; + Widget* component = nullptr; + double baseline_offset = 0; + + int id = -1; + }; + + struct LineSegment + { + ObjectType type = object_text; + + std::shared_ptr font; + Colorf color; + size_t start = 0; + size_t end = 0; + double ascender = 0; + double descender = 0; + + double x_position = 0; + double width = 0; + + std::shared_ptr image; + Widget* component = nullptr; + double baseline_offset = 0; + + int id = -1; + }; + + struct Line + { + double width = 0; // Width of the entire line (including spaces) + double height = 0; + double ascender = 0; + std::vector segments; + }; + + struct TextSizeResult + { + size_t start = 0; + size_t end = 0; + double width = 0; + double height = 0; + double ascender = 0; + double descender = 0; + int objects_traversed = 0; + std::vector segments; + }; + + struct CurrentLine + { + std::vector::size_type object_index = 0; + Line cur_line; + double x_position = 0; + double y_position = 0; + }; + + struct FloatBox + { + Rect rect; + ObjectType type = object_image; + std::shared_ptr image; + Widget* component = nullptr; + int id = -1; + }; + + TextSizeResult FindTextSize(Canvas* canvas, const TextBlock& block, size_t object_index); + std::vector FindTextBlocks(); + void LayoutLines(Canvas* canvas, double max_width); + void LayoutText(Canvas* canvas, std::vector blocks, std::vector::size_type block_index, CurrentLine& current_line, double max_width); + void LayoutBlock(CurrentLine& current_line, double max_width, std::vector& blocks, std::vector::size_type block_index); + void LayoutFloatBlock(CurrentLine& current_line, double max_width); + void LayoutInlineBlock(CurrentLine& current_line, double max_width, std::vector& blocks, std::vector::size_type block_index); + void ReflowLine(CurrentLine& current_line, double max_width); + FloatBox FloatBoxLeft(FloatBox float_box, double max_width); + FloatBox FloatBoxRight(FloatBox float_box, double max_width); + FloatBox FloatBoxAny(FloatBox box, double max_width, const std::vector& floats1); + bool BoxFitsOnLine(const FloatBox& box, double max_width); + void PlaceLineSegments(CurrentLine& current_line, TextSizeResult& text_size_result); + void ForcePlaceLineSegments(CurrentLine& current_line, TextSizeResult& text_size_result, double max_width); + void NextLine(CurrentLine& current_line); + bool IsNewline(const TextBlock& block); + bool IsWhitespace(const TextBlock& block); + bool FitsOnLine(double x_position, const TextSizeResult& text_size_result, double max_width); + bool LargerThanLine(const TextSizeResult& text_size_result, double max_width); + void AlignJustify(double max_width); + void AlignCenter(double max_width); + void AlignRight(double max_width); + void DrawLayoutImage(Canvas* canvas, Line& line, LineSegment& segment, double x, double y); + void DrawLayoutText(Canvas* canvas, Line& line, LineSegment& segment, double x, double y); + + bool cursor_visible = false; + std::string::size_type cursor_pos = 0; + bool cursor_overwrite_mode = false; + Colorf cursor_color; + + std::string::size_type sel_start = 0, sel_end = 0; + Colorf sel_foreground, sel_background = Colorf::fromRgba8(153, 201, 239); + + std::string text; + std::vector objects; + std::vector lines; + Point position; + + std::vector floats_left, floats_right; + + SpanAlign alignment = span_left; + + struct LayoutCache + { + int object_index = -1; + FontMetrics metrics; + }; + LayoutCache layout_cache; + + bool is_ellipsis_draw = false; + Rect ellipsis_content_rect; + + template + static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } +}; diff --git a/libraries/ZWidget/include/zwidget/core/theme.h b/libraries/ZWidget/include/zwidget/core/theme.h new file mode 100644 index 000000000..5a5271bc3 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/theme.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include "rect.h" +#include "colorf.h" + +class Widget; +class Canvas; + +class WidgetStyle +{ +public: + WidgetStyle(WidgetStyle* parentStyle = nullptr) : ParentStyle(parentStyle) { } + virtual ~WidgetStyle() = default; + virtual void Paint(Widget* widget, Canvas* canvas, Size size) = 0; + + void SetBool(const std::string& state, const std::string& propertyName, bool value); + void SetInt(const std::string& state, const std::string& propertyName, int value); + void SetDouble(const std::string& state, const std::string& propertyName, double value); + void SetString(const std::string& state, const std::string& propertyName, const std::string& value); + void SetColor(const std::string& state, const std::string& propertyName, const Colorf& value); + + void SetBool(const std::string& propertyName, bool value) { SetBool(std::string(), propertyName, value); } + void SetInt(const std::string& propertyName, int value) { SetInt(std::string(), propertyName, value); } + void SetDouble(const std::string& propertyName, double value) { SetDouble(std::string(), propertyName, value); } + void SetString(const std::string& propertyName, const std::string& value) { SetString(std::string(), propertyName, value); } + void SetColor(const std::string& propertyName, const Colorf& value) { SetColor(std::string(), propertyName, value); } + +private: + // Note: do not call these directly. Use widget->GetStyleXX instead since a widget may explicitly override a class style + bool GetBool(const std::string& state, const std::string& propertyName) const; + int GetInt(const std::string& state, const std::string& propertyName) const; + double GetDouble(const std::string& state, const std::string& propertyName) const; + std::string GetString(const std::string& state, const std::string& propertyName) const; + Colorf GetColor(const std::string& state, const std::string& propertyName) const; + + WidgetStyle* ParentStyle = nullptr; + typedef std::variant PropertyVariant; + std::unordered_map> StyleProperties; + + const PropertyVariant* FindProperty(const std::string& state, const std::string& propertyName) const; + + friend class Widget; +}; + +class BasicWidgetStyle : public WidgetStyle +{ +public: + using WidgetStyle::WidgetStyle; + void Paint(Widget* widget, Canvas* canvas, Size size) override; +}; + +class WidgetTheme +{ +public: + virtual ~WidgetTheme() = default; + + WidgetStyle* RegisterStyle(std::unique_ptr widgetStyle, const std::string& widgetClass); + WidgetStyle* GetStyle(const std::string& widgetClass); + + static void SetTheme(std::unique_ptr theme); + static WidgetTheme* GetTheme(); + +private: + std::unordered_map> Styles; +}; + +class DarkWidgetTheme : public WidgetTheme +{ +public: + DarkWidgetTheme(); +}; + +class LightWidgetTheme : public WidgetTheme +{ +public: + LightWidgetTheme(); +}; diff --git a/libraries/ZWidget/include/zwidget/core/timer.h b/libraries/ZWidget/include/zwidget/core/timer.h new file mode 100644 index 000000000..25e6fb346 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/timer.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +class Widget; + +class Timer +{ +public: + Timer(Widget* owner); + ~Timer(); + + void Start(int timeoutMilliseconds, bool repeat = true); + void Stop(); + + std::function FuncExpired; + +private: + Widget* OwnerObj = nullptr; + Timer* PrevTimerObj = nullptr; + Timer* NextTimerObj = nullptr; + + void* TimerId = nullptr; + + friend class Widget; +}; diff --git a/libraries/ZWidget/include/zwidget/core/utf8reader.h b/libraries/ZWidget/include/zwidget/core/utf8reader.h new file mode 100644 index 000000000..f9c186b47 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/utf8reader.h @@ -0,0 +1,78 @@ +/* +** Copyright (c) 1997-2015 Mark Page +** +** This software is provided 'as-is', without any express or implied +** warranty. In no event will the authors be held liable for any damages +** arising from the use of this software. +** +** Permission is granted to anyone to use this software for any purpose, +** including commercial applications, and to alter it and redistribute it +** freely, subject to the following restrictions: +** +** 1. The origin of this software must not be misrepresented; you must not +** claim that you wrote the original software. If you use this software +** in a product, an acknowledgment in the product documentation would be +** appreciated but is not required. +** 2. Altered source versions must be plainly marked as such, and must not be +** misrepresented as being the original software. +** 3. This notice may not be removed or altered from any source distribution. +** +*/ + +#pragma once + +#include + +/// \brief UTF8 reader helper functions. +class UTF8Reader +{ +public: + /// Important: text is not copied by this class and must remain valid during its usage. + UTF8Reader(const std::string::value_type *text, std::string::size_type length); + + /// \brief Returns true if the current position is at the end of the string + bool is_end(); + + /// \brief Get the character at the current position + unsigned int character(); + + /// \brief Returns the length of the current character + std::string::size_type char_length(); + + /// \brief Moves position to the previous character + void prev(); + + /// \brief Moves position to the next character + void next(); + + /// \brief Moves position to the lead byte of the character + void move_to_leadbyte(); + + /// \brief Get the current position of the reader + std::string::size_type position(); + + /// \brief Set the current position of the reader + void set_position(std::string::size_type position); + + static size_t utf8_length(const std::string& text) + { + return utf8_length(text.data(), text.size()); + } + + static size_t utf8_length(const std::string::value_type* text, std::string::size_type length) + { + UTF8Reader reader(text, length); + size_t i = 0; + while (!reader.is_end()) + { + reader.next(); + i++; + } + return i; + } + +private: + std::string::size_type current_position = 0; + std::string::size_type length = 0; + const unsigned char *data = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/core/widget.h b/libraries/ZWidget/include/zwidget/core/widget.h new file mode 100644 index 000000000..6d87f72a0 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/widget.h @@ -0,0 +1,241 @@ +#pragma once + +#include +#include +#include +#include +#include "canvas.h" +#include "rect.h" +#include "colorf.h" +#include "../window/window.h" + +class Canvas; +class Timer; + +enum class WidgetType +{ + Child, + Window, + Popup +}; + +class Widget : DisplayWindowHost +{ +public: + Widget(Widget* parent = nullptr, WidgetType type = WidgetType::Child, RenderAPI api = RenderAPI::Unspecified); + virtual ~Widget(); + + void SetParent(Widget* parent); + void MoveBefore(Widget* sibling); + + std::string GetWindowTitle() const; + void SetWindowTitle(const std::string& text); + + // Icon GetWindowIcon() const; + // void SetWindowIcon(const Icon& icon); + + // Widget content box + Size GetSize() const; + double GetWidth() const { return GetSize().width; } + double GetHeight() const { return GetSize().height; } + + // Widget noncontent area + void SetNoncontentSizes(double left, double top, double right, double bottom); + double GetNoncontentLeft() const { return GridFitSize(GetStyleDouble("noncontent-left")); } + double GetNoncontentTop() const { return GridFitSize(GetStyleDouble("noncontent-top")); } + double GetNoncontentRight() const { return GridFitSize(GetStyleDouble("noncontent-right")); } + double GetNoncontentBottom() const { return GridFitSize(GetStyleDouble("noncontent-bottom")); } + + // Get the DPI scale factor for the window the widget is located on + double GetDpiScale() const; + + // Align point to the nearest physical screen pixel + double GridFitPoint(double p) const; + Point GridFitPoint(double x, double y) const { return GridFitPoint(Point(x, y)); } + Point GridFitPoint(Point p) const { return Point(GridFitPoint(p.x), GridFitPoint(p.y)); } + + // Convert size to exactly covering physical screen pixels + double GridFitSize(double s) const; + Size GridFitSize(double w, double h) const { return GridFitSize(Size(w, h)); } + Size GridFitSize(Size s) const { return Size(GridFitSize(s.width), GridFitSize(s.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)); } + + // Style properties + void SetStyleClass(const std::string& styleClass); + const std::string& GetStyleClass() const { return StyleClass; } + void SetStyleState(const std::string& state); + const std::string& GetStyleState() const { return StyleState; } + void SetStyleBool(const std::string& propertyName, bool value); + void SetStyleInt(const std::string& propertyName, int value); + void SetStyleDouble(const std::string& propertyName, double value); + void SetStyleString(const std::string& propertyName, const std::string& value); + void SetStyleColor(const std::string& propertyName, const Colorf& value); + bool GetStyleBool(const std::string& propertyName) const; + int GetStyleInt(const std::string& propertyName) const; + double GetStyleDouble(const std::string& propertyName) const; + std::string GetStyleString(const std::string& propertyName) const; + Colorf GetStyleColor(const std::string& propertyName) const; + + void SetWindowBackground(const Colorf& color); + void SetWindowBorderColor(const Colorf& color); + void SetWindowCaptionColor(const Colorf& color); + void SetWindowCaptionTextColor(const Colorf& color); + + void SetVisible(bool enable) { if (enable) Show(); else Hide(); } + void Show(); + void ShowFullscreen(); + void ShowMaximized(); + void ShowMinimized(); + void ShowNormal(); + void Hide(); + + void ActivateWindow(); + + void Close(); + + void Update(); + void Repaint(); + + bool HasFocus(); + bool IsEnabled(); + bool IsVisible(); + bool IsHidden(); + bool IsFullscreen(); + + 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 SetPointerCapture(); + void ReleasePointerCapture(); + + void SetModalCapture(); + void ReleaseModalCapture(); + + bool GetKeyState(InputKey key); + + std::string GetClipboardText(); + void SetClipboardText(const std::string& text); + + Widget* Window() const; + Canvas* GetCanvas() const; + Widget* ChildAt(double x, double y) { return ChildAt(Point(x, y)); } + Widget* ChildAt(const Point& pos); + + bool IsParent(const Widget* w) const; + bool IsChild(const Widget* w) const; + + Widget* Parent() const { return ParentObj; } + Widget* PrevSibling() const { return PrevSiblingObj; } + Widget* NextSibling() const { return NextSiblingObj; } + Widget* FirstChild() const { return FirstChildObj; } + Widget* LastChild() const { return LastChildObj; } + + Point MapFrom(const Widget* parent, const Point& pos) const; + Point MapFromGlobal(const Point& pos) const; + Point MapFromParent(const Point& pos) const { return MapFrom(Parent(), pos); } + + Point MapTo(const Widget* parent, const Point& pos) const; + Point MapToGlobal(const Point& pos) const; + Point MapToParent(const Point& pos) const { return MapTo(Parent(), pos); } + + static Size GetScreenSize(); + + void SetCanvas(std::unique_ptr canvas); + void* GetNativeHandle(); + int GetNativePixelWidth(); + int GetNativePixelHeight(); + + // Vulkan support: + std::vector GetVulkanInstanceExtensions() { return Window()->DispWindow->GetVulkanInstanceExtensions(); } + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) { return Window()->DispWindow->CreateVulkanSurface(instance); } + +protected: + virtual void OnPaintFrame(Canvas* canvas); + virtual void OnPaint(Canvas* canvas) { } + virtual bool OnMouseDown(const Point& pos, InputKey key) { return false; } + virtual bool OnMouseDoubleclick(const Point& pos, InputKey key) { return false; } + virtual bool OnMouseUp(const Point& pos, InputKey key) { return false; } + virtual bool OnMouseWheel(const Point& pos, InputKey key) { return false; } + virtual void OnMouseMove(const Point& pos) { } + virtual void OnMouseLeave() { } + virtual void OnRawMouseMove(int dx, int dy) { } + virtual void OnKeyChar(std::string chars) { } + virtual void OnKeyDown(InputKey key) { } + virtual void OnKeyUp(InputKey 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 OnWindowMouseLeave() override; + void OnWindowMouseDown(const Point& pos, InputKey key) override; + void OnWindowMouseDoubleclick(const Point& pos, InputKey key) override; + void OnWindowMouseUp(const Point& pos, InputKey key) override; + void OnWindowMouseWheel(const Point& pos, InputKey key) override; + void OnWindowRawMouseMove(int dx, int dy) override; + void OnWindowKeyChar(std::string chars) override; + void OnWindowKeyDown(InputKey key) override; + void OnWindowKeyUp(InputKey key) override; + void OnWindowGeometryChanged() override; + void OnWindowClose() override; + void OnWindowActivated() override; + void OnWindowDeactivated() override; + void OnWindowDpiScaleChanged() override; + + WidgetType Type = {}; + + Widget* ParentObj = nullptr; + Widget* PrevSiblingObj = nullptr; + Widget* NextSiblingObj = nullptr; + Widget* FirstChildObj = nullptr; + Widget* LastChildObj = nullptr; + + Timer* FirstTimerObj = nullptr; + + Rect FrameGeometry = Rect::xywh(0.0, 0.0, 0.0, 0.0); + Rect ContentGeometry = Rect::xywh(0.0, 0.0, 0.0, 0.0); + + Colorf WindowBackground = Colorf::fromRgba8(240, 240, 240); + + std::string WindowTitle; + std::unique_ptr DispWindow; + std::unique_ptr DispCanvas; + Widget* FocusWidget = nullptr; + Widget* CaptureWidget = nullptr; + Widget* HoverWidget = nullptr; + bool HiddenFlag = false; + + StandardCursor CurrentCursor = StandardCursor::arrow; + + std::string StyleClass = "widget"; + std::string StyleState; + typedef std::variant PropertyVariant; + std::unordered_map StyleProperties; + + Widget(const Widget&) = delete; + Widget& operator=(const Widget&) = delete; + + friend class Timer; + friend class OpenFileDialog; + friend class OpenFolderDialog; + friend class SaveFileDialog; +}; diff --git a/libraries/ZWidget/include/zwidget/systemdialogs/open_file_dialog.h b/libraries/ZWidget/include/zwidget/systemdialogs/open_file_dialog.h new file mode 100644 index 000000000..29ede966e --- /dev/null +++ b/libraries/ZWidget/include/zwidget/systemdialogs/open_file_dialog.h @@ -0,0 +1,55 @@ + +#pragma once + +#include +#include +#include + +class Widget; + +/// \brief Displays the system open file dialog. +class OpenFileDialog +{ +public: + /// \brief Constructs an open file dialog. + static std::unique_ptr Create(Widget* owner); + + virtual ~OpenFileDialog() = default; + + /// \brief Get the full path of the file selected. + /// + /// If multiple files are selected, this returns the first file. + virtual std::string Filename() const = 0; + + /// \brief Gets an array that contains one file name for each selected file. + virtual std::vector Filenames() const = 0; + + /// \brief Sets if multiple files can be selected or not. + /// \param multiselect = When true, multiple items can be selected. + virtual void SetMultiSelect(bool multiselect) = 0; + + /// \brief Sets a string containing the full path of the file selected. + virtual void SetFilename(const std::string &filename) = 0; + + /// \brief Sets the default extension to use. + virtual void SetDefaultExtension(const std::string& extension) = 0; + + /// \brief Add a filter that determines what types of files are displayed. + virtual void AddFilter(const std::string &filter_description, const std::string &filter_extension) = 0; + + /// \brief Clears all filters. + virtual void ClearFilters() = 0; + + /// \brief Sets a default filter, on a 0-based index. + virtual void SetFilterIndex(int filter_index) = 0; + + /// \brief Sets the initial directory that is displayed. + virtual void SetInitialDirectory(const std::string &path) = 0; + + /// \brief Sets the text that appears in the title bar. + virtual void SetTitle(const std::string &title) = 0; + + /// \brief Shows the file dialog. + /// \return true if the user clicks the OK button of the dialog that is displayed, false otherwise. + virtual bool Show() = 0; +}; diff --git a/libraries/ZWidget/include/zwidget/systemdialogs/open_folder_dialog.h b/libraries/ZWidget/include/zwidget/systemdialogs/open_folder_dialog.h new file mode 100644 index 000000000..9727f353b --- /dev/null +++ b/libraries/ZWidget/include/zwidget/systemdialogs/open_folder_dialog.h @@ -0,0 +1,30 @@ + +#pragma once + +#include +#include + +class Widget; + +/// \brief Displays the system folder browsing dialog +class OpenFolderDialog +{ +public: + /// \brief Constructs an browse folder dialog. + static std::unique_ptr Create(Widget*owner); + + virtual ~OpenFolderDialog() = default; + + /// \brief Get the full path of the directory selected. + virtual std::string SelectedPath() const = 0; + + /// \brief Sets the initial directory that is displayed. + virtual void SetInitialDirectory(const std::string& path) = 0; + + /// \brief Sets the text that appears in the title bar. + virtual void SetTitle(const std::string& title) = 0; + + /// \brief Shows the file dialog. + /// \return true if the user clicks the OK button of the dialog that is displayed, false otherwise. + virtual bool Show() = 0; +}; diff --git a/libraries/ZWidget/include/zwidget/systemdialogs/save_file_dialog.h b/libraries/ZWidget/include/zwidget/systemdialogs/save_file_dialog.h new file mode 100644 index 000000000..ce10a127f --- /dev/null +++ b/libraries/ZWidget/include/zwidget/systemdialogs/save_file_dialog.h @@ -0,0 +1,46 @@ + +#pragma once + +#include +#include +#include + +class Widget; + +/// \brief Displays the system save file dialog. +class SaveFileDialog +{ +public: + /// \brief Constructs a save file dialog. + static std::unique_ptr Create(Widget *owner); + + virtual ~SaveFileDialog() = default; + + /// \brief Get the full path of the file selected. + virtual std::string Filename() const = 0; + + /// \brief Sets a string containing the full path of the file selected. + virtual void SetFilename(const std::string &filename) = 0; + + /// \brief Sets the default extension to use. + virtual void SetDefaultExtension(const std::string& extension) = 0; + + /// \brief Add a filter that determines what types of files are displayed. + virtual void AddFilter(const std::string &filter_description, const std::string &filter_extension) = 0; + + /// \brief Clears all filters. + virtual void ClearFilters() = 0; + + /// \brief Sets a default filter, on a 0-based index. + virtual void SetFilterIndex(int filter_index) = 0; + + /// \brief Sets the initial directory that is displayed. + virtual void SetInitialDirectory(const std::string &path) = 0; + + /// \brief Sets the text that appears in the title bar. + virtual void SetTitle(const std::string &title) = 0; + + /// \brief Shows the file dialog. + /// \return true if the user clicks the OK button of the dialog that is displayed, false otherwise. + virtual bool Show() = 0; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h b/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h new file mode 100644 index 000000000..21ce0b697 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h @@ -0,0 +1,34 @@ + +#pragma once + +#include "../../core/widget.h" + +class CheckboxLabel : public Widget +{ +public: + CheckboxLabel(Widget* parent = nullptr); + + void SetText(const std::string& value); + const std::string& GetText() const; + + void SetChecked(bool value); + bool GetChecked() const; + void Toggle(); + + double GetPreferredHeight() const; + std::function FuncChanged; + void SetRadioStyle(bool on) { radiostyle = on; } + +protected: + void OnPaint(Canvas* canvas) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseLeave() override; + void OnKeyUp(InputKey key) override; + +private: + std::string text; + bool checked = false; + bool radiostyle = false; + bool mouseDownActive = false; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h b/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h new file mode 100644 index 000000000..7cae08c24 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h @@ -0,0 +1,31 @@ + +#pragma once + +#include "../../core/widget.h" +#include "../../core/image.h" + +enum ImageBoxMode +{ + Center, + Contain, + Cover +}; + +class ImageBox : public Widget +{ +public: + ImageBox(Widget* parent); + + void SetImage(std::shared_ptr newImage); + void SetImageMode(ImageBoxMode mode); + + double GetPreferredWidth() const; + double GetPreferredHeight() const; + +protected: + void OnPaint(Canvas* canvas) override; + +private: + std::shared_ptr image; + ImageBoxMode mode = ImageBoxMode::Center; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h b/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h new file mode 100644 index 000000000..9c5a4b198 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h @@ -0,0 +1,158 @@ + +#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; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseDoubleclick(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnKeyChar(std::string chars) override; + void OnKeyDown(InputKey key) override; + void OnKeyUp(InputKey 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; + + struct UndoInfo + { + /* set undo text when: + - added char after moving + - destructive block operation (del, cut etc) + - beginning erase + */ + std::string undo_text; + bool first_erase = false; + bool first_text_insert = false; + }; + + UndoInfo undo_info; + + bool select_all_on_focus_gain = true; + + static const std::string break_characters; + static const std::string numeric_mode_characters; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/listview/listview.h b/libraries/ZWidget/include/zwidget/widgets/listview/listview.h new file mode 100644 index 000000000..e1b08192a --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/listview/listview.h @@ -0,0 +1,46 @@ + +#pragma once + +#include "../../core/widget.h" +#include +#include + +class Scrollbar; + +class ListView : public Widget +{ +public: + ListView(Widget* parent = nullptr); + + void SetColumnWidths(const std::vector& widths); + void AddItem(const std::string& text, int index = -1, int column = 0); + void UpdateItem(const std::string& text, int index, int column = 0); + void RemoveItem(int index = -1); + size_t GetItemAmount() const { return items.size(); } + size_t GetColumnAmount() const { return columnwidths.size(); } + int GetSelectedItem() const { return selectedItem; } + void SetSelectedItem(int index); + void ScrollToItem(int index); + + void Activate(); + + std::function OnChanged; + std::function OnActivated; + +protected: + void OnPaint(Canvas* canvas) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseDoubleclick(const Point& pos, InputKey key) override; + bool OnMouseWheel(const Point& pos, InputKey key) override; + void OnKeyDown(InputKey key) override; + void OnGeometryChanged() override; + void OnScrollbarScroll(); + + static double getItemHeight(); + + Scrollbar* scrollbar = nullptr; + + std::vector> items; + std::vector columnwidths; + int selectedItem = 0; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h b/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h new file mode 100644 index 000000000..13498e56d --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h @@ -0,0 +1,33 @@ + +#pragma once + +#include "../../core/widget.h" + +class Menubar; +class Toolbar; +class Statusbar; + +class MainWindow : public Widget +{ +public: + MainWindow(RenderAPI api = RenderAPI::Unspecified); + ~MainWindow(); + + Menubar* GetMenubar() const { return MenubarWidget; } + Toolbar* GetTopToolbar() const { return TopToolbarWidget; } + Toolbar* GetLeftToolbar() const { return LeftToolbarWidget; } + Statusbar* GetStatusbar() const { return StatusbarWidget; } + Widget* GetCentralWidget() const { return CentralWidget; } + + void SetCentralWidget(Widget* widget); + +protected: + void OnGeometryChanged() override; + +private: + Menubar* MenubarWidget = nullptr; + Toolbar* TopToolbarWidget = nullptr; + Toolbar* LeftToolbarWidget = nullptr; + Widget* CentralWidget = nullptr; + Statusbar* StatusbarWidget = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h b/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h new file mode 100644 index 000000000..1881111f4 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h @@ -0,0 +1,121 @@ + +#pragma once + +#include "../../core/widget.h" +#include "../textlabel/textlabel.h" +#include "../imagebox/imagebox.h" +#include + +class Menu; +class MenubarItem; +class MenuItem; +class MenuItemSeparator; + +class Menubar : public Widget +{ +public: + Menubar(Widget* parent); + ~Menubar(); + + MenubarItem* AddItem(std::string text, std::function onOpen, bool alignRight = false); + +protected: + void OnGeometryChanged() override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnKeyDown(InputKey key) override; + +private: + void ShowMenu(MenubarItem* item); + void CloseMenu(); + + MenubarItem* GetMenubarItemAt(const Point& pos); + int GetItemIndex(MenubarItem* item); + + std::vector menuItems; + int currentMenubarItem = -1; + Menu* openMenu = nullptr; + bool modalMode = false; + + friend class MenubarItem; +}; + +class MenubarItem : public Widget +{ +public: + MenubarItem(Menubar* menubar, std::string text, bool alignRight); + + void SetOpenCallback(std::function callback) { onOpen = std::move(callback); } + const std::function& GetOpenCallback() const { return onOpen; } + + double GetPreferredWidth() const; + + bool AlignRight = false; + +protected: + void OnPaint(Canvas* canvas) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + +private: + Menubar* menubar = nullptr; + std::function onOpen; + std::string text; +}; + +class Menu : public Widget +{ +public: + Menu(Widget* parent); + + void SetLeftPosition(const Point& globalPos); + void SetRightPosition(const Point& globalPos); + MenuItem* AddItem(std::shared_ptr icon, std::string text, std::function onClick = {}); + MenuItemSeparator* AddSeparator(); + + double GetPreferredWidth() const; + double GetPreferredHeight() const; + + void SetSelected(MenuItem* item); + +protected: + void OnGeometryChanged() override; + + std::function onCloseMenu; + MenuItem* selectedItem = nullptr; + + friend class Menubar; +}; + +class MenuItem : public Widget +{ +public: + MenuItem(Menu* menu, std::function onClick); + + void Click(); + + Menu* menu = nullptr; + ImageBox* icon = nullptr; + TextLabel* text = nullptr; + +protected: + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + void OnGeometryChanged() override; + +private: + std::function onClick; +}; + +class MenuItemSeparator : public Widget +{ +public: + MenuItemSeparator(Widget* parent); + +protected: + void OnPaint(Canvas* canvas) override; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h b/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h new file mode 100644 index 000000000..d3fe0295b --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h @@ -0,0 +1,32 @@ + +#pragma once + +#include "../../core/widget.h" +#include + +class PushButton : public Widget +{ +public: + PushButton(Widget* parent = nullptr); + + void SetText(const std::string& value); + const std::string& GetText() const; + + double GetPreferredHeight() const; + + void Click(); + + std::function OnClick; + +protected: + void OnPaint(Canvas* canvas) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + void OnKeyDown(InputKey key) override; + void OnKeyUp(InputKey key) override; + +private: + std::string text; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h b/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h new file mode 100644 index 000000000..6e169b1fe --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h @@ -0,0 +1,99 @@ + +#pragma once + +#include "../../core/widget.h" +#include "../../core/timer.h" +#include + +class Scrollbar : public Widget +{ +public: + Scrollbar(Widget* parent); + ~Scrollbar(); + + bool IsVertical() const; + bool IsHorizontal() const; + double GetMin() const; + double GetMax() const; + double GetLineStep() const; + double GetPageStep() const; + double GetPosition() const; + + void SetVertical(); + void SetHorizontal(); + + void SetMin(double scroll_min); + void SetMax(double scroll_max); + void SetLineStep(double step); + void SetPageStep(double step); + + void SetRanges(double scroll_min, double scroll_max, double line_step, double page_step); + void SetRanges(double view_size, double total_size); + + void SetPosition(double pos); + + double GetPreferredWidth() const { return 16.0; } + double GetPreferredHeight() const { return 16.0; } + + std::function FuncScroll; + std::function FuncScrollMin; + std::function FuncScrollMax; + std::function FuncScrollLineDecrement; + std::function FuncScrollLineIncrement; + std::function FuncScrollPageDecrement; + std::function FuncScrollPageIncrement; + std::function FuncScrollThumbRelease; + std::function FuncScrollThumbTrack; + std::function FuncScrollEnd; + +protected: + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + void OnPaint(Canvas* canvas) override; + void OnEnableChanged() override; + void OnGeometryChanged() override; + +private: + bool UpdatePartPositions(); + double CalculateThumbSize(double track_size); + double CalculateThumbPosition(double thumb_size, double track_size); + Rect CreateRect(double start, double end); + void InvokeScrollEvent(std::function* event_ptr); + void OnTimerExpired(); + + bool vertical = true; + double scroll_min = 0.0; + double scroll_max = 1.0; + double line_step = 1.0; + double page_step = 10.0; + double position = 0.0; + + bool showbuttons = false; + + enum MouseDownMode + { + mouse_down_none, + mouse_down_button_decr, + mouse_down_button_incr, + mouse_down_track_decr, + mouse_down_track_incr, + mouse_down_thumb_drag + } mouse_down_mode = mouse_down_none; + + double thumb_start_position = 0.0; + Point mouse_drag_start_pos; + double thumb_start_pixel_position = 0.0; + + Timer* mouse_down_timer = nullptr; + double last_step_size = 0.0; + + Rect rect_button_decrement; + Rect rect_track_decrement; + Rect rect_thumb; + Rect rect_track_increment; + Rect rect_button_increment; + + std::function* FuncScrollOnMouseDown = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/statusbar/statusbar.h b/libraries/ZWidget/include/zwidget/widgets/statusbar/statusbar.h new file mode 100644 index 000000000..c03a3e282 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/statusbar/statusbar.h @@ -0,0 +1,19 @@ + +#pragma once + +#include "../../core/widget.h" + +class LineEdit; + +class Statusbar : public Widget +{ +public: + Statusbar(Widget* parent); + ~Statusbar(); + +protected: + void OnPaint(Canvas* canvas) override; + +private: + LineEdit* CommandEdit = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/tabwidget/tabwidget.h b/libraries/ZWidget/include/zwidget/widgets/tabwidget/tabwidget.h new file mode 100644 index 000000000..8682f1a73 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/tabwidget/tabwidget.h @@ -0,0 +1,129 @@ + +#pragma once + +#include "../../core/widget.h" +#include +#include +#include + +class TabBar; +class TabBarTab; +class TabBarSpacer; +class TabWidgetStack; +class TextLabel; +class ImageBox; +class Image; + +class TabWidget : public Widget +{ +public: + TabWidget(Widget* parent); + + int AddTab(Widget* page, const std::string& label); + int AddTab(Widget* page, const std::shared_ptr& icon, const std::string& label); + + void SetTabText(int index, const std::string& text); + void SetTabText(Widget* page, const std::string& text); + void SetTabIcon(int index, const std::shared_ptr& icon); + void SetTabIcon(Widget* page, const std::shared_ptr& icon); + + int GetCurrentIndex() const; + Widget* GetCurrentWidget() const; + + int GetPageIndex(Widget* pageWidget) const; + + void SetCurrentIndex(int pageIndex); + void SetCurrentWidget(Widget* pageWidget); + + std::function OnCurrentChanged; + +protected: + void OnGeometryChanged() override; + +private: + void OnBarCurrentChanged(); + + TabBar* Bar = nullptr; + TabWidgetStack* PageStack = nullptr; + std::vector Pages; +}; + +class TabBar : public Widget +{ +public: + TabBar(Widget* parent); + + int AddTab(const std::string& label); + int AddTab(const std::shared_ptr& icon, const std::string& label); + + void SetTabText(int index, const std::string& text); + void SetTabIcon(int index, const std::shared_ptr& icon); + + int GetCurrentIndex() const; + void SetCurrentIndex(int pageIndex); + + double GetPreferredHeight() const { return 30.0; } + + std::function OnCurrentChanged; + +protected: + void OnGeometryChanged() override; + +private: + void OnTabClicked(TabBarTab* tab); + int GetTabIndex(TabBarTab* tab); + + int CurrentIndex = -1; + std::vector Tabs; + TabBarSpacer* leftSpacer = nullptr; + TabBarSpacer* rightSpacer = nullptr; +}; + +class TabBarSpacer : public Widget +{ +public: + TabBarSpacer(Widget* parent); +}; + +class TabBarTab : public Widget +{ +public: + TabBarTab(Widget* parent); + + void SetText(const std::string& text); + void SetIcon(const std::shared_ptr& icon); + void SetCurrent(bool value); + + double GetPreferredWidth() const; + + std::function OnClick; + +protected: + void OnGeometryChanged() override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + +private: + bool IsCurrent = false; + + ImageBox* Icon = nullptr; + TextLabel* Label = nullptr; + bool hot = false; +}; + +class TabWidgetStack : public Widget +{ +public: + TabWidgetStack(Widget* parent); + + void SetCurrentWidget(Widget* widget); + Widget* GetCurrentWidget() const { return CurrentWidget; } + +protected: + void OnGeometryChanged() override; + +private: + Widget* CurrentWidget = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h b/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h new file mode 100644 index 000000000..f392f85d6 --- /dev/null +++ b/libraries/ZWidget/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) override; + void OnMouseMove(const Point& pos) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseDoubleclick(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnKeyChar(std::string chars) override; + void OnKeyDown(InputKey key) override; + void OnKeyUp(InputKey key) override; + void OnGeometryChanged() override; + void OnEnableChanged() override; + void OnSetFocus() override; + void OnLostFocus() override; + +private: + void LayoutLines(Canvas* canvas); + + void OnTimerExpired(); + void OnScrollTimerExpired(); + void CreateComponents(); + void OnVerticalScroll(); + void UpdateVerticalScroll(); + void MoveVerticalScroll(); + double GetTotalLineHeight(); + + struct Line + { + std::string text; + SpanLayout layout; + Rect box; + bool invalidated = true; + }; + + struct ivec2 + { + ivec2() = default; + ivec2(int x, int y) : x(x), y(y) { } + int x = 0; + int y = 0; + + bool operator==(const ivec2& b) const { return x == b.x && y == b.y; } + bool operator!=(const ivec2& b) const { return x != b.x || y != b.y; } + }; + + Scrollbar* vert_scrollbar; + Timer* timer = nullptr; + std::vector lines = { Line() }; + ivec2 cursor_pos = { 0, 0 }; + int max_length = -1; + bool mouse_selecting = false; + bool lowercase = false; + bool uppercase = false; + bool readonly = false; + ivec2 selection_start = { -1, 0 }; + int selection_length = 0; + std::string input_mask; + + static std::string break_characters; + + void Move(int steps, bool shift, bool ctrl); + void InsertText(ivec2 pos, const std::string& str); + void Backspace(); + void Del(); + ivec2 GetCharacterIndex(Point mouse_wincoords); + ivec2 FindNextBreakCharacter(ivec2 pos); + ivec2 FindPreviousBreakCharacter(ivec2 pos); + bool InputMaskAcceptsInput(ivec2 cursor_pos, const std::string& str); + + std::string::size_type ToOffset(ivec2 pos) const; + ivec2 FromOffset(std::string::size_type offset) const; + + VerticalTextPosition vertical_text_align; + Timer* scroll_timer = nullptr; + + bool mouse_moves_left = false; + bool cursor_blink_visible = true; + unsigned int blink_timer = 0; + int clip_start_offset = 0; + int clip_end_offset = 0; + bool ignore_mouse_events = false; + + struct UndoInfo + { + /* set undo text when: + - added char after moving + - destructive block operation (del, cut etc) + - beginning erase + */ + + std::string undo_text; + bool first_erase = false; + bool first_text_insert = false; + } undo_info; + + bool select_all_on_focus_gain = false; + + std::shared_ptr font = Font::Create("NotoSans", 12.0); + + template + static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h b/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h new file mode 100644 index 000000000..18cfcbf5f --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h @@ -0,0 +1,33 @@ + +#pragma once + +#include "../../core/widget.h" + +enum class TextLabelAlignment +{ + Left, + Center, + Right +}; + +class TextLabel : public Widget +{ +public: + TextLabel(Widget* parent = nullptr); + + void SetText(const std::string& value); + const std::string& GetText() const; + + void SetTextAlignment(TextLabelAlignment alignment); + TextLabelAlignment GetTextAlignment() const; + + double GetPreferredWidth() const; + double GetPreferredHeight() const; + +protected: + void OnPaint(Canvas* canvas) override; + +private: + std::string text; + TextLabelAlignment textAlignment = TextLabelAlignment::Left; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h new file mode 100644 index 000000000..fac66b97d --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h @@ -0,0 +1,31 @@ + +#pragma once + +#include "../../core/widget.h" + +enum class ToolbarDirection +{ + Horizontal, + Vertical +}; + +class ToolbarButton; + +class Toolbar : public Widget +{ +public: + Toolbar(Widget* parent); + ~Toolbar(); + + void SetDirection(ToolbarDirection direction); + ToolbarDirection GetDirection(ToolbarDirection) const { return direction; } + + ToolbarButton* AddButton(std::string icon, std::string text, std::function onClicked = {}); + +protected: + void OnGeometryChanged() override; + +private: + ToolbarDirection direction = ToolbarDirection::Horizontal; + std::vector buttons; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h new file mode 100644 index 000000000..f6e1557a1 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h @@ -0,0 +1,34 @@ + +#pragma once + +#include "../../core/widget.h" +#include "../textlabel/textlabel.h" +#include "../imagebox/imagebox.h" + +class ToolbarButton : public Widget +{ +public: + ToolbarButton(Widget* parent); + ~ToolbarButton(); + + void SetIcon(std::string icon); + void SetText(std::string text); + + void Click(); + + double GetPreferredWidth(); + double GetPreferredHeight(); + + std::function OnClick; + +protected: + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnGeometryChanged() override; + +private: + ImageBox* image = nullptr; + TextLabel* label = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/window/sdl2nativehandle.h b/libraries/ZWidget/include/zwidget/window/sdl2nativehandle.h new file mode 100644 index 000000000..9e396a80b --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/sdl2nativehandle.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +struct SDL_Window; + +class SDL2NativeHandle +{ +public: + SDL_Window* window = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/window/waylandnativehandle.h b/libraries/ZWidget/include/zwidget/window/waylandnativehandle.h new file mode 100644 index 000000000..b7358d782 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/waylandnativehandle.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class WaylandNativeHandle +{ +public: + wl_display* display = nullptr; + wl_surface* surface = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/window/win32nativehandle.h b/libraries/ZWidget/include/zwidget/window/win32nativehandle.h new file mode 100644 index 000000000..979ac63a1 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/win32nativehandle.h @@ -0,0 +1,9 @@ +#pragma once + +typedef struct HWND__* HWND; + +class Win32NativeHandle +{ +public: + HWND hwnd = {}; +}; diff --git a/libraries/ZWidget/include/zwidget/window/window.h b/libraries/ZWidget/include/zwidget/window/window.h new file mode 100644 index 000000000..ad59f1f02 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/window.h @@ -0,0 +1,243 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "../core/rect.h" + +#ifndef VULKAN_H_ + +#define VK_DEFINE_HANDLE(object) typedef struct object##_T* object; + +#if defined(__LP64__) || defined(_WIN64) || defined(__x86_64__) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__) +#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef struct object##_T *object; +#else +#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; +#endif + +VK_DEFINE_HANDLE(VkInstance) +VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkSurfaceKHR) + +#endif + +class Widget; +class OpenFileDialog; +class SaveFileDialog; +class OpenFolderDialog; + +enum class StandardCursor +{ + arrow, + appstarting, + cross, + hand, + ibeam, + no, + size_all, + size_nesw, + size_ns, + size_nwse, + size_we, + uparrow, + wait +}; + +enum class InputKey : uint32_t +{ + None, LeftMouse, RightMouse, Cancel, + MiddleMouse, Unknown05, Unknown06, Unknown07, + Backspace, Tab, Unknown0A, Unknown0B, + Unknown0C, Enter, Unknown0E, Unknown0F, + Shift, Ctrl, Alt, Pause, + CapsLock, Unknown15, Unknown16, Unknown17, + Unknown18, Unknown19, Unknown1A, Escape, + Unknown1C, Unknown1D, Unknown1E, Unknown1F, + Space, PageUp, PageDown, End, + Home, Left, Up, Right, + Down, Select, Print, Execute, + PrintScrn, Insert, Delete, Help, + _0, _1, _2, _3, + _4, _5, _6, _7, + _8, _9, Unknown3A, Unknown3B, + Unknown3C, Unknown3D, Unknown3E, Unknown3F, + Unknown40, A, B, C, + D, E, F, G, + H, I, J, K, + L, M, N, O, + P, Q, R, S, + T, U, V, W, + X, Y, Z, Unknown5B, + Unknown5C, Unknown5D, Unknown5E, Unknown5F, + NumPad0, NumPad1, NumPad2, NumPad3, + NumPad4, NumPad5, NumPad6, NumPad7, + NumPad8, NumPad9, GreyStar, GreyPlus, + Separator, GreyMinus, NumPadPeriod, GreySlash, + F1, F2, F3, F4, + F5, F6, F7, F8, + F9, F10, F11, F12, + F13, F14, F15, F16, + F17, F18, F19, F20, + F21, F22, F23, F24, + Unknown88, Unknown89, Unknown8A, Unknown8B, + Unknown8C, Unknown8D, Unknown8E, Unknown8F, + NumLock, ScrollLock, Unknown92, Unknown93, + Unknown94, Unknown95, Unknown96, Unknown97, + Unknown98, Unknown99, Unknown9A, Unknown9B, + Unknown9C, Unknown9D, Unknown9E, Unknown9F, + LShift, RShift, LControl, RControl, + UnknownA4, UnknownA5, UnknownA6, UnknownA7, + UnknownA8, UnknownA9, UnknownAA, UnknownAB, + UnknownAC, UnknownAD, UnknownAE, UnknownAF, + UnknownB0, UnknownB1, UnknownB2, UnknownB3, + UnknownB4, UnknownB5, UnknownB6, UnknownB7, + UnknownB8, UnknownB9, Semicolon, Equals, + Comma, Minus, Period, Slash, + Tilde, UnknownC1, UnknownC2, UnknownC3, + UnknownC4, UnknownC5, UnknownC6, UnknownC7, + Joy1, Joy2, Joy3, Joy4, + Joy5, Joy6, Joy7, Joy8, + Joy9, Joy10, Joy11, Joy12, + Joy13, Joy14, Joy15, Joy16, + UnknownD8, UnknownD9, UnknownDA, LeftBracket, + Backslash, RightBracket, SingleQuote, UnknownDF, + JoyX, JoyY, JoyZ, JoyR, + MouseX, MouseY, MouseZ, MouseW, + JoyU, JoyV, UnknownEA, UnknownEB, + MouseWheelUp, MouseWheelDown, Unknown10E, Unknown10F, + JoyPovUp, JoyPovDown, JoyPovLeft, JoyPovRight, + UnknownF4, UnknownF5, Attn, CrSel, + ExSel, ErEof, Play, Zoom, + NoName, PA1, OEMClear +}; + +enum class RenderAPI +{ + Unspecified, + Bitmap, + Vulkan, + OpenGL, + D3D11, + D3D12, + Metal +}; + +class DisplayWindow; + +class DisplayWindowHost +{ +public: + virtual void OnWindowPaint() = 0; + virtual void OnWindowMouseMove(const Point& pos) = 0; + virtual void OnWindowMouseLeave() = 0; + virtual void OnWindowMouseDown(const Point& pos, InputKey key) = 0; + virtual void OnWindowMouseDoubleclick(const Point& pos, InputKey key) = 0; + virtual void OnWindowMouseUp(const Point& pos, InputKey key) = 0; + virtual void OnWindowMouseWheel(const Point& pos, InputKey key) = 0; + virtual void OnWindowRawMouseMove(int dx, int dy) = 0; + virtual void OnWindowKeyChar(std::string chars) = 0; + virtual void OnWindowKeyDown(InputKey key) = 0; + virtual void OnWindowKeyUp(InputKey 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, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI); + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + + static Size GetScreenSize(); + + virtual ~DisplayWindow() = default; + + virtual void SetWindowTitle(const std::string& text) = 0; + virtual void SetWindowFrame(const Rect& box) = 0; + virtual void SetClientFrame(const Rect& box) = 0; + virtual void Show() = 0; + virtual void ShowFullscreen() = 0; + virtual void ShowMaximized() = 0; + virtual void ShowMinimized() = 0; + virtual void ShowNormal() = 0; + virtual bool IsWindowFullscreen() = 0; + virtual void Hide() = 0; + virtual void Activate() = 0; + virtual void ShowCursor(bool enable) = 0; + virtual void LockCursor() = 0; + virtual void UnlockCursor() = 0; + virtual void CaptureMouse() = 0; + virtual void ReleaseMouseCapture() = 0; + virtual void Update() = 0; + virtual bool GetKeyState(InputKey key) = 0; + + virtual void SetCursor(StandardCursor cursor) = 0; + + virtual Rect GetWindowFrame() const = 0; + virtual Size GetClientSize() const = 0; + virtual int GetPixelWidth() const = 0; + virtual int GetPixelHeight() const = 0; + virtual double GetDpiScale() const = 0; + + virtual Point MapFromGlobal(const Point& pos) const = 0; + virtual Point MapToGlobal(const Point& pos) const = 0; + + virtual void SetBorderColor(uint32_t bgra8) = 0; + virtual void SetCaptionColor(uint32_t bgra8) = 0; + virtual void SetCaptionTextColor(uint32_t bgra8) = 0; + + virtual void PresentBitmap(int width, int height, const uint32_t* pixels) = 0; + + virtual std::string GetClipboardText() = 0; + virtual void SetClipboardText(const std::string& text) = 0; + + virtual void* GetNativeHandle() = 0; + + virtual std::vector GetVulkanInstanceExtensions() = 0; + virtual VkSurfaceKHR CreateVulkanSurface(VkInstance instance) = 0; +}; + +class DisplayBackend +{ +public: + static DisplayBackend* Get(); + static void Set(std::unique_ptr instance); + + static std::unique_ptr TryCreateWin32(); + static std::unique_ptr TryCreateSDL2(); + static std::unique_ptr TryCreateX11(); + static std::unique_ptr TryCreateWayland(); + + static std::unique_ptr TryCreateBackend(); + + virtual ~DisplayBackend() = default; + + virtual bool IsWin32() { return false; } + virtual bool IsSDL2() { return false; } + virtual bool IsX11() { return false; } + virtual bool IsWayland() { return false; } + + virtual std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) = 0; + virtual void ProcessEvents() = 0; + virtual void RunLoop() = 0; + virtual void ExitLoop() = 0; + + virtual void* StartTimer(int timeoutMilliseconds, std::function onTimer) = 0; + virtual void StopTimer(void* timerID) = 0; + + virtual Size GetScreenSize() = 0; + + virtual std::unique_ptr CreateOpenFileDialog(DisplayWindow* owner); + virtual std::unique_ptr CreateSaveFileDialog(DisplayWindow* owner); + virtual std::unique_ptr CreateOpenFolderDialog(DisplayWindow* owner); +}; diff --git a/libraries/ZWidget/include/zwidget/window/x11nativehandle.h b/libraries/ZWidget/include/zwidget/window/x11nativehandle.h new file mode 100644 index 000000000..f78247b2f --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/x11nativehandle.h @@ -0,0 +1,13 @@ +#pragma once + +typedef struct _XDisplay Display; +typedef unsigned long XID; +typedef unsigned long VisualID; +typedef XID Window; + +class X11NativeHandle +{ +public: + Display* display = nullptr; + Window window = 0; +}; diff --git a/libraries/ZWidget/src/core/canvas.cpp b/libraries/ZWidget/src/core/canvas.cpp new file mode 100644 index 000000000..bc72b8f19 --- /dev/null +++ b/libraries/ZWidget/src/core/canvas.cpp @@ -0,0 +1,1084 @@ + +#include "core/canvas.h" +#include "core/rect.h" +#include "core/colorf.h" +#include "core/utf8reader.h" +#include "core/resourcedata.h" +#include "core/image.h" +#include "core/truetypefont.h" +#include "core/pathfill.h" +#include "window/window.h" +#include +#include +#include +#include + +#if defined(__SSE2__) || defined(_M_X64) +#include +#define USE_SSE2 +#endif + +//////////////////////////////////////////////////////////////////////////// + +class CanvasGlyph +{ +public: + struct + { + double leftSideBearing = 0.0; + double yOffset = 0.0; + double advanceWidth = 0.0; + } metrics; + + double u = 0.0; + double v = 0.0; + double uvwidth = 0.0f; + double uvheight = 0.0f; + std::shared_ptr texture; +}; + +class CanvasFont +{ +public: + CanvasFont(const std::string& fontname, double height, std::vector data); + ~CanvasFont(); + + CanvasGlyph* getGlyph(Canvas* canvas, uint32_t utfchar); + +private: + std::unique_ptr ttf; + + std::string fontname; + double height = 0.0; + + TrueTypeTextMetrics textmetrics; + std::unordered_map> glyphs; + + friend class CanvasFontGroup; +}; + +class CanvasFontGroup +{ +public: + struct SingleFont + { + std::unique_ptr font; + std::string language; + }; + + CanvasFontGroup(const std::string& fontname, double height); + + CanvasGlyph* getGlyph(Canvas* canvas, uint32_t utfchar, const char* lang = nullptr); + TrueTypeTextMetrics& GetTextMetrics(); + + double height; + std::vector fonts; +}; + +//////////////////////////////////////////////////////////////////////////// + +CanvasFont::CanvasFont(const std::string& fontname, double height, std::vector data) : fontname(fontname), height(height) +{ + auto tdata = std::make_shared(std::move(data)); + ttf = std::make_unique(tdata); + textmetrics = ttf->GetTextMetrics(height); +} + +CanvasFont::~CanvasFont() +{ +} + +CanvasGlyph* CanvasFont::getGlyph(Canvas* canvas, uint32_t utfchar) +{ + uint32_t glyphIndex = ttf->GetGlyphIndex(utfchar); + if (glyphIndex == 0) return nullptr; + + auto& glyph = glyphs[glyphIndex]; + if (glyph) + return glyph.get(); + + glyph = std::make_unique(); + + TrueTypeGlyph ttfglyph = ttf->LoadGlyph(glyphIndex, height); + + // Create final subpixel version + int w = ttfglyph.width; + int h = ttfglyph.height; + int destwidth = (w + 2) / 3; + auto texture = std::make_shared(); + std::vector data(destwidth * h); + + uint8_t* grayscale = ttfglyph.grayscale.get(); + uint32_t* dest = data.data(); + for (int y = 0; y < h; y++) + { + uint8_t* sline = grayscale + y * w; + uint32_t* dline = dest + y * destwidth; + for (int x = 0; x < w; x += 3) + { + uint32_t values[5] = + { + x > 0 ? sline[x - 1] : 0U, + sline[x], + x + 1 < w ? sline[x + 1] : 0U, + x + 2 < w ? sline[x + 2] : 0U, + x + 3 < w ? sline[x + 3] : 0U + }; + + uint32_t red = (values[0] + values[1] + values[1] + values[2] + 2) >> 2; + uint32_t green = (values[1] + values[2] + values[2] + values[3] + 2) >> 2; + uint32_t blue = (values[2] + values[3] + values[3] + values[4] + 2) >> 2; + uint32_t alpha = (red | green | blue) ? 255 : 0; + + *(dline++) = (alpha << 24) | (red << 16) | (green << 8) | blue; + } + } + + glyph->u = 0.0; + glyph->v = 0.0; + glyph->uvwidth = destwidth; + glyph->uvheight = h; + glyph->texture = canvas->createTexture(destwidth, h, data.data(), ImageFormat::B8G8R8A8); + + glyph->metrics.advanceWidth = (ttfglyph.advanceWidth + 2) / 3; + glyph->metrics.leftSideBearing = (ttfglyph.leftSideBearing + 2) / 3; + glyph->metrics.yOffset = ttfglyph.yOffset; + + return glyph.get(); +} + +//////////////////////////////////////////////////////////////////////////// + +CanvasFontGroup::CanvasFontGroup(const std::string& fontname, double height) : height(height) +{ + auto fontdata = LoadWidgetFontData(fontname); + fonts.resize(fontdata.size()); + for (size_t i = 0; i < fonts.size(); i++) + { + fonts[i].font = std::make_unique(fontname, height, fontdata[i].fontdata); + fonts[i].language = fontdata[i].language; + } +} + +CanvasGlyph* CanvasFontGroup::getGlyph(Canvas* canvas, uint32_t utfchar, const char* lang) +{ + for (int i = 0; i < 2; i++) + { + for (auto& fd : fonts) + { + if (i == 1 || lang == nullptr || *lang == 0 || fd.language.empty() || fd.language == lang) + { + auto g = fd.font->getGlyph(canvas, utfchar); + if (g) return g; + } + } + } + + return nullptr; +} + +TrueTypeTextMetrics& CanvasFontGroup::GetTextMetrics() +{ + return fonts[0].font->textmetrics; +} + +//////////////////////////////////////////////////////////////////////////// + +Canvas::Canvas() +{ +} + +Canvas::~Canvas() +{ +} + +void Canvas::attach(DisplayWindow* newWindow) +{ + window = newWindow; + uiscale = window->GetDpiScale(); + uint32_t white = 0xffffffff; + whiteTexture = createTexture(1, 1, &white); + font = std::make_unique("NotoSans", 13.0 * uiscale); +} + +void Canvas::detach() +{ +} + +void Canvas::begin(const Colorf& color) +{ + uiscale = window->GetDpiScale(); + width = window->GetPixelWidth(); + height = window->GetPixelHeight(); +} + +Point Canvas::getOrigin() +{ + return origin; +} + +void Canvas::setOrigin(const Point& newOrigin) +{ + origin = newOrigin; +} + +void Canvas::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 Canvas::popClip() +{ + clipStack.pop_back(); +} + +void Canvas::fillRect(const Rect& box, const Colorf& color) +{ + fillTile((float)((origin.x + box.x) * uiscale), (float)((origin.y + box.y) * uiscale), (float)(box.width * uiscale), (float)(box.height * uiscale), color); +} + +void Canvas::drawImage(const std::shared_ptr& image, const Point& pos) +{ + auto& texture = imageTextures[image]; + if (!texture) + { + texture = createTexture(image->GetWidth(), image->GetHeight(), image->GetData(), image->GetFormat()); + } + Colorf color(1.0f, 1.0f, 1.0f); + drawTile(texture.get(), (float)((origin.x + pos.x) * uiscale), (float)((origin.y + pos.y) * uiscale), (float)(texture->Width * uiscale), (float)(texture->Height * uiscale), 0.0, 0.0, (float)texture->Width, (float)texture->Height, color); +} + +void Canvas::drawImage(const std::shared_ptr& image, const Rect& box) +{ + auto& texture = imageTextures[image]; + if (!texture) + { + texture = createTexture(image->GetWidth(), image->GetHeight(), image->GetData(), image->GetFormat()); + } + Colorf color(1.0f, 1.0f, 1.0f); + drawTile(texture.get(), (float)((origin.x + box.x) * uiscale), (float)((origin.y + box.y) * uiscale), (float)(box.width * uiscale), (float)(box.height * uiscale), 0.0, 0.0, (float)texture->Width, (float)texture->Height, color); +} + +void Canvas::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 Canvas::drawText(const Point& pos, const Colorf& color, const std::string& text) +{ + double x = std::round((origin.x + pos.x) * uiscale); + double y = std::round((origin.y + pos.y) * uiscale); + + UTF8Reader reader(text.data(), text.size()); + while (!reader.is_end()) + { + CanvasGlyph* glyph = font->getGlyph(this, reader.character(), language.c_str()); + if (!glyph || !glyph->texture) + { + glyph = font->getGlyph(this, 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(); + } +} + +void Canvas::drawText(const std::shared_ptr& font, const Point& pos, const std::string& text, const Colorf& color) +{ + drawText(pos, color, text); +} + +void Canvas::drawTextEllipsis(const std::shared_ptr& font, const Point& pos, const Rect& clipBox, const std::string& text, const Colorf& color) +{ + drawText(pos, color, text); +} + +Rect Canvas::measureText(const std::shared_ptr& font, const std::string& text) +{ + return measureText(text); +} + +Rect Canvas::measureText(const std::string& text) +{ + double x = 0.0; + double y = font->GetTextMetrics().ascender - font->GetTextMetrics().descender; + + UTF8Reader reader(text.data(), text.size()); + while (!reader.is_end()) + { + CanvasGlyph* glyph = font->getGlyph(this, reader.character(), language.c_str()); + if (!glyph || !glyph->texture) + { + glyph = font->getGlyph(this, 32); + } + + x += std::round(glyph->metrics.advanceWidth); + reader.next(); + } + + return Rect::xywh(0.0, 0.0, x / uiscale, y / uiscale); +} + +FontMetrics Canvas::getFontMetrics(const std::shared_ptr& font) +{ + 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 Canvas::getCharacterIndex(const std::shared_ptr& font, const std::string& text, const Point& hitPoint) +{ + return 0; +} + +VerticalTextPosition Canvas::verticalTextAlign() +{ + VerticalTextPosition align; + align.top = 0.0f; + auto tm = font->GetTextMetrics(); + align.baseline = tm.ascender / uiscale; + align.bottom = (tm.ascender - tm.descender) / uiscale; + return align; +} + +void Canvas::drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color) +{ + if (p0.x == p1.x) + { + fillTile((float)((p0.x - 0.5) * uiscale), (float)(p0.y * uiscale), (float)uiscale, (float)((p1.y - p0.y) * uiscale), color); + } + else if (p0.y == p1.y) + { + fillTile((float)(p0.x * uiscale), (float)((p0.y - 0.5) * uiscale), (float)((p1.x - p0.x) * uiscale), (float)uiscale, color); + } + else + { + drawLineAntialiased((float)(p0.x * uiscale), (float)(p0.y * uiscale), (float)(p1.x * uiscale), (float)(p1.y * uiscale), color); + } +} + +int Canvas::getClipMinX() const +{ + return clipStack.empty() ? 0 : (int)std::round(std::max(clipStack.back().x * uiscale, 0.0)); +} + +int Canvas::getClipMinY() const +{ + return clipStack.empty() ? 0 : (int)std::round(std::max(clipStack.back().y * uiscale, 0.0)); +} + +int Canvas::getClipMaxX() const +{ + return clipStack.empty() ? width : (int)std::round(std::min((clipStack.back().x + clipStack.back().width) * uiscale, (double)width)); +} + +int Canvas::getClipMaxY() const +{ + return clipStack.empty() ? height : (int)std::round(std::min((clipStack.back().y + clipStack.back().height) * uiscale, (double)height)); +} + +///////////////////////////////////////////////////////////////////////////// + +class BitmapTexture : public CanvasTexture +{ +public: + std::vector Data; +}; + +class BitmapCanvas : public Canvas +{ +public: + void begin(const Colorf& color) override; + void end() override; + + void fillTile(float x, float y, float width, float height, Colorf color) override; + void drawTile(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) override; + void drawGlyph(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) override; + void drawLineAntialiased(float x0, float y0, float x1, float y1, Colorf color) override; + void plot(float x, float y, float alpha, const Colorf& color); + + std::unique_ptr createTexture(int width, int height, const void* pixels, ImageFormat format = ImageFormat::B8G8R8A8) override; + + std::vector pixels; +}; + +std::unique_ptr BitmapCanvas::createTexture(int width, int height, const void* pixels, ImageFormat format) +{ + auto texture = std::make_unique(); + texture->Width = width; + texture->Height = height; + texture->Data.resize(width * height); + if (format == ImageFormat::B8G8R8A8) + { + memcpy(texture->Data.data(), pixels, width * height * sizeof(uint32_t)); + } + else + { + const uint32_t* src = (const uint32_t*)pixels; + uint32_t* dest = texture->Data.data(); + int count = width * height; + for (int i = 0; i < count; i++) + { + uint32_t a = (src[i] >> 24) & 0xff; + uint32_t b = (src[i] >> 16) & 0xff; + uint32_t g = (src[i] >> 8) & 0xff; + uint32_t r = src[i] & 0xff; + dest[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + } + return texture; +} + +void BitmapCanvas::plot(float x, float y, float alpha, const Colorf& color) +{ + int xx = (int)x; + int yy = (int)y; + + int dwidth = width; + int dheight = height; + uint32_t* dest = pixels.data() + xx + yy * dwidth; + + 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 * alpha * 256.0f, 0.0f, 256.0f); + uint32_t invalpha = 256 - calpha; + + uint32_t dpixel = *dest; + uint32_t dalpha = dpixel >> 24; + uint32_t dred = (dpixel >> 16) & 0xff; + uint32_t dgreen = (dpixel >> 8) & 0xff; + uint32_t dblue = dpixel & 0xff; + + // dest.rgba = color.rgba + dest.rgba * (1-color.a) + uint32_t a = (calpha * calpha + dalpha * invalpha + 127) >> 8; + uint32_t r = (cred * calpha + dred * invalpha + 127) >> 8; + uint32_t g = (cgreen * calpha + dgreen * invalpha + 127) >> 8; + uint32_t b = (cblue * calpha + dblue * invalpha + 127) >> 8; + *dest = (a << 24) | (r << 16) | (g << 8) | b; +} + +static float fpart(float x) +{ + return x - std::floor(x); +} + +static float rfpart(float x) +{ + return 1 - fpart(x); +} + +void BitmapCanvas::drawLineAntialiased(float x0, float y0, float x1, float y1, Colorf color) +{ + bool steep = std::abs(y1 - y0) > std::abs(x1 - x0); + + if (steep) + { + std::swap(x0, y0); + std::swap(x1, y1); + } + + if (x0 > x1) + { + std::swap(x0, x1); + std::swap(y0, y1); + } + + float dx = x1 - x0; + float dy = y1 - y0; + float gradient = (dx == 0.0f) ? 1.0f : dy / dx; + + // handle first endpoint + float xend = std::round(x0); + float yend = y0 + gradient * (xend - x0); + float xgap = rfpart(x0 + 0.5f); + float xpxl1 = xend; // this will be used in the main loop + float ypxl1 = std::floor(yend); + if (steep) + { + plot(ypxl1, xpxl1, rfpart(yend) * xgap, color); + plot(ypxl1 + 1, xpxl1, fpart(yend) * xgap, color); + } + else + { + plot(xpxl1, ypxl1, rfpart(yend) * xgap, color); + plot(xpxl1, ypxl1 + 1, fpart(yend) * xgap, color); + } + float intery = yend + gradient; // first y-intersection for the main loop + + // handle second endpoint + xend = std::floor(x1 + 0.5f); + yend = y1 + gradient * (xend - x1); + xgap = fpart(x1 + 0.5f); + float xpxl2 = xend; // this will be used in the main loop + float ypxl2 = std::floor(yend); + if (steep) + { + plot(ypxl2, xpxl2, rfpart(yend) * xgap, color); + plot(ypxl2 + 1.0f, xpxl2, fpart(yend) * xgap, color); + } + else + { + plot(xpxl2, ypxl2, rfpart(yend) * xgap, color); + plot(xpxl2, ypxl2 + 1.0f, fpart(yend) * xgap, color); + } + + // main loop + if (steep) + { + for (float x = xpxl1 + 1.0f; x <= xpxl2 - 1.0f; x++) + { + plot(std::floor(intery), x, rfpart(intery), color); + plot(std::floor(intery) + 1.0f, x, fpart(intery), color); + intery = intery + gradient; + } + } + else + { + for (float x = xpxl1 + 1.0f; x <= xpxl2 - 1.0f; x++) + { + plot(x, std::floor(intery), rfpart(intery), color); + plot(x, std::floor(intery) + 1, fpart(intery), color); + intery = intery + gradient; + } + } +} + +void BitmapCanvas::fillTile(float left, float top, float width, float height, Colorf color) +{ + if (width <= 0.0f || height <= 0.0f || color.a <= 0.0f) + return; + + int dwidth = this->width; + int dheight = this->height; + uint32_t* dest = this->pixels.data(); + + int x0 = (int)left; + int x1 = (int)(left + width); + int y0 = (int)top; + int y1 = (int)(top + height); + + x0 = std::max(x0, getClipMinX()); + y0 = std::max(y0, getClipMinY()); + x1 = std::min(x1, getClipMaxX()); + y1 = std::min(y1, getClipMaxY()); + if (x1 <= x0 || y1 <= y0) + return; + + uint32_t cred = (int32_t)clamp(color.r * 255.0f, 0.0f, 255.0f); + uint32_t cgreen = (int32_t)clamp(color.g * 255.0f, 0.0f, 255.0f); + uint32_t cblue = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); + uint32_t calpha = (int32_t)clamp(color.a * 255.0f, 0.0f, 255.0f); + uint32_t invalpha = 256 - (calpha + (calpha >> 7)); + + if (invalpha == 0) // Solid fill + { + uint32_t c = (calpha << 24) | (cred << 16) | (cgreen << 8) | cblue; +#ifdef USE_SSE2 + __m128i cargb = _mm_set1_epi32(c); +#endif + + for (int y = y0; y < y1; y++) + { + uint32_t* dline = dest + y * dwidth; + + int x = x0; +#ifdef USE_SSE2 + int ssex1 = x0 + (((x1 - x0) >> 2) << 2); + while (x < ssex1) + { + _mm_storeu_si128((__m128i*)(dline + x), cargb); + x += 4; + } +#endif + + while (x < x1) + { + dline[x] = c; + x++; + } + } + } + else // Alpha blended fill + { + cred <<= 8; + cgreen <<= 8; + cblue <<= 8; + calpha <<= 8; +#ifdef USE_SSE2 + __m128i cargb = _mm_set_epi16(calpha, cred, cgreen, cblue, calpha, cred, cgreen, cblue); + __m128i cinvalpha = _mm_set1_epi16(invalpha); +#endif + + for (int y = y0; y < y1; y++) + { + uint32_t* dline = dest + y * dwidth; + + int x = x0; +#ifdef USE_SSE2 + int ssex1 = x0 + (((x1 - x0) >> 1) << 1); + while (x < ssex1) + { + __m128i dpixel = _mm_loadl_epi64((const __m128i*)(dline + x)); + dpixel = _mm_unpacklo_epi8(dpixel, _mm_setzero_si128()); + + // dest.rgba = color.rgba + dest.rgba * (1-color.a) + __m128i result = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(cargb, _mm_mullo_epi16(dpixel, cinvalpha)), _mm_set1_epi16(127)), 8); + _mm_storel_epi64((__m128i*)(dline + x), _mm_packus_epi16(result, _mm_setzero_si128())); + x += 2; + } +#endif + + while (x < x1) + { + 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; + + // dest.rgba = color.rgba + dest.rgba * (1-color.a) + uint32_t a = (calpha + dalpha * invalpha + 127) >> 8; + uint32_t r = (cred + dred * invalpha + 127) >> 8; + uint32_t g = (cgreen + dgreen * invalpha + 127) >> 8; + uint32_t b = (cblue + dblue * invalpha + 127) >> 8; + dline[x] = (a << 24) | (r << 16) | (g << 8) | b; + x++; + } + } + } +} + +void BitmapCanvas::drawTile(CanvasTexture* tex, float left, float top, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) +{ + if (width <= 0.0f || height <= 0.0f || color.a <= 0.0f) + return; + + auto texture = static_cast(tex); + int swidth = texture->Width; + int sheight = texture->Height; + const uint32_t* src = texture->Data.data(); + + int dwidth = this->width; + int dheight = this->height; + uint32_t* dest = this->pixels.data(); + + int x0 = (int)left; + int x1 = (int)(left + width); + int y0 = (int)top; + int y1 = (int)(top + height); + + x0 = std::max(x0, getClipMinX()); + y0 = std::max(y0, getClipMinY()); + x1 = std::min(x1, getClipMaxX()); + y1 = std::min(y1, getClipMaxY()); + if (x1 <= x0 || y1 <= y0) + return; + + uint32_t cred = (int32_t)clamp(color.r * 256.0f, 0.0f, 256.0f); + uint32_t cgreen = (int32_t)clamp(color.g * 256.0f, 0.0f, 256.0f); + uint32_t cblue = (int32_t)clamp(color.b * 256.0f, 0.0f, 256.0f); + uint32_t calpha = (int32_t)clamp(color.a * 256.0f, 0.0f, 256.0f); +#ifdef USE_SSE2 + __m128i cargb = _mm_set_epi16(calpha, cred, cgreen, cblue, calpha, cred, cgreen, cblue); +#endif + + float uscale = uvwidth / width; + float vscale = uvheight / height; + + for (int y = y0; y < y1; y++) + { + float vpix = v + vscale * (y + 0.5f - top); + const uint32_t* sline = src + ((int)vpix) * swidth; + uint32_t* dline = dest + y * dwidth; + + int x = x0; +#ifdef USE_SSE2 + int ssex1 = x0 + (((x1 - x0) >> 1) << 1); + while (x < ssex1) + { + float upix0 = u + uscale * (x + 0.5f - left); + float upix1 = u + uscale * (x + 1 + 0.5f - left); + uint32_t spixel0 = sline[(int)upix0]; + uint32_t spixel1 = sline[(int)upix1]; + __m128i spixel = _mm_set_epi32(0, 0, spixel1, spixel0); + spixel = _mm_unpacklo_epi8(spixel, _mm_setzero_si128()); + + __m128i dpixel = _mm_loadl_epi64((const __m128i*)(dline + x)); + dpixel = _mm_unpacklo_epi8(dpixel, _mm_setzero_si128()); + + // Pixel shade + spixel = _mm_srli_epi16(_mm_add_epi16(_mm_mullo_epi16(spixel, cargb), _mm_set1_epi16(127)), 8); + + // Rescale from [0,255] to [0,256] + __m128i sa = _mm_shufflehi_epi16(_mm_shufflelo_epi16(spixel, _MM_SHUFFLE(3, 3, 3, 3)), _MM_SHUFFLE(3, 3, 3, 3)); + sa = _mm_add_epi16(sa, _mm_srli_epi16(sa, 7)); + __m128i sinva = _mm_sub_epi16(_mm_set1_epi16(256), sa); + + // dest.rgba = color.rgba * src.rgba * src.a + dest.rgba * (1-src.a) + __m128i result = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(_mm_mullo_epi16(spixel, sa), _mm_mullo_epi16(dpixel, sinva)), _mm_set1_epi16(127)), 8); + _mm_storel_epi64((__m128i*)(dline + x), _mm_packus_epi16(result, _mm_setzero_si128())); + x += 2; + } +#endif + + while (x < x1) + { + float upix = u + uscale * (x + 0.5f - left); + uint32_t spixel = sline[(int)upix]; + uint32_t salpha = spixel >> 24; + uint32_t sred = (spixel >> 16) & 0xff; + uint32_t sgreen = (spixel >> 8) & 0xff; + uint32_t sblue = spixel & 0xff; + + uint32_t dpixel = dline[x]; + uint32_t dalpha = dpixel >> 24; + uint32_t dred = (dpixel >> 16) & 0xff; + uint32_t dgreen = (dpixel >> 8) & 0xff; + uint32_t dblue = dpixel & 0xff; + + // Pixel shade + sred = (cred * sred + 127) >> 8; + sgreen = (cgreen * sgreen + 127) >> 8; + sblue = (cblue * sblue + 127) >> 8; + salpha = (calpha * salpha + 127) >> 8; + + // Rescale from [0,255] to [0,256] + uint32_t sa = salpha + (salpha >> 7); + uint32_t sinva = 256 - sa; + + // dest.rgba = color.rgba * src.rgba * src.a + dest.rgba * (1-src.a) + uint32_t a = (salpha * sa + dalpha * sinva + 127) >> 8; + uint32_t r = (sred * sa + dred * sinva + 127) >> 8; + uint32_t g = (sgreen * sa + dgreen * sinva + 127) >> 8; + uint32_t b = (sblue * sa + dblue * sinva + 127) >> 8; + dline[x] = (a << 24) | (r << 16) | (g << 8) | b; + x++; + } + } +} + +void BitmapCanvas::drawGlyph(CanvasTexture* tex, float left, float top, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) +{ + if (width <= 0.0f || height <= 0.0f) + return; + + auto texture = static_cast(tex); + int swidth = texture->Width; + int sheight = texture->Height; + const uint32_t* src = texture->Data.data(); + + int dwidth = this->width; + int dheight = this->height; + uint32_t* dest = this->pixels.data(); + + int x0 = (int)left; + int x1 = (int)(left + width); + int y0 = (int)top; + int y1 = (int)(top + height); + + x0 = std::max(x0, getClipMinX()); + y0 = std::max(y0, getClipMinY()); + x1 = std::min(x1, getClipMaxX()); + y1 = std::min(y1, getClipMaxY()); + if (x1 <= x0 || y1 <= y0) + return; + +#if 1 // Use gamma correction + + // To linear + float cred = color.r * color.r; // std::pow(color.r, 2.2f); + float cgreen = color.g * color.g; // std::pow(color.g, 2.2f); + float cblue = color.b * color.b; // std::pow(color.b, 2.2f); +#ifdef USE_SSE2 + __m128 crgba = _mm_set_ps(0.0f, cred, cgreen, cblue); +#endif + + float uscale = uvwidth / width; + float vscale = uvheight / height; + + for (int y = y0; y < y1; y++) + { + float vpix = v + vscale * (y + 0.5f - top); + const uint32_t* sline = src + ((int)vpix) * swidth; + uint32_t* dline = dest + y * dwidth; + + int x = x0; +#ifdef USE_SSE2 + while (x < x1) + { + float upix = u + uscale * (x + 0.5f - left); + __m128i spixel = _mm_cvtsi32_si128(sline[(int)upix]); + spixel = _mm_unpacklo_epi8(spixel, _mm_setzero_si128()); + spixel = _mm_unpacklo_epi16(spixel, _mm_setzero_si128()); + __m128 srgba = _mm_mul_ps(_mm_cvtepi32_ps(spixel), _mm_set_ps1(1.0f / 255.0f)); + + __m128i dpixel = _mm_cvtsi32_si128(dline[x]); + dpixel = _mm_unpacklo_epi8(dpixel, _mm_setzero_si128()); + dpixel = _mm_unpacklo_epi16(dpixel, _mm_setzero_si128()); + __m128 drgba = _mm_mul_ps(_mm_cvtepi32_ps(dpixel), _mm_set_ps1(1.0f / 255.0f)); + + // To linear + drgba = _mm_mul_ps(drgba, drgba); + + // dest.rgb = color.rgb * src.rgb + dest.rgb * (1-src.rgb) + __m128 frgba = _mm_add_ps(_mm_mul_ps(crgba, srgba), _mm_mul_ps(drgba, _mm_sub_ps(_mm_set_ps1(1.0f), srgba))); + + // To srgb + frgba = _mm_sqrt_ps(frgba); + + __m128i rgba = _mm_cvtps_epi32(_mm_add_ps(_mm_mul_ps(frgba, _mm_set_ps1(255.0f)), _mm_set_ps1(0.5f))); + rgba = _mm_packs_epi32(rgba, _mm_setzero_si128()); + rgba = _mm_packus_epi16(rgba, _mm_setzero_si128()); + dline[x] = ((uint32_t)_mm_cvtsi128_si32(rgba)) | 0xff000000; + x++; + } +#else + while (x < x1) + { + float upix = u + uscale * (x + 0.5f - left); + uint32_t spixel = sline[(int)upix]; + float sred = ((spixel >> 16) & 0xff) * (1.0f / 255.0f); + float sgreen = ((spixel >> 8) & 0xff) * (1.0f / 255.0f); + float sblue = (spixel & 0xff) * (1.0f / 255.0f); + + uint32_t dpixel = dline[x]; + float dred = ((dpixel >> 16) & 0xff) * (1.0f / 255.0f); + float dgreen = ((dpixel >> 8) & 0xff) * (1.0f / 255.0f); + float dblue = (dpixel & 0xff) * (1.0f / 255.0f); + + // To linear + dred = dred * dred; // std::pow(dred, 2.2f); + dgreen = dgreen * dgreen; // std::pow(dgreen, 2.2f); + dblue = dblue * dblue; // std::pow(dblue, 2.2f); + + // dest.rgb = color.rgb * src.rgb + dest.rgb * (1-src.rgb) + double fr = cred * sred + dred * (1.0f - sred); + double fg = cgreen * sgreen + dgreen * (1.0f - sgreen); + double fb = cblue * sblue + dblue * (1.0f - sblue); + + // To srgb + fr = std::sqrt(fr); // std::pow(fr, 1.0f / 2.2f); + fg = std::sqrt(fg); // std::pow(fg, 1.0f / 2.2f); + fb = std::sqrt(fb); // std::pow(fb, 1.0f / 2.2f); + + uint32_t r = (int)(fr * 255.0f + 0.5f); + uint32_t g = (int)(fg * 255.0f + 0.5f); + uint32_t b = (int)(fb * 255.0f + 0.5f); + dline[x] = 0xff000000 | (r << 16) | (g << 8) | b; + x++; + } +#endif + } + +#else + + uint32_t cred = (int32_t)clamp(color.r * 255.0f, 0.0f, 255.0f); + uint32_t cgreen = (int32_t)clamp(color.g * 255.0f, 0.0f, 255.0f); + uint32_t cblue = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); +#ifdef USE_SSE2 + __m128i crgba = _mm_set_epi16(0, cred, cgreen, cblue, 0, cred, cgreen, cblue); +#endif + + float uscale = uvwidth / width; + float vscale = uvheight / height; + + for (int y = y0; y < y1; y++) + { + float vpix = v + vscale * (y + 0.5f - top); + const uint32_t* sline = src + ((int)vpix) * swidth; + uint32_t* dline = dest + y * dwidth; + + int x = x0; +#ifdef USE_SSE2 + int ssex1 = x0 + (((x1 - x0) >> 1) << 1); + while (x < ssex1) + { + float upix0 = u + uscale * (x + 0.5f - left); + float upix1 = u + uscale * (x + 1 + 0.5f - left); + uint32_t spixel0 = sline[(int)upix0]; + uint32_t spixel1 = sline[(int)upix1]; + __m128i spixel = _mm_set_epi32(0, 0, spixel1, spixel0); + spixel = _mm_unpacklo_epi8(spixel, _mm_setzero_si128()); + + __m128i dpixel = _mm_loadl_epi64((const __m128i*)(dline + x)); + dpixel = _mm_unpacklo_epi8(dpixel, _mm_setzero_si128()); + + // Rescale from [0,255] to [0,256] + spixel = _mm_add_epi16(spixel, _mm_srli_epi16(spixel, 7)); + + // dest.rgb = color.rgb * src.rgb + dest.rgb * (1-src.rgb) + __m128i result = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(_mm_mullo_epi16(crgba, spixel), _mm_mullo_epi16(dpixel, _mm_sub_epi16(_mm_set1_epi16(256), spixel))), _mm_set1_epi16(127)), 8); + _mm_storel_epi64((__m128i*)(dline + x), _mm_or_si128(_mm_packus_epi16(result, _mm_setzero_si128()), _mm_set1_epi32(0xff000000))); + x += 2; + } +#endif + + while (x < x1) + { + float upix = u + uscale * (x + 0.5f - left); + uint32_t spixel = sline[(int)upix]; + uint32_t sred = (spixel >> 16) & 0xff; + uint32_t sgreen = (spixel >> 8) & 0xff; + uint32_t sblue = spixel & 0xff; + + uint32_t dpixel = dline[x]; + uint32_t dred = (dpixel >> 16) & 0xff; + uint32_t dgreen = (dpixel >> 8) & 0xff; + uint32_t dblue = dpixel & 0xff; + + // Rescale from [0,255] to [0,256] + sred += sred >> 7; + sgreen += sgreen >> 7; + sblue += sblue >> 7; + + // dest.rgb = color.rgb * src.rgb + dest.rgb * (1-src.rgb) + uint32_t r = (cred * sred + dred * (256 - sred) + 127) >> 8; + uint32_t g = (cgreen * sgreen + dgreen * (256 - sgreen) + 127) >> 8; + uint32_t b = (cblue * sblue + dblue * (256 - sblue) + 127) >> 8; + dline[x] = 0xff000000 | (r << 16) | (g << 8) | b; + x++; + } + } +#endif +} + +void BitmapCanvas::begin(const Colorf& color) +{ + Canvas::begin(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; + pixels.clear(); + pixels.resize(width * height, bgcolor); +} + +void BitmapCanvas::end() +{ + window->PresentBitmap(width, height, pixels.data()); +} + +///////////////////////////////////////////////////////////////////////////// + +std::unique_ptr Canvas::create() +{ + return std::make_unique(); +} diff --git a/libraries/ZWidget/src/core/font.cpp b/libraries/ZWidget/src/core/font.cpp new file mode 100644 index 000000000..f646912a9 --- /dev/null +++ b/libraries/ZWidget/src/core/font.cpp @@ -0,0 +1,28 @@ + +#include "core/font.h" + +class FontImpl : public Font +{ +public: + FontImpl(const std::string& name, double height) : Name(name), Height(height) + { + } + + const std::string& GetName() const override + { + return Name; + } + + double GetHeight() const override + { + return Height; + } + + std::string Name; + double Height = 0.0; +}; + +std::shared_ptr Font::Create(const std::string& name, double height) +{ + return std::make_shared(name, height); +} diff --git a/libraries/ZWidget/src/core/image.cpp b/libraries/ZWidget/src/core/image.cpp new file mode 100644 index 000000000..3998d78e8 --- /dev/null +++ b/libraries/ZWidget/src/core/image.cpp @@ -0,0 +1,110 @@ + +#include "core/image.h" +#include "core/resourcedata.h" +#include "picopng/picopng.h" +#include "nanosvg/nanosvg.h" +#include "nanosvg/nanosvgrast.h" +#include +#include + +class ImageImpl : public Image +{ +public: + ImageImpl(int width, int height, ImageFormat format, const void* data) : Width(width), Height(height), Format(format) + { + Data = std::make_unique(width * height); + if (data) + memcpy(Data.get(), data, width * height * sizeof(uint32_t)); + } + + int GetWidth() const override + { + return Width; + } + + int GetHeight() const override + { + return Height; + } + + ImageFormat GetFormat() const override + { + return Format; + } + + void* GetData() const override + { + return Data.get(); + } + + int Width = 0; + int Height = 0; + ImageFormat Format = {}; + std::unique_ptr Data; +}; + +std::shared_ptr Image::Create(int width, int height, ImageFormat format, const void* data) +{ + return std::make_shared(width, height, format, data); +} + +std::shared_ptr Image::LoadResource(const std::string& resourcename, double dpiscale) +{ + size_t extensionpos = resourcename.find_last_of("./\\"); + if (extensionpos == std::string::npos || resourcename[extensionpos] != '.') + throw std::runtime_error("Unsupported image format"); + std::string extension = resourcename.substr(extensionpos + 1); + for (char& c : extension) + { + if (c >= 'A' && c <= 'Z') + c = c - 'A' + 'a'; + } + + if (extension == "png") + { + auto filedata = LoadWidgetData(resourcename); + + std::vector pixels; + unsigned long width = 0, height = 0; + int result = decodePNG(pixels, width, height, (const unsigned char*)filedata.data(), filedata.size(), true); + if (result != 0) + throw std::runtime_error("Could not decode PNG file"); + + return Image::Create(width, height, ImageFormat::R8G8B8A8, pixels.data()); + } + else if (extension == "svg") + { + auto filedata = LoadWidgetData(resourcename); + filedata.push_back(0); + + NSVGimage* svgimage = nsvgParse((char*)filedata.data(), "px", (float)(96.0 * dpiscale)); + if (!svgimage) + throw std::runtime_error("Could not parse SVG file"); + + try + { + int width = (int)(svgimage->width * dpiscale); + int height = (int)(svgimage->height * dpiscale); + std::shared_ptr image = Image::Create(width, height, ImageFormat::R8G8B8A8, nullptr); + + NSVGrasterizer* rast = nsvgCreateRasterizer(); + if (!rast) + throw std::runtime_error("Could not create SVG rasterizer"); + + nsvgRasterize(rast, svgimage, 0.0f, 0.0f, (float)dpiscale, (unsigned char*)image->GetData(), width, height, width * 4); + + nsvgDeleteRasterizer(rast); + nsvgDelete(svgimage); + return image; + } + catch (...) + { + nsvgDelete(svgimage); + throw; + } + } + else + { + throw std::runtime_error("Unsupported image format"); + } +} diff --git a/libraries/ZWidget/src/core/nanosvg/nanosvg.cpp b/libraries/ZWidget/src/core/nanosvg/nanosvg.cpp new file mode 100644 index 000000000..764b854b0 --- /dev/null +++ b/libraries/ZWidget/src/core/nanosvg/nanosvg.cpp @@ -0,0 +1,6 @@ + +#define NANOSVG_IMPLEMENTATION +#include "nanosvg.h" + +#define NANOSVGRAST_IMPLEMENTATION +#include "nanosvgrast.h" diff --git a/libraries/ZWidget/src/core/nanosvg/nanosvg.h b/libraries/ZWidget/src/core/nanosvg/nanosvg.h new file mode 100644 index 000000000..55f9bc5ea --- /dev/null +++ b/libraries/ZWidget/src/core/nanosvg/nanosvg.h @@ -0,0 +1,3107 @@ +/* + * Copyright (c) 2013-14 Mikko Mononen memon@inside.org + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * The SVG parser is based on Anti-Grain Geometry 2.4 SVG example + * Copyright (C) 2002-2004 Maxim Shemanarev (McSeem) (http://www.antigrain.com/) + * + * Arc calculation code based on canvg (https://code.google.com/p/canvg/) + * + * Bounding box calculation based on http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + * + */ + +#ifndef NANOSVG_H +#define NANOSVG_H + +#ifndef NANOSVG_CPLUSPLUS +#ifdef __cplusplus +extern "C" { +#endif +#endif + +// NanoSVG is a simple stupid single-header-file SVG parse. The output of the parser is a list of cubic bezier shapes. +// +// The library suits well for anything from rendering scalable icons in your editor application to prototyping a game. +// +// NanoSVG supports a wide range of SVG features, but something may be missing, feel free to create a pull request! +// +// The shapes in the SVG images are transformed by the viewBox and converted to specified units. +// That is, you should get the same looking data as your designed in your favorite app. +// +// NanoSVG can return the paths in few different units. For example if you want to render an image, you may choose +// to get the paths in pixels, or if you are feeding the data into a CNC-cutter, you may want to use millimeters. +// +// The units passed to NanoSVG should be one of: 'px', 'pt', 'pc' 'mm', 'cm', or 'in'. +// DPI (dots-per-inch) controls how the unit conversion is done. +// +// If you don't know or care about the units stuff, "px" and 96 should get you going. + + +/* Example Usage: + // Load SVG + NSVGimage* image; + image = nsvgParseFromFile("test.svg", "px", 96); + printf("size: %f x %f\n", image->width, image->height); + // Use... + for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { + for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { + for (int i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + drawCubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7]); + } + } + } + // Delete + nsvgDelete(image); +*/ + +enum NSVGpaintType { + NSVG_PAINT_UNDEF = -1, + NSVG_PAINT_NONE = 0, + NSVG_PAINT_COLOR = 1, + NSVG_PAINT_LINEAR_GRADIENT = 2, + NSVG_PAINT_RADIAL_GRADIENT = 3 +}; + +enum NSVGspreadType { + NSVG_SPREAD_PAD = 0, + NSVG_SPREAD_REFLECT = 1, + NSVG_SPREAD_REPEAT = 2 +}; + +enum NSVGlineJoin { + NSVG_JOIN_MITER = 0, + NSVG_JOIN_ROUND = 1, + NSVG_JOIN_BEVEL = 2 +}; + +enum NSVGlineCap { + NSVG_CAP_BUTT = 0, + NSVG_CAP_ROUND = 1, + NSVG_CAP_SQUARE = 2 +}; + +enum NSVGfillRule { + NSVG_FILLRULE_NONZERO = 0, + NSVG_FILLRULE_EVENODD = 1 +}; + +enum NSVGflags { + NSVG_FLAGS_VISIBLE = 0x01 +}; + +typedef struct NSVGgradientStop { + unsigned int color; + float offset; +} NSVGgradientStop; + +typedef struct NSVGgradient { + float xform[6]; + char spread; + float fx, fy; + int nstops; + NSVGgradientStop stops[1]; +} NSVGgradient; + +typedef struct NSVGpaint { + signed char type; + union { + unsigned int color; + NSVGgradient* gradient; + }; +} NSVGpaint; + +typedef struct NSVGpath +{ + float* pts; // Cubic bezier points: x0,y0, [cpx1,cpx1,cpx2,cpy2,x1,y1], ... + int npts; // Total number of bezier points. + char closed; // Flag indicating if shapes should be treated as closed. + float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + struct NSVGpath* next; // Pointer to next path, or NULL if last element. +} NSVGpath; + +typedef struct NSVGshape +{ + char id[64]; // Optional 'id' attr of the shape or its group + NSVGpaint fill; // Fill paint + NSVGpaint stroke; // Stroke paint + float opacity; // Opacity of the shape. + float strokeWidth; // Stroke width (scaled). + float strokeDashOffset; // Stroke dash offset (scaled). + float strokeDashArray[8]; // Stroke dash array (scaled). + char strokeDashCount; // Number of dash values in dash array. + char strokeLineJoin; // Stroke join type. + char strokeLineCap; // Stroke cap type. + float miterLimit; // Miter limit + char fillRule; // Fill rule, see NSVGfillRule. + unsigned char flags; // Logical or of NSVG_FLAGS_* flags + float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + char fillGradient[64]; // Optional 'id' of fill gradient + char strokeGradient[64]; // Optional 'id' of stroke gradient + float xform[6]; // Root transformation for fill/stroke gradient + NSVGpath* paths; // Linked list of paths in the image. + struct NSVGshape* next; // Pointer to next shape, or NULL if last element. +} NSVGshape; + +typedef struct NSVGimage +{ + float width; // Width of the image. + float height; // Height of the image. + NSVGshape* shapes; // Linked list of shapes in the image. +} NSVGimage; + +// Parses SVG file from a file, returns SVG image as paths. +NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi); + +// Parses SVG file from a null terminated string, returns SVG image as paths. +// Important note: changes the string. +NSVGimage* nsvgParse(char* input, const char* units, float dpi); + +// Duplicates a path. +NSVGpath* nsvgDuplicatePath(NSVGpath* p); + +// Deletes an image. +void nsvgDelete(NSVGimage* image); + +#ifndef NANOSVG_CPLUSPLUS +#ifdef __cplusplus +} +#endif +#endif + +#ifdef NANOSVG_IMPLEMENTATION + +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable:4244) +#endif + +#define NSVG_PI (3.14159265358979323846264338327f) +#define NSVG_KAPPA90 (0.5522847493f) // Length proportional to radius of a cubic bezier handle for 90deg arcs. + +#define NSVG_ALIGN_MIN 0 +#define NSVG_ALIGN_MID 1 +#define NSVG_ALIGN_MAX 2 +#define NSVG_ALIGN_NONE 0 +#define NSVG_ALIGN_MEET 1 +#define NSVG_ALIGN_SLICE 2 + +#define NSVG_NOTUSED(v) do { (void)(1 ? (void)0 : ( (void)(v) ) ); } while(0) +#define NSVG_RGB(r, g, b) (((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16)) + +#ifdef _MSC_VER + #pragma warning (disable: 4996) // Switch off security warnings + #pragma warning (disable: 4100) // Switch off unreferenced formal parameter warnings + #ifdef __cplusplus + #define NSVG_INLINE inline + #else + #define NSVG_INLINE + #endif +#else + #define NSVG_INLINE inline +#endif + + +static int nsvg__isspace(char c) +{ + return strchr(" \t\n\v\f\r", c) != 0; +} + +static int nsvg__isdigit(char c) +{ + return c >= '0' && c <= '9'; +} + +static NSVG_INLINE float nsvg__minf(float a, float b) { return a < b ? a : b; } +static NSVG_INLINE float nsvg__maxf(float a, float b) { return a > b ? a : b; } + + +// Simple XML parser + +#define NSVG_XML_TAG 1 +#define NSVG_XML_CONTENT 2 +#define NSVG_XML_MAX_ATTRIBS 256 + +static void nsvg__parseContent(char* s, + void (*contentCb)(void* ud, const char* s), + void* ud) +{ + // Trim start white spaces + while (*s && nsvg__isspace(*s)) s++; + if (!*s) return; + + if (contentCb) + (*contentCb)(ud, s); +} + +static void nsvg__parseElement(char* s, + void (*startelCb)(void* ud, const char* el, const char** attr), + void (*endelCb)(void* ud, const char* el), + void* ud) +{ + const char* attr[NSVG_XML_MAX_ATTRIBS]; + int nattr = 0; + char* name; + int start = 0; + int end = 0; + char quote; + + // Skip white space after the '<' + while (*s && nsvg__isspace(*s)) s++; + + // Check if the tag is end tag + if (*s == '/') { + s++; + end = 1; + } else { + start = 1; + } + + // Skip comments, data and preprocessor stuff. + if (!*s || *s == '?' || *s == '!') + return; + + // Get tag name + name = s; + while (*s && !nsvg__isspace(*s)) s++; + if (*s) { *s++ = '\0'; } + + // Get attribs + while (!end && *s && nattr < NSVG_XML_MAX_ATTRIBS-3) { + char* name = NULL; + char* value = NULL; + + // Skip white space before the attrib name + while (*s && nsvg__isspace(*s)) s++; + if (!*s) break; + if (*s == '/') { + end = 1; + break; + } + name = s; + // Find end of the attrib name. + while (*s && !nsvg__isspace(*s) && *s != '=') s++; + if (*s) { *s++ = '\0'; } + // Skip until the beginning of the value. + while (*s && *s != '\"' && *s != '\'') s++; + if (!*s) break; + quote = *s; + s++; + // Store value and find the end of it. + value = s; + while (*s && *s != quote) s++; + if (*s) { *s++ = '\0'; } + + // Store only well formed attributes + if (name && value) { + attr[nattr++] = name; + attr[nattr++] = value; + } + } + + // List terminator + attr[nattr++] = 0; + attr[nattr++] = 0; + + // Call callbacks. + if (start && startelCb) + (*startelCb)(ud, name, attr); + if (end && endelCb) + (*endelCb)(ud, name); +} + +int nsvg__parseXML(char* input, + void (*startelCb)(void* ud, const char* el, const char** attr), + void (*endelCb)(void* ud, const char* el), + void (*contentCb)(void* ud, const char* s), + void* ud) +{ + char* s = input; + char* mark = s; + int state = NSVG_XML_CONTENT; + while (*s) { + if (*s == '<' && state == NSVG_XML_CONTENT) { + // Start of a tag + *s++ = '\0'; + nsvg__parseContent(mark, contentCb, ud); + mark = s; + state = NSVG_XML_TAG; + } else if (*s == '>' && state == NSVG_XML_TAG) { + // Start of a content or new tag. + *s++ = '\0'; + nsvg__parseElement(mark, startelCb, endelCb, ud); + mark = s; + state = NSVG_XML_CONTENT; + } else { + s++; + } + } + + return 1; +} + + +/* Simple SVG parser. */ + +#define NSVG_MAX_ATTR 128 + +enum NSVGgradientUnits { + NSVG_USER_SPACE = 0, + NSVG_OBJECT_SPACE = 1 +}; + +#define NSVG_MAX_DASHES 8 + +enum NSVGunits { + NSVG_UNITS_USER, + NSVG_UNITS_PX, + NSVG_UNITS_PT, + NSVG_UNITS_PC, + NSVG_UNITS_MM, + NSVG_UNITS_CM, + NSVG_UNITS_IN, + NSVG_UNITS_PERCENT, + NSVG_UNITS_EM, + NSVG_UNITS_EX +}; + +typedef struct NSVGcoordinate { + float value; + int units; +} NSVGcoordinate; + +typedef struct NSVGlinearData { + NSVGcoordinate x1, y1, x2, y2; +} NSVGlinearData; + +typedef struct NSVGradialData { + NSVGcoordinate cx, cy, r, fx, fy; +} NSVGradialData; + +typedef struct NSVGgradientData +{ + char id[64]; + char ref[64]; + signed char type; + union { + NSVGlinearData linear; + NSVGradialData radial; + }; + char spread; + char units; + float xform[6]; + int nstops; + NSVGgradientStop* stops; + struct NSVGgradientData* next; +} NSVGgradientData; + +typedef struct NSVGattrib +{ + char id[64]; + float xform[6]; + unsigned int fillColor; + unsigned int strokeColor; + float opacity; + float fillOpacity; + float strokeOpacity; + char fillGradient[64]; + char strokeGradient[64]; + float strokeWidth; + float strokeDashOffset; + float strokeDashArray[NSVG_MAX_DASHES]; + int strokeDashCount; + char strokeLineJoin; + char strokeLineCap; + float miterLimit; + char fillRule; + float fontSize; + unsigned int stopColor; + float stopOpacity; + float stopOffset; + char hasFill; + char hasStroke; + char visible; +} NSVGattrib; + +typedef struct NSVGparser +{ + NSVGattrib attr[NSVG_MAX_ATTR]; + int attrHead; + float* pts; + int npts; + int cpts; + NSVGpath* plist; + NSVGimage* image; + NSVGgradientData* gradients; + NSVGshape* shapesTail; + float viewMinx, viewMiny, viewWidth, viewHeight; + int alignX, alignY, alignType; + float dpi; + char pathFlag; + char defsFlag; +} NSVGparser; + +static void nsvg__xformIdentity(float* t) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = 0.0f; t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetTranslation(float* t, float tx, float ty) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = 0.0f; t[3] = 1.0f; + t[4] = tx; t[5] = ty; +} + +static void nsvg__xformSetScale(float* t, float sx, float sy) +{ + t[0] = sx; t[1] = 0.0f; + t[2] = 0.0f; t[3] = sy; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetSkewX(float* t, float a) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = tanf(a); t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetSkewY(float* t, float a) +{ + t[0] = 1.0f; t[1] = tanf(a); + t[2] = 0.0f; t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetRotation(float* t, float a) +{ + float cs = cosf(a), sn = sinf(a); + t[0] = cs; t[1] = sn; + t[2] = -sn; t[3] = cs; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformMultiply(float* t, float* s) +{ + float t0 = t[0] * s[0] + t[1] * s[2]; + float t2 = t[2] * s[0] + t[3] * s[2]; + float t4 = t[4] * s[0] + t[5] * s[2] + s[4]; + t[1] = t[0] * s[1] + t[1] * s[3]; + t[3] = t[2] * s[1] + t[3] * s[3]; + t[5] = t[4] * s[1] + t[5] * s[3] + s[5]; + t[0] = t0; + t[2] = t2; + t[4] = t4; +} + +static void nsvg__xformInverse(float* inv, float* t) +{ + double invdet, det = (double)t[0] * t[3] - (double)t[2] * t[1]; + if (det > -1e-6 && det < 1e-6) { + nsvg__xformIdentity(t); + return; + } + invdet = 1.0 / det; + inv[0] = (float)(t[3] * invdet); + inv[2] = (float)(-t[2] * invdet); + inv[4] = (float)(((double)t[2] * t[5] - (double)t[3] * t[4]) * invdet); + inv[1] = (float)(-t[1] * invdet); + inv[3] = (float)(t[0] * invdet); + inv[5] = (float)(((double)t[1] * t[4] - (double)t[0] * t[5]) * invdet); +} + +static void nsvg__xformPremultiply(float* t, float* s) +{ + float s2[6]; + memcpy(s2, s, sizeof(float)*6); + nsvg__xformMultiply(s2, t); + memcpy(t, s2, sizeof(float)*6); +} + +static void nsvg__xformPoint(float* dx, float* dy, float x, float y, float* t) +{ + *dx = x*t[0] + y*t[2] + t[4]; + *dy = x*t[1] + y*t[3] + t[5]; +} + +static void nsvg__xformVec(float* dx, float* dy, float x, float y, float* t) +{ + *dx = x*t[0] + y*t[2]; + *dy = x*t[1] + y*t[3]; +} + +#define NSVG_EPSILON (1e-12) + +static int nsvg__ptInBounds(float* pt, float* bounds) +{ + return pt[0] >= bounds[0] && pt[0] <= bounds[2] && pt[1] >= bounds[1] && pt[1] <= bounds[3]; +} + + +static double nsvg__evalBezier(double t, double p0, double p1, double p2, double p3) +{ + double it = 1.0-t; + return it*it*it*p0 + 3.0*it*it*t*p1 + 3.0*it*t*t*p2 + t*t*t*p3; +} + +static void nsvg__curveBounds(float* bounds, float* curve) +{ + int i, j, count; + double roots[2], a, b, c, b2ac, t, v; + float* v0 = &curve[0]; + float* v1 = &curve[2]; + float* v2 = &curve[4]; + float* v3 = &curve[6]; + + // Start the bounding box by end points + bounds[0] = nsvg__minf(v0[0], v3[0]); + bounds[1] = nsvg__minf(v0[1], v3[1]); + bounds[2] = nsvg__maxf(v0[0], v3[0]); + bounds[3] = nsvg__maxf(v0[1], v3[1]); + + // Bezier curve fits inside the convex hull of it's control points. + // If control points are inside the bounds, we're done. + if (nsvg__ptInBounds(v1, bounds) && nsvg__ptInBounds(v2, bounds)) + return; + + // Add bezier curve inflection points in X and Y. + for (i = 0; i < 2; i++) { + a = -3.0 * v0[i] + 9.0 * v1[i] - 9.0 * v2[i] + 3.0 * v3[i]; + b = 6.0 * v0[i] - 12.0 * v1[i] + 6.0 * v2[i]; + c = 3.0 * v1[i] - 3.0 * v0[i]; + count = 0; + if (fabs(a) < NSVG_EPSILON) { + if (fabs(b) > NSVG_EPSILON) { + t = -c / b; + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + } + } else { + b2ac = b*b - 4.0*c*a; + if (b2ac > NSVG_EPSILON) { + t = (-b + sqrt(b2ac)) / (2.0 * a); + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + t = (-b - sqrt(b2ac)) / (2.0 * a); + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + } + } + for (j = 0; j < count; j++) { + v = nsvg__evalBezier(roots[j], v0[i], v1[i], v2[i], v3[i]); + bounds[0+i] = nsvg__minf(bounds[0+i], (float)v); + bounds[2+i] = nsvg__maxf(bounds[2+i], (float)v); + } + } +} + +static NSVGparser* nsvg__createParser(void) +{ + NSVGparser* p; + p = (NSVGparser*)malloc(sizeof(NSVGparser)); + if (p == NULL) goto error; + memset(p, 0, sizeof(NSVGparser)); + + p->image = (NSVGimage*)malloc(sizeof(NSVGimage)); + if (p->image == NULL) goto error; + memset(p->image, 0, sizeof(NSVGimage)); + + // Init style + nsvg__xformIdentity(p->attr[0].xform); + memset(p->attr[0].id, 0, sizeof p->attr[0].id); + p->attr[0].fillColor = NSVG_RGB(0,0,0); + p->attr[0].strokeColor = NSVG_RGB(0,0,0); + p->attr[0].opacity = 1; + p->attr[0].fillOpacity = 1; + p->attr[0].strokeOpacity = 1; + p->attr[0].stopOpacity = 1; + p->attr[0].strokeWidth = 1; + p->attr[0].strokeLineJoin = NSVG_JOIN_MITER; + p->attr[0].strokeLineCap = NSVG_CAP_BUTT; + p->attr[0].miterLimit = 4; + p->attr[0].fillRule = NSVG_FILLRULE_NONZERO; + p->attr[0].hasFill = 1; + p->attr[0].visible = 1; + + return p; + +error: + if (p) { + if (p->image) free(p->image); + free(p); + } + return NULL; +} + +static void nsvg__deletePaths(NSVGpath* path) +{ + while (path) { + NSVGpath *next = path->next; + if (path->pts != NULL) + free(path->pts); + free(path); + path = next; + } +} + +static void nsvg__deletePaint(NSVGpaint* paint) +{ + if (paint->type == NSVG_PAINT_LINEAR_GRADIENT || paint->type == NSVG_PAINT_RADIAL_GRADIENT) + free(paint->gradient); +} + +static void nsvg__deleteGradientData(NSVGgradientData* grad) +{ + NSVGgradientData* next; + while (grad != NULL) { + next = grad->next; + free(grad->stops); + free(grad); + grad = next; + } +} + +static void nsvg__deleteParser(NSVGparser* p) +{ + if (p != NULL) { + nsvg__deletePaths(p->plist); + nsvg__deleteGradientData(p->gradients); + nsvgDelete(p->image); + free(p->pts); + free(p); + } +} + +static void nsvg__resetPath(NSVGparser* p) +{ + p->npts = 0; +} + +static void nsvg__addPoint(NSVGparser* p, float x, float y) +{ + if (p->npts+1 > p->cpts) { + p->cpts = p->cpts ? p->cpts*2 : 8; + p->pts = (float*)realloc(p->pts, p->cpts*2*sizeof(float)); + if (!p->pts) return; + } + p->pts[p->npts*2+0] = x; + p->pts[p->npts*2+1] = y; + p->npts++; +} + +static void nsvg__moveTo(NSVGparser* p, float x, float y) +{ + if (p->npts > 0) { + p->pts[(p->npts-1)*2+0] = x; + p->pts[(p->npts-1)*2+1] = y; + } else { + nsvg__addPoint(p, x, y); + } +} + +static void nsvg__lineTo(NSVGparser* p, float x, float y) +{ + float px,py, dx,dy; + if (p->npts > 0) { + px = p->pts[(p->npts-1)*2+0]; + py = p->pts[(p->npts-1)*2+1]; + dx = x - px; + dy = y - py; + nsvg__addPoint(p, px + dx/3.0f, py + dy/3.0f); + nsvg__addPoint(p, x - dx/3.0f, y - dy/3.0f); + nsvg__addPoint(p, x, y); + } +} + +static void nsvg__cubicBezTo(NSVGparser* p, float cpx1, float cpy1, float cpx2, float cpy2, float x, float y) +{ + if (p->npts > 0) { + nsvg__addPoint(p, cpx1, cpy1); + nsvg__addPoint(p, cpx2, cpy2); + nsvg__addPoint(p, x, y); + } +} + +static NSVGattrib* nsvg__getAttr(NSVGparser* p) +{ + return &p->attr[p->attrHead]; +} + +static void nsvg__pushAttr(NSVGparser* p) +{ + if (p->attrHead < NSVG_MAX_ATTR-1) { + p->attrHead++; + memcpy(&p->attr[p->attrHead], &p->attr[p->attrHead-1], sizeof(NSVGattrib)); + } +} + +static void nsvg__popAttr(NSVGparser* p) +{ + if (p->attrHead > 0) + p->attrHead--; +} + +static float nsvg__actualOrigX(NSVGparser* p) +{ + return p->viewMinx; +} + +static float nsvg__actualOrigY(NSVGparser* p) +{ + return p->viewMiny; +} + +static float nsvg__actualWidth(NSVGparser* p) +{ + return p->viewWidth; +} + +static float nsvg__actualHeight(NSVGparser* p) +{ + return p->viewHeight; +} + +static float nsvg__actualLength(NSVGparser* p) +{ + float w = nsvg__actualWidth(p), h = nsvg__actualHeight(p); + return sqrtf(w*w + h*h) / sqrtf(2.0f); +} + +static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c, float orig, float length) +{ + NSVGattrib* attr = nsvg__getAttr(p); + switch (c.units) { + case NSVG_UNITS_USER: return c.value; + case NSVG_UNITS_PX: return c.value; + case NSVG_UNITS_PT: return c.value / 72.0f * p->dpi; + case NSVG_UNITS_PC: return c.value / 6.0f * p->dpi; + case NSVG_UNITS_MM: return c.value / 25.4f * p->dpi; + case NSVG_UNITS_CM: return c.value / 2.54f * p->dpi; + case NSVG_UNITS_IN: return c.value * p->dpi; + case NSVG_UNITS_EM: return c.value * attr->fontSize; + case NSVG_UNITS_EX: return c.value * attr->fontSize * 0.52f; // x-height of Helvetica. + case NSVG_UNITS_PERCENT: return orig + c.value / 100.0f * length; + default: return c.value; + } + return c.value; +} + +static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) +{ + NSVGgradientData* grad = p->gradients; + if (id == NULL || *id == '\0') + return NULL; + while (grad != NULL) { + if (strcmp(grad->id, id) == 0) + return grad; + grad = grad->next; + } + return NULL; +} + +static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, float *xform, signed char* paintType) +{ + NSVGgradientData* data = NULL; + NSVGgradientData* ref = NULL; + NSVGgradientStop* stops = NULL; + NSVGgradient* grad; + float ox, oy, sw, sh, sl; + int nstops = 0; + int refIter; + + data = nsvg__findGradientData(p, id); + if (data == NULL) return NULL; + + // TODO: use ref to fill in all unset values too. + ref = data; + refIter = 0; + while (ref != NULL) { + NSVGgradientData* nextRef = NULL; + if (stops == NULL && ref->stops != NULL) { + stops = ref->stops; + nstops = ref->nstops; + break; + } + nextRef = nsvg__findGradientData(p, ref->ref); + if (nextRef == ref) break; // prevent infite loops on malformed data + ref = nextRef; + refIter++; + if (refIter > 32) break; // prevent infite loops on malformed data + } + if (stops == NULL) return NULL; + + grad = (NSVGgradient*)malloc(sizeof(NSVGgradient) + sizeof(NSVGgradientStop)*(nstops-1)); + if (grad == NULL) return NULL; + + // The shape width and height. + if (data->units == NSVG_OBJECT_SPACE) { + ox = localBounds[0]; + oy = localBounds[1]; + sw = localBounds[2] - localBounds[0]; + sh = localBounds[3] - localBounds[1]; + } else { + ox = nsvg__actualOrigX(p); + oy = nsvg__actualOrigY(p); + sw = nsvg__actualWidth(p); + sh = nsvg__actualHeight(p); + } + sl = sqrtf(sw*sw + sh*sh) / sqrtf(2.0f); + + if (data->type == NSVG_PAINT_LINEAR_GRADIENT) { + float x1, y1, x2, y2, dx, dy; + x1 = nsvg__convertToPixels(p, data->linear.x1, ox, sw); + y1 = nsvg__convertToPixels(p, data->linear.y1, oy, sh); + x2 = nsvg__convertToPixels(p, data->linear.x2, ox, sw); + y2 = nsvg__convertToPixels(p, data->linear.y2, oy, sh); + // Calculate transform aligned to the line + dx = x2 - x1; + dy = y2 - y1; + grad->xform[0] = dy; grad->xform[1] = -dx; + grad->xform[2] = dx; grad->xform[3] = dy; + grad->xform[4] = x1; grad->xform[5] = y1; + } else { + float cx, cy, fx, fy, r; + cx = nsvg__convertToPixels(p, data->radial.cx, ox, sw); + cy = nsvg__convertToPixels(p, data->radial.cy, oy, sh); + fx = nsvg__convertToPixels(p, data->radial.fx, ox, sw); + fy = nsvg__convertToPixels(p, data->radial.fy, oy, sh); + r = nsvg__convertToPixels(p, data->radial.r, 0, sl); + // Calculate transform aligned to the circle + grad->xform[0] = r; grad->xform[1] = 0; + grad->xform[2] = 0; grad->xform[3] = r; + grad->xform[4] = cx; grad->xform[5] = cy; + grad->fx = fx / r; + grad->fy = fy / r; + } + + nsvg__xformMultiply(grad->xform, data->xform); + nsvg__xformMultiply(grad->xform, xform); + + grad->spread = data->spread; + memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop)); + grad->nstops = nstops; + + *paintType = data->type; + + return grad; +} + +static float nsvg__getAverageScale(float* t) +{ + float sx = sqrtf(t[0]*t[0] + t[2]*t[2]); + float sy = sqrtf(t[1]*t[1] + t[3]*t[3]); + return (sx + sy) * 0.5f; +} + +static void nsvg__getLocalBounds(float* bounds, NSVGshape *shape, float* xform) +{ + NSVGpath* path; + float curve[4*2], curveBounds[4]; + int i, first = 1; + for (path = shape->paths; path != NULL; path = path->next) { + nsvg__xformPoint(&curve[0], &curve[1], path->pts[0], path->pts[1], xform); + for (i = 0; i < path->npts-1; i += 3) { + nsvg__xformPoint(&curve[2], &curve[3], path->pts[(i+1)*2], path->pts[(i+1)*2+1], xform); + nsvg__xformPoint(&curve[4], &curve[5], path->pts[(i+2)*2], path->pts[(i+2)*2+1], xform); + nsvg__xformPoint(&curve[6], &curve[7], path->pts[(i+3)*2], path->pts[(i+3)*2+1], xform); + nsvg__curveBounds(curveBounds, curve); + if (first) { + bounds[0] = curveBounds[0]; + bounds[1] = curveBounds[1]; + bounds[2] = curveBounds[2]; + bounds[3] = curveBounds[3]; + first = 0; + } else { + bounds[0] = nsvg__minf(bounds[0], curveBounds[0]); + bounds[1] = nsvg__minf(bounds[1], curveBounds[1]); + bounds[2] = nsvg__maxf(bounds[2], curveBounds[2]); + bounds[3] = nsvg__maxf(bounds[3], curveBounds[3]); + } + curve[0] = curve[6]; + curve[1] = curve[7]; + } + } +} + +static void nsvg__addShape(NSVGparser* p) +{ + NSVGattrib* attr = nsvg__getAttr(p); + float scale = 1.0f; + NSVGshape* shape; + NSVGpath* path; + int i; + + if (p->plist == NULL) + return; + + shape = (NSVGshape*)malloc(sizeof(NSVGshape)); + if (shape == NULL) goto error; + memset(shape, 0, sizeof(NSVGshape)); + + memcpy(shape->id, attr->id, sizeof shape->id); + memcpy(shape->fillGradient, attr->fillGradient, sizeof shape->fillGradient); + memcpy(shape->strokeGradient, attr->strokeGradient, sizeof shape->strokeGradient); + memcpy(shape->xform, attr->xform, sizeof shape->xform); + scale = nsvg__getAverageScale(attr->xform); + shape->strokeWidth = attr->strokeWidth * scale; + shape->strokeDashOffset = attr->strokeDashOffset * scale; + shape->strokeDashCount = (char)attr->strokeDashCount; + for (i = 0; i < attr->strokeDashCount; i++) + shape->strokeDashArray[i] = attr->strokeDashArray[i] * scale; + shape->strokeLineJoin = attr->strokeLineJoin; + shape->strokeLineCap = attr->strokeLineCap; + shape->miterLimit = attr->miterLimit; + shape->fillRule = attr->fillRule; + shape->opacity = attr->opacity; + + shape->paths = p->plist; + p->plist = NULL; + + // Calculate shape bounds + shape->bounds[0] = shape->paths->bounds[0]; + shape->bounds[1] = shape->paths->bounds[1]; + shape->bounds[2] = shape->paths->bounds[2]; + shape->bounds[3] = shape->paths->bounds[3]; + for (path = shape->paths->next; path != NULL; path = path->next) { + shape->bounds[0] = nsvg__minf(shape->bounds[0], path->bounds[0]); + shape->bounds[1] = nsvg__minf(shape->bounds[1], path->bounds[1]); + shape->bounds[2] = nsvg__maxf(shape->bounds[2], path->bounds[2]); + shape->bounds[3] = nsvg__maxf(shape->bounds[3], path->bounds[3]); + } + + // Set fill + if (attr->hasFill == 0) { + shape->fill.type = NSVG_PAINT_NONE; + } else if (attr->hasFill == 1) { + shape->fill.type = NSVG_PAINT_COLOR; + shape->fill.color = attr->fillColor; + shape->fill.color |= (unsigned int)(attr->fillOpacity*255) << 24; + } else if (attr->hasFill == 2) { + shape->fill.type = NSVG_PAINT_UNDEF; + } + + // Set stroke + if (attr->hasStroke == 0) { + shape->stroke.type = NSVG_PAINT_NONE; + } else if (attr->hasStroke == 1) { + shape->stroke.type = NSVG_PAINT_COLOR; + shape->stroke.color = attr->strokeColor; + shape->stroke.color |= (unsigned int)(attr->strokeOpacity*255) << 24; + } else if (attr->hasStroke == 2) { + shape->stroke.type = NSVG_PAINT_UNDEF; + } + + // Set flags + shape->flags = (attr->visible ? NSVG_FLAGS_VISIBLE : 0x00); + + // Add to tail + if (p->image->shapes == NULL) + p->image->shapes = shape; + else + p->shapesTail->next = shape; + p->shapesTail = shape; + + return; + +error: + if (shape) free(shape); +} + +static void nsvg__addPath(NSVGparser* p, char closed) +{ + NSVGattrib* attr = nsvg__getAttr(p); + NSVGpath* path = NULL; + float bounds[4]; + float* curve; + int i; + + if (p->npts < 4) + return; + + if (closed) + nsvg__lineTo(p, p->pts[0], p->pts[1]); + + // Expect 1 + N*3 points (N = number of cubic bezier segments). + if ((p->npts % 3) != 1) + return; + + path = (NSVGpath*)malloc(sizeof(NSVGpath)); + if (path == NULL) goto error; + memset(path, 0, sizeof(NSVGpath)); + + path->pts = (float*)malloc(p->npts*2*sizeof(float)); + if (path->pts == NULL) goto error; + path->closed = closed; + path->npts = p->npts; + + // Transform path. + for (i = 0; i < p->npts; ++i) + nsvg__xformPoint(&path->pts[i*2], &path->pts[i*2+1], p->pts[i*2], p->pts[i*2+1], attr->xform); + + // Find bounds + for (i = 0; i < path->npts-1; i += 3) { + curve = &path->pts[i*2]; + nsvg__curveBounds(bounds, curve); + if (i == 0) { + path->bounds[0] = bounds[0]; + path->bounds[1] = bounds[1]; + path->bounds[2] = bounds[2]; + path->bounds[3] = bounds[3]; + } else { + path->bounds[0] = nsvg__minf(path->bounds[0], bounds[0]); + path->bounds[1] = nsvg__minf(path->bounds[1], bounds[1]); + path->bounds[2] = nsvg__maxf(path->bounds[2], bounds[2]); + path->bounds[3] = nsvg__maxf(path->bounds[3], bounds[3]); + } + } + + path->next = p->plist; + p->plist = path; + + return; + +error: + if (path != NULL) { + if (path->pts != NULL) free(path->pts); + free(path); + } +} + +// We roll our own string to float because the std library one uses locale and messes things up. +static double nsvg__atof(const char* s) +{ + char* cur = (char*)s; + char* end = NULL; + double res = 0.0, sign = 1.0; + long long intPart = 0, fracPart = 0; + char hasIntPart = 0, hasFracPart = 0; + + // Parse optional sign + if (*cur == '+') { + cur++; + } else if (*cur == '-') { + sign = -1; + cur++; + } + + // Parse integer part + if (nsvg__isdigit(*cur)) { + // Parse digit sequence + intPart = strtoll(cur, &end, 10); + if (cur != end) { + res = (double)intPart; + hasIntPart = 1; + cur = end; + } + } + + // Parse fractional part. + if (*cur == '.') { + cur++; // Skip '.' + if (nsvg__isdigit(*cur)) { + // Parse digit sequence + fracPart = strtoll(cur, &end, 10); + if (cur != end) { + res += (double)fracPart / pow(10.0, (double)(end - cur)); + hasFracPart = 1; + cur = end; + } + } + } + + // A valid number should have integer or fractional part. + if (!hasIntPart && !hasFracPart) + return 0.0; + + // Parse optional exponent + if (*cur == 'e' || *cur == 'E') { + long expPart = 0; + cur++; // skip 'E' + expPart = strtol(cur, &end, 10); // Parse digit sequence with sign + if (cur != end) { + res *= pow(10.0, (double)expPart); + } + } + + return res * sign; +} + + +static const char* nsvg__parseNumber(const char* s, char* it, const int size) +{ + const int last = size-1; + int i = 0; + + // sign + if (*s == '-' || *s == '+') { + if (i < last) it[i++] = *s; + s++; + } + // integer part + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + if (*s == '.') { + // decimal point + if (i < last) it[i++] = *s; + s++; + // fraction part + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + } + // exponent + if ((*s == 'e' || *s == 'E') && (s[1] != 'm' && s[1] != 'x')) { + if (i < last) it[i++] = *s; + s++; + if (*s == '-' || *s == '+') { + if (i < last) it[i++] = *s; + s++; + } + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + } + it[i] = '\0'; + + return s; +} + +static const char* nsvg__getNextPathItemWhenArcFlag(const char* s, char* it) +{ + it[0] = '\0'; + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '0' || *s == '1') { + it[0] = *s++; + it[1] = '\0'; + return s; + } + return s; +} + +static const char* nsvg__getNextPathItem(const char* s, char* it) +{ + it[0] = '\0'; + // Skip white spaces and commas + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '-' || *s == '+' || *s == '.' || nsvg__isdigit(*s)) { + s = nsvg__parseNumber(s, it, 64); + } else { + // Parse command + it[0] = *s++; + it[1] = '\0'; + return s; + } + + return s; +} + +static unsigned int nsvg__parseColorHex(const char* str) +{ + unsigned int r=0, g=0, b=0; + if (sscanf(str, "#%2x%2x%2x", &r, &g, &b) == 3 ) // 2 digit hex + return NSVG_RGB(r, g, b); + if (sscanf(str, "#%1x%1x%1x", &r, &g, &b) == 3 ) // 1 digit hex, e.g. #abc -> 0xccbbaa + return NSVG_RGB(r*17, g*17, b*17); // same effect as (r<<4|r), (g<<4|g), .. + return NSVG_RGB(128, 128, 128); +} + +// Parse rgb color. The pointer 'str' must point at "rgb(" (4+ characters). +// This function returns gray (rgb(128, 128, 128) == '#808080') on parse errors +// for backwards compatibility. Note: other image viewers return black instead. + +static unsigned int nsvg__parseColorRGB(const char* str) +{ + int i; + unsigned int rgbi[3]; + float rgbf[3]; + // try decimal integers first + if (sscanf(str, "rgb(%u, %u, %u)", &rgbi[0], &rgbi[1], &rgbi[2]) != 3) { + // integers failed, try percent values (float, locale independent) + const char delimiter[3] = {',', ',', ')'}; + str += 4; // skip "rgb(" + for (i = 0; i < 3; i++) { + while (*str && (nsvg__isspace(*str))) str++; // skip leading spaces + if (*str == '+') str++; // skip '+' (don't allow '-') + if (!*str) break; + rgbf[i] = nsvg__atof(str); + + // Note 1: it would be great if nsvg__atof() returned how many + // bytes it consumed but it doesn't. We need to skip the number, + // the '%' character, spaces, and the delimiter ',' or ')'. + + // Note 2: The following code does not allow values like "33.%", + // i.e. a decimal point w/o fractional part, but this is consistent + // with other image viewers, e.g. firefox, chrome, eog, gimp. + + while (*str && nsvg__isdigit(*str)) str++; // skip integer part + if (*str == '.') { + str++; + if (!nsvg__isdigit(*str)) break; // error: no digit after '.' + while (*str && nsvg__isdigit(*str)) str++; // skip fractional part + } + if (*str == '%') str++; else break; + while (nsvg__isspace(*str)) str++; + if (*str == delimiter[i]) str++; + else break; + } + if (i == 3) { + rgbi[0] = roundf(rgbf[0] * 2.55f); + rgbi[1] = roundf(rgbf[1] * 2.55f); + rgbi[2] = roundf(rgbf[2] * 2.55f); + } else { + rgbi[0] = rgbi[1] = rgbi[2] = 128; + } + } + // clip values as the CSS spec requires + for (i = 0; i < 3; i++) { + if (rgbi[i] > 255) rgbi[i] = 255; + } + return NSVG_RGB(rgbi[0], rgbi[1], rgbi[2]); +} + +typedef struct NSVGNamedColor { + const char* name; + unsigned int color; +} NSVGNamedColor; + +NSVGNamedColor nsvg__colors[] = { + + { "red", NSVG_RGB(255, 0, 0) }, + { "green", NSVG_RGB( 0, 128, 0) }, + { "blue", NSVG_RGB( 0, 0, 255) }, + { "yellow", NSVG_RGB(255, 255, 0) }, + { "cyan", NSVG_RGB( 0, 255, 255) }, + { "magenta", NSVG_RGB(255, 0, 255) }, + { "black", NSVG_RGB( 0, 0, 0) }, + { "grey", NSVG_RGB(128, 128, 128) }, + { "gray", NSVG_RGB(128, 128, 128) }, + { "white", NSVG_RGB(255, 255, 255) }, + +#ifdef NANOSVG_ALL_COLOR_KEYWORDS + { "aliceblue", NSVG_RGB(240, 248, 255) }, + { "antiquewhite", NSVG_RGB(250, 235, 215) }, + { "aqua", NSVG_RGB( 0, 255, 255) }, + { "aquamarine", NSVG_RGB(127, 255, 212) }, + { "azure", NSVG_RGB(240, 255, 255) }, + { "beige", NSVG_RGB(245, 245, 220) }, + { "bisque", NSVG_RGB(255, 228, 196) }, + { "blanchedalmond", NSVG_RGB(255, 235, 205) }, + { "blueviolet", NSVG_RGB(138, 43, 226) }, + { "brown", NSVG_RGB(165, 42, 42) }, + { "burlywood", NSVG_RGB(222, 184, 135) }, + { "cadetblue", NSVG_RGB( 95, 158, 160) }, + { "chartreuse", NSVG_RGB(127, 255, 0) }, + { "chocolate", NSVG_RGB(210, 105, 30) }, + { "coral", NSVG_RGB(255, 127, 80) }, + { "cornflowerblue", NSVG_RGB(100, 149, 237) }, + { "cornsilk", NSVG_RGB(255, 248, 220) }, + { "crimson", NSVG_RGB(220, 20, 60) }, + { "darkblue", NSVG_RGB( 0, 0, 139) }, + { "darkcyan", NSVG_RGB( 0, 139, 139) }, + { "darkgoldenrod", NSVG_RGB(184, 134, 11) }, + { "darkgray", NSVG_RGB(169, 169, 169) }, + { "darkgreen", NSVG_RGB( 0, 100, 0) }, + { "darkgrey", NSVG_RGB(169, 169, 169) }, + { "darkkhaki", NSVG_RGB(189, 183, 107) }, + { "darkmagenta", NSVG_RGB(139, 0, 139) }, + { "darkolivegreen", NSVG_RGB( 85, 107, 47) }, + { "darkorange", NSVG_RGB(255, 140, 0) }, + { "darkorchid", NSVG_RGB(153, 50, 204) }, + { "darkred", NSVG_RGB(139, 0, 0) }, + { "darksalmon", NSVG_RGB(233, 150, 122) }, + { "darkseagreen", NSVG_RGB(143, 188, 143) }, + { "darkslateblue", NSVG_RGB( 72, 61, 139) }, + { "darkslategray", NSVG_RGB( 47, 79, 79) }, + { "darkslategrey", NSVG_RGB( 47, 79, 79) }, + { "darkturquoise", NSVG_RGB( 0, 206, 209) }, + { "darkviolet", NSVG_RGB(148, 0, 211) }, + { "deeppink", NSVG_RGB(255, 20, 147) }, + { "deepskyblue", NSVG_RGB( 0, 191, 255) }, + { "dimgray", NSVG_RGB(105, 105, 105) }, + { "dimgrey", NSVG_RGB(105, 105, 105) }, + { "dodgerblue", NSVG_RGB( 30, 144, 255) }, + { "firebrick", NSVG_RGB(178, 34, 34) }, + { "floralwhite", NSVG_RGB(255, 250, 240) }, + { "forestgreen", NSVG_RGB( 34, 139, 34) }, + { "fuchsia", NSVG_RGB(255, 0, 255) }, + { "gainsboro", NSVG_RGB(220, 220, 220) }, + { "ghostwhite", NSVG_RGB(248, 248, 255) }, + { "gold", NSVG_RGB(255, 215, 0) }, + { "goldenrod", NSVG_RGB(218, 165, 32) }, + { "greenyellow", NSVG_RGB(173, 255, 47) }, + { "honeydew", NSVG_RGB(240, 255, 240) }, + { "hotpink", NSVG_RGB(255, 105, 180) }, + { "indianred", NSVG_RGB(205, 92, 92) }, + { "indigo", NSVG_RGB( 75, 0, 130) }, + { "ivory", NSVG_RGB(255, 255, 240) }, + { "khaki", NSVG_RGB(240, 230, 140) }, + { "lavender", NSVG_RGB(230, 230, 250) }, + { "lavenderblush", NSVG_RGB(255, 240, 245) }, + { "lawngreen", NSVG_RGB(124, 252, 0) }, + { "lemonchiffon", NSVG_RGB(255, 250, 205) }, + { "lightblue", NSVG_RGB(173, 216, 230) }, + { "lightcoral", NSVG_RGB(240, 128, 128) }, + { "lightcyan", NSVG_RGB(224, 255, 255) }, + { "lightgoldenrodyellow", NSVG_RGB(250, 250, 210) }, + { "lightgray", NSVG_RGB(211, 211, 211) }, + { "lightgreen", NSVG_RGB(144, 238, 144) }, + { "lightgrey", NSVG_RGB(211, 211, 211) }, + { "lightpink", NSVG_RGB(255, 182, 193) }, + { "lightsalmon", NSVG_RGB(255, 160, 122) }, + { "lightseagreen", NSVG_RGB( 32, 178, 170) }, + { "lightskyblue", NSVG_RGB(135, 206, 250) }, + { "lightslategray", NSVG_RGB(119, 136, 153) }, + { "lightslategrey", NSVG_RGB(119, 136, 153) }, + { "lightsteelblue", NSVG_RGB(176, 196, 222) }, + { "lightyellow", NSVG_RGB(255, 255, 224) }, + { "lime", NSVG_RGB( 0, 255, 0) }, + { "limegreen", NSVG_RGB( 50, 205, 50) }, + { "linen", NSVG_RGB(250, 240, 230) }, + { "maroon", NSVG_RGB(128, 0, 0) }, + { "mediumaquamarine", NSVG_RGB(102, 205, 170) }, + { "mediumblue", NSVG_RGB( 0, 0, 205) }, + { "mediumorchid", NSVG_RGB(186, 85, 211) }, + { "mediumpurple", NSVG_RGB(147, 112, 219) }, + { "mediumseagreen", NSVG_RGB( 60, 179, 113) }, + { "mediumslateblue", NSVG_RGB(123, 104, 238) }, + { "mediumspringgreen", NSVG_RGB( 0, 250, 154) }, + { "mediumturquoise", NSVG_RGB( 72, 209, 204) }, + { "mediumvioletred", NSVG_RGB(199, 21, 133) }, + { "midnightblue", NSVG_RGB( 25, 25, 112) }, + { "mintcream", NSVG_RGB(245, 255, 250) }, + { "mistyrose", NSVG_RGB(255, 228, 225) }, + { "moccasin", NSVG_RGB(255, 228, 181) }, + { "navajowhite", NSVG_RGB(255, 222, 173) }, + { "navy", NSVG_RGB( 0, 0, 128) }, + { "oldlace", NSVG_RGB(253, 245, 230) }, + { "olive", NSVG_RGB(128, 128, 0) }, + { "olivedrab", NSVG_RGB(107, 142, 35) }, + { "orange", NSVG_RGB(255, 165, 0) }, + { "orangered", NSVG_RGB(255, 69, 0) }, + { "orchid", NSVG_RGB(218, 112, 214) }, + { "palegoldenrod", NSVG_RGB(238, 232, 170) }, + { "palegreen", NSVG_RGB(152, 251, 152) }, + { "paleturquoise", NSVG_RGB(175, 238, 238) }, + { "palevioletred", NSVG_RGB(219, 112, 147) }, + { "papayawhip", NSVG_RGB(255, 239, 213) }, + { "peachpuff", NSVG_RGB(255, 218, 185) }, + { "peru", NSVG_RGB(205, 133, 63) }, + { "pink", NSVG_RGB(255, 192, 203) }, + { "plum", NSVG_RGB(221, 160, 221) }, + { "powderblue", NSVG_RGB(176, 224, 230) }, + { "purple", NSVG_RGB(128, 0, 128) }, + { "rosybrown", NSVG_RGB(188, 143, 143) }, + { "royalblue", NSVG_RGB( 65, 105, 225) }, + { "saddlebrown", NSVG_RGB(139, 69, 19) }, + { "salmon", NSVG_RGB(250, 128, 114) }, + { "sandybrown", NSVG_RGB(244, 164, 96) }, + { "seagreen", NSVG_RGB( 46, 139, 87) }, + { "seashell", NSVG_RGB(255, 245, 238) }, + { "sienna", NSVG_RGB(160, 82, 45) }, + { "silver", NSVG_RGB(192, 192, 192) }, + { "skyblue", NSVG_RGB(135, 206, 235) }, + { "slateblue", NSVG_RGB(106, 90, 205) }, + { "slategray", NSVG_RGB(112, 128, 144) }, + { "slategrey", NSVG_RGB(112, 128, 144) }, + { "snow", NSVG_RGB(255, 250, 250) }, + { "springgreen", NSVG_RGB( 0, 255, 127) }, + { "steelblue", NSVG_RGB( 70, 130, 180) }, + { "tan", NSVG_RGB(210, 180, 140) }, + { "teal", NSVG_RGB( 0, 128, 128) }, + { "thistle", NSVG_RGB(216, 191, 216) }, + { "tomato", NSVG_RGB(255, 99, 71) }, + { "turquoise", NSVG_RGB( 64, 224, 208) }, + { "violet", NSVG_RGB(238, 130, 238) }, + { "wheat", NSVG_RGB(245, 222, 179) }, + { "whitesmoke", NSVG_RGB(245, 245, 245) }, + { "yellowgreen", NSVG_RGB(154, 205, 50) }, +#endif +}; + +static unsigned int nsvg__parseColorName(const char* str) +{ + int i, ncolors = sizeof(nsvg__colors) / sizeof(NSVGNamedColor); + + for (i = 0; i < ncolors; i++) { + if (strcmp(nsvg__colors[i].name, str) == 0) { + return nsvg__colors[i].color; + } + } + + return NSVG_RGB(128, 128, 128); +} + +static unsigned int nsvg__parseColor(const char* str) +{ + size_t len = 0; + while(*str == ' ') ++str; + len = strlen(str); + if (len >= 1 && *str == '#') + return nsvg__parseColorHex(str); + else if (len >= 4 && str[0] == 'r' && str[1] == 'g' && str[2] == 'b' && str[3] == '(') + return nsvg__parseColorRGB(str); + return nsvg__parseColorName(str); +} + +static float nsvg__parseOpacity(const char* str) +{ + float val = nsvg__atof(str); + if (val < 0.0f) val = 0.0f; + if (val > 1.0f) val = 1.0f; + return val; +} + +static float nsvg__parseMiterLimit(const char* str) +{ + float val = nsvg__atof(str); + if (val < 0.0f) val = 0.0f; + return val; +} + +static int nsvg__parseUnits(const char* units) +{ + if (units[0] == 'p' && units[1] == 'x') + return NSVG_UNITS_PX; + else if (units[0] == 'p' && units[1] == 't') + return NSVG_UNITS_PT; + else if (units[0] == 'p' && units[1] == 'c') + return NSVG_UNITS_PC; + else if (units[0] == 'm' && units[1] == 'm') + return NSVG_UNITS_MM; + else if (units[0] == 'c' && units[1] == 'm') + return NSVG_UNITS_CM; + else if (units[0] == 'i' && units[1] == 'n') + return NSVG_UNITS_IN; + else if (units[0] == '%') + return NSVG_UNITS_PERCENT; + else if (units[0] == 'e' && units[1] == 'm') + return NSVG_UNITS_EM; + else if (units[0] == 'e' && units[1] == 'x') + return NSVG_UNITS_EX; + return NSVG_UNITS_USER; +} + +static int nsvg__isCoordinate(const char* s) +{ + // optional sign + if (*s == '-' || *s == '+') + s++; + // must have at least one digit, or start by a dot + return (nsvg__isdigit(*s) || *s == '.'); +} + +static NSVGcoordinate nsvg__parseCoordinateRaw(const char* str) +{ + NSVGcoordinate coord = {0, NSVG_UNITS_USER}; + char buf[64]; + coord.units = nsvg__parseUnits(nsvg__parseNumber(str, buf, 64)); + coord.value = nsvg__atof(buf); + return coord; +} + +static NSVGcoordinate nsvg__coord(float v, int units) +{ + NSVGcoordinate coord = {v, units}; + return coord; +} + +static float nsvg__parseCoordinate(NSVGparser* p, const char* str, float orig, float length) +{ + NSVGcoordinate coord = nsvg__parseCoordinateRaw(str); + return nsvg__convertToPixels(p, coord, orig, length); +} + +static int nsvg__parseTransformArgs(const char* str, float* args, int maxNa, int* na) +{ + const char* end; + const char* ptr; + char it[64]; + + *na = 0; + ptr = str; + while (*ptr && *ptr != '(') ++ptr; + if (*ptr == 0) + return 1; + end = ptr; + while (*end && *end != ')') ++end; + if (*end == 0) + return 1; + + while (ptr < end) { + if (*ptr == '-' || *ptr == '+' || *ptr == '.' || nsvg__isdigit(*ptr)) { + if (*na >= maxNa) return 0; + ptr = nsvg__parseNumber(ptr, it, 64); + args[(*na)++] = (float)nsvg__atof(it); + } else { + ++ptr; + } + } + return (int)(end - str); +} + + +static int nsvg__parseMatrix(float* xform, const char* str) +{ + float t[6]; + int na = 0; + int len = nsvg__parseTransformArgs(str, t, 6, &na); + if (na != 6) return len; + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseTranslate(float* xform, const char* str) +{ + float args[2]; + float t[6]; + int na = 0; + int len = nsvg__parseTransformArgs(str, args, 2, &na); + if (na == 1) args[1] = 0.0; + + nsvg__xformSetTranslation(t, args[0], args[1]); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseScale(float* xform, const char* str) +{ + float args[2]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 2, &na); + if (na == 1) args[1] = args[0]; + nsvg__xformSetScale(t, args[0], args[1]); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseSkewX(float* xform, const char* str) +{ + float args[1]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 1, &na); + nsvg__xformSetSkewX(t, args[0]/180.0f*NSVG_PI); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseSkewY(float* xform, const char* str) +{ + float args[1]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 1, &na); + nsvg__xformSetSkewY(t, args[0]/180.0f*NSVG_PI); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseRotate(float* xform, const char* str) +{ + float args[3]; + int na = 0; + float m[6]; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 3, &na); + if (na == 1) + args[1] = args[2] = 0.0f; + nsvg__xformIdentity(m); + + if (na > 1) { + nsvg__xformSetTranslation(t, -args[1], -args[2]); + nsvg__xformMultiply(m, t); + } + + nsvg__xformSetRotation(t, args[0]/180.0f*NSVG_PI); + nsvg__xformMultiply(m, t); + + if (na > 1) { + nsvg__xformSetTranslation(t, args[1], args[2]); + nsvg__xformMultiply(m, t); + } + + memcpy(xform, m, sizeof(float)*6); + + return len; +} + +static void nsvg__parseTransform(float* xform, const char* str) +{ + float t[6]; + int len; + nsvg__xformIdentity(xform); + while (*str) + { + if (strncmp(str, "matrix", 6) == 0) + len = nsvg__parseMatrix(t, str); + else if (strncmp(str, "translate", 9) == 0) + len = nsvg__parseTranslate(t, str); + else if (strncmp(str, "scale", 5) == 0) + len = nsvg__parseScale(t, str); + else if (strncmp(str, "rotate", 6) == 0) + len = nsvg__parseRotate(t, str); + else if (strncmp(str, "skewX", 5) == 0) + len = nsvg__parseSkewX(t, str); + else if (strncmp(str, "skewY", 5) == 0) + len = nsvg__parseSkewY(t, str); + else{ + ++str; + continue; + } + if (len != 0) { + str += len; + } else { + ++str; + continue; + } + + nsvg__xformPremultiply(xform, t); + } +} + +static void nsvg__parseUrl(char* id, const char* str) +{ + int i = 0; + str += 4; // "url("; + if (*str && *str == '#') + str++; + while (i < 63 && *str && *str != ')') { + id[i] = *str++; + i++; + } + id[i] = '\0'; +} + +static char nsvg__parseLineCap(const char* str) +{ + if (strcmp(str, "butt") == 0) + return NSVG_CAP_BUTT; + else if (strcmp(str, "round") == 0) + return NSVG_CAP_ROUND; + else if (strcmp(str, "square") == 0) + return NSVG_CAP_SQUARE; + // TODO: handle inherit. + return NSVG_CAP_BUTT; +} + +static char nsvg__parseLineJoin(const char* str) +{ + if (strcmp(str, "miter") == 0) + return NSVG_JOIN_MITER; + else if (strcmp(str, "round") == 0) + return NSVG_JOIN_ROUND; + else if (strcmp(str, "bevel") == 0) + return NSVG_JOIN_BEVEL; + // TODO: handle inherit. + return NSVG_JOIN_MITER; +} + +static char nsvg__parseFillRule(const char* str) +{ + if (strcmp(str, "nonzero") == 0) + return NSVG_FILLRULE_NONZERO; + else if (strcmp(str, "evenodd") == 0) + return NSVG_FILLRULE_EVENODD; + // TODO: handle inherit. + return NSVG_FILLRULE_NONZERO; +} + +static const char* nsvg__getNextDashItem(const char* s, char* it) +{ + int n = 0; + it[0] = '\0'; + // Skip white spaces and commas + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + // Advance until whitespace, comma or end. + while (*s && (!nsvg__isspace(*s) && *s != ',')) { + if (n < 63) + it[n++] = *s; + s++; + } + it[n++] = '\0'; + return s; +} + +static int nsvg__parseStrokeDashArray(NSVGparser* p, const char* str, float* strokeDashArray) +{ + char item[64]; + int count = 0, i; + float sum = 0.0f; + + // Handle "none" + if (str[0] == 'n') + return 0; + + // Parse dashes + while (*str) { + str = nsvg__getNextDashItem(str, item); + if (!*item) break; + if (count < NSVG_MAX_DASHES) + strokeDashArray[count++] = fabsf(nsvg__parseCoordinate(p, item, 0.0f, nsvg__actualLength(p))); + } + + for (i = 0; i < count; i++) + sum += strokeDashArray[i]; + if (sum <= 1e-6f) + count = 0; + + return count; +} + +static void nsvg__parseStyle(NSVGparser* p, const char* str); + +static int nsvg__parseAttr(NSVGparser* p, const char* name, const char* value) +{ + float xform[6]; + NSVGattrib* attr = nsvg__getAttr(p); + if (!attr) return 0; + + if (strcmp(name, "style") == 0) { + nsvg__parseStyle(p, value); + } else if (strcmp(name, "display") == 0) { + if (strcmp(value, "none") == 0) + attr->visible = 0; + // Don't reset ->visible on display:inline, one display:none hides the whole subtree + + } else if (strcmp(name, "fill") == 0) { + if (strcmp(value, "none") == 0) { + attr->hasFill = 0; + } else if (strncmp(value, "url(", 4) == 0) { + attr->hasFill = 2; + nsvg__parseUrl(attr->fillGradient, value); + } else { + attr->hasFill = 1; + attr->fillColor = nsvg__parseColor(value); + } + } else if (strcmp(name, "opacity") == 0) { + attr->opacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "fill-opacity") == 0) { + attr->fillOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "stroke") == 0) { + if (strcmp(value, "none") == 0) { + attr->hasStroke = 0; + } else if (strncmp(value, "url(", 4) == 0) { + attr->hasStroke = 2; + nsvg__parseUrl(attr->strokeGradient, value); + } else { + attr->hasStroke = 1; + attr->strokeColor = nsvg__parseColor(value); + } + } else if (strcmp(name, "stroke-width") == 0) { + attr->strokeWidth = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "stroke-dasharray") == 0) { + attr->strokeDashCount = nsvg__parseStrokeDashArray(p, value, attr->strokeDashArray); + } else if (strcmp(name, "stroke-dashoffset") == 0) { + attr->strokeDashOffset = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "stroke-opacity") == 0) { + attr->strokeOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "stroke-linecap") == 0) { + attr->strokeLineCap = nsvg__parseLineCap(value); + } else if (strcmp(name, "stroke-linejoin") == 0) { + attr->strokeLineJoin = nsvg__parseLineJoin(value); + } else if (strcmp(name, "stroke-miterlimit") == 0) { + attr->miterLimit = nsvg__parseMiterLimit(value); + } else if (strcmp(name, "fill-rule") == 0) { + attr->fillRule = nsvg__parseFillRule(value); + } else if (strcmp(name, "font-size") == 0) { + attr->fontSize = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "transform") == 0) { + nsvg__parseTransform(xform, value); + nsvg__xformPremultiply(attr->xform, xform); + } else if (strcmp(name, "stop-color") == 0) { + attr->stopColor = nsvg__parseColor(value); + } else if (strcmp(name, "stop-opacity") == 0) { + attr->stopOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "offset") == 0) { + attr->stopOffset = nsvg__parseCoordinate(p, value, 0.0f, 1.0f); + } else if (strcmp(name, "id") == 0) { + strncpy(attr->id, value, 63); + attr->id[63] = '\0'; + } else { + return 0; + } + return 1; +} + +static int nsvg__parseNameValue(NSVGparser* p, const char* start, const char* end) +{ + const char* str; + const char* val; + char name[512]; + char value[512]; + int n; + + str = start; + while (str < end && *str != ':') ++str; + + val = str; + + // Right Trim + while (str > start && (*str == ':' || nsvg__isspace(*str))) --str; + ++str; + + n = (int)(str - start); + if (n > 511) n = 511; + if (n) memcpy(name, start, n); + name[n] = 0; + + while (val < end && (*val == ':' || nsvg__isspace(*val))) ++val; + + n = (int)(end - val); + if (n > 511) n = 511; + if (n) memcpy(value, val, n); + value[n] = 0; + + return nsvg__parseAttr(p, name, value); +} + +static void nsvg__parseStyle(NSVGparser* p, const char* str) +{ + const char* start; + const char* end; + + while (*str) { + // Left Trim + while(*str && nsvg__isspace(*str)) ++str; + start = str; + while(*str && *str != ';') ++str; + end = str; + + // Right Trim + while (end > start && (*end == ';' || nsvg__isspace(*end))) --end; + ++end; + + nsvg__parseNameValue(p, start, end); + if (*str) ++str; + } +} + +static void nsvg__parseAttribs(NSVGparser* p, const char** attr) +{ + int i; + for (i = 0; attr[i]; i += 2) + { + if (strcmp(attr[i], "style") == 0) + nsvg__parseStyle(p, attr[i + 1]); + else + nsvg__parseAttr(p, attr[i], attr[i + 1]); + } +} + +static int nsvg__getArgsPerElement(char cmd) +{ + switch (cmd) { + case 'v': + case 'V': + case 'h': + case 'H': + return 1; + case 'm': + case 'M': + case 'l': + case 'L': + case 't': + case 'T': + return 2; + case 'q': + case 'Q': + case 's': + case 'S': + return 4; + case 'c': + case 'C': + return 6; + case 'a': + case 'A': + return 7; + case 'z': + case 'Z': + return 0; + } + return -1; +} + +static void nsvg__pathMoveTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) { + *cpx += args[0]; + *cpy += args[1]; + } else { + *cpx = args[0]; + *cpy = args[1]; + } + nsvg__moveTo(p, *cpx, *cpy); +} + +static void nsvg__pathLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) { + *cpx += args[0]; + *cpy += args[1]; + } else { + *cpx = args[0]; + *cpy = args[1]; + } + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathHLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) + *cpx += args[0]; + else + *cpx = args[0]; + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathVLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) + *cpy += args[0]; + else + *cpy = args[0]; + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathCubicBezTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x2, y2, cx1, cy1, cx2, cy2; + + if (rel) { + cx1 = *cpx + args[0]; + cy1 = *cpy + args[1]; + cx2 = *cpx + args[2]; + cy2 = *cpy + args[3]; + x2 = *cpx + args[4]; + y2 = *cpy + args[5]; + } else { + cx1 = args[0]; + cy1 = args[1]; + cx2 = args[2]; + cy2 = args[3]; + x2 = args[4]; + y2 = args[5]; + } + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx2; + *cpy2 = cy2; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathCubicBezShortTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + cx2 = *cpx + args[0]; + cy2 = *cpy + args[1]; + x2 = *cpx + args[2]; + y2 = *cpy + args[3]; + } else { + cx2 = args[0]; + cy2 = args[1]; + x2 = args[2]; + y2 = args[3]; + } + + cx1 = 2*x1 - *cpx2; + cy1 = 2*y1 - *cpy2; + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx2; + *cpy2 = cy2; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathQuadBezTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx, cy; + float cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + cx = *cpx + args[0]; + cy = *cpy + args[1]; + x2 = *cpx + args[2]; + y2 = *cpy + args[3]; + } else { + cx = args[0]; + cy = args[1]; + x2 = args[2]; + y2 = args[3]; + } + + // Convert to cubic bezier + cx1 = x1 + 2.0f/3.0f*(cx - x1); + cy1 = y1 + 2.0f/3.0f*(cy - y1); + cx2 = x2 + 2.0f/3.0f*(cx - x2); + cy2 = y2 + 2.0f/3.0f*(cy - y2); + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx; + *cpy2 = cy; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathQuadBezShortTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx, cy; + float cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + x2 = *cpx + args[0]; + y2 = *cpy + args[1]; + } else { + x2 = args[0]; + y2 = args[1]; + } + + cx = 2*x1 - *cpx2; + cy = 2*y1 - *cpy2; + + // Convert to cubix bezier + cx1 = x1 + 2.0f/3.0f*(cx - x1); + cy1 = y1 + 2.0f/3.0f*(cy - y1); + cx2 = x2 + 2.0f/3.0f*(cx - x2); + cy2 = y2 + 2.0f/3.0f*(cy - y2); + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx; + *cpy2 = cy; + *cpx = x2; + *cpy = y2; +} + +static float nsvg__sqr(float x) { return x*x; } +static float nsvg__vmag(float x, float y) { return sqrtf(x*x + y*y); } + +static float nsvg__vecrat(float ux, float uy, float vx, float vy) +{ + return (ux*vx + uy*vy) / (nsvg__vmag(ux,uy) * nsvg__vmag(vx,vy)); +} + +static float nsvg__vecang(float ux, float uy, float vx, float vy) +{ + float r = nsvg__vecrat(ux,uy, vx,vy); + if (r < -1.0f) r = -1.0f; + if (r > 1.0f) r = 1.0f; + return ((ux*vy < uy*vx) ? -1.0f : 1.0f) * acosf(r); +} + +static void nsvg__pathArcTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + // Ported from canvg (https://code.google.com/p/canvg/) + float rx, ry, rotx; + float x1, y1, x2, y2, cx, cy, dx, dy, d; + float x1p, y1p, cxp, cyp, s, sa, sb; + float ux, uy, vx, vy, a1, da; + float x, y, tanx, tany, a, px = 0, py = 0, ptanx = 0, ptany = 0, t[6]; + float sinrx, cosrx; + int fa, fs; + int i, ndivs; + float hda, kappa; + + rx = fabsf(args[0]); // y radius + ry = fabsf(args[1]); // x radius + rotx = args[2] / 180.0f * NSVG_PI; // x rotation angle + fa = fabsf(args[3]) > 1e-6 ? 1 : 0; // Large arc + fs = fabsf(args[4]) > 1e-6 ? 1 : 0; // Sweep direction + x1 = *cpx; // start point + y1 = *cpy; + if (rel) { // end point + x2 = *cpx + args[5]; + y2 = *cpy + args[6]; + } else { + x2 = args[5]; + y2 = args[6]; + } + + dx = x1 - x2; + dy = y1 - y2; + d = sqrtf(dx*dx + dy*dy); + if (d < 1e-6f || rx < 1e-6f || ry < 1e-6f) { + // The arc degenerates to a line + nsvg__lineTo(p, x2, y2); + *cpx = x2; + *cpy = y2; + return; + } + + sinrx = sinf(rotx); + cosrx = cosf(rotx); + + // Convert to center point parameterization. + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + // 1) Compute x1', y1' + x1p = cosrx * dx / 2.0f + sinrx * dy / 2.0f; + y1p = -sinrx * dx / 2.0f + cosrx * dy / 2.0f; + d = nsvg__sqr(x1p)/nsvg__sqr(rx) + nsvg__sqr(y1p)/nsvg__sqr(ry); + if (d > 1) { + d = sqrtf(d); + rx *= d; + ry *= d; + } + // 2) Compute cx', cy' + s = 0.0f; + sa = nsvg__sqr(rx)*nsvg__sqr(ry) - nsvg__sqr(rx)*nsvg__sqr(y1p) - nsvg__sqr(ry)*nsvg__sqr(x1p); + sb = nsvg__sqr(rx)*nsvg__sqr(y1p) + nsvg__sqr(ry)*nsvg__sqr(x1p); + if (sa < 0.0f) sa = 0.0f; + if (sb > 0.0f) + s = sqrtf(sa / sb); + if (fa == fs) + s = -s; + cxp = s * rx * y1p / ry; + cyp = s * -ry * x1p / rx; + + // 3) Compute cx,cy from cx',cy' + cx = (x1 + x2)/2.0f + cosrx*cxp - sinrx*cyp; + cy = (y1 + y2)/2.0f + sinrx*cxp + cosrx*cyp; + + // 4) Calculate theta1, and delta theta. + ux = (x1p - cxp) / rx; + uy = (y1p - cyp) / ry; + vx = (-x1p - cxp) / rx; + vy = (-y1p - cyp) / ry; + a1 = nsvg__vecang(1.0f,0.0f, ux,uy); // Initial angle + da = nsvg__vecang(ux,uy, vx,vy); // Delta angle + +// if (vecrat(ux,uy,vx,vy) <= -1.0f) da = NSVG_PI; +// if (vecrat(ux,uy,vx,vy) >= 1.0f) da = 0; + + if (fs == 0 && da > 0) + da -= 2 * NSVG_PI; + else if (fs == 1 && da < 0) + da += 2 * NSVG_PI; + + // Approximate the arc using cubic spline segments. + t[0] = cosrx; t[1] = sinrx; + t[2] = -sinrx; t[3] = cosrx; + t[4] = cx; t[5] = cy; + + // Split arc into max 90 degree segments. + // The loop assumes an iteration per end point (including start and end), this +1. + ndivs = (int)(fabsf(da) / (NSVG_PI*0.5f) + 1.0f); + hda = (da / (float)ndivs) / 2.0f; + // Fix for ticket #179: division by 0: avoid cotangens around 0 (infinite) + if ((hda < 1e-3f) && (hda > -1e-3f)) + hda *= 0.5f; + else + hda = (1.0f - cosf(hda)) / sinf(hda); + kappa = fabsf(4.0f / 3.0f * hda); + if (da < 0.0f) + kappa = -kappa; + + for (i = 0; i <= ndivs; i++) { + a = a1 + da * ((float)i/(float)ndivs); + dx = cosf(a); + dy = sinf(a); + nsvg__xformPoint(&x, &y, dx*rx, dy*ry, t); // position + nsvg__xformVec(&tanx, &tany, -dy*rx * kappa, dx*ry * kappa, t); // tangent + if (i > 0) + nsvg__cubicBezTo(p, px+ptanx,py+ptany, x-tanx, y-tany, x, y); + px = x; + py = y; + ptanx = tanx; + ptany = tany; + } + + *cpx = x2; + *cpy = y2; +} + +static void nsvg__parsePath(NSVGparser* p, const char** attr) +{ + const char* s = NULL; + char cmd = '\0'; + float args[10]; + int nargs; + int rargs = 0; + char initPoint; + float cpx, cpy, cpx2, cpy2; + const char* tmp[4]; + char closedFlag; + int i; + char item[64]; + + for (i = 0; attr[i]; i += 2) { + if (strcmp(attr[i], "d") == 0) { + s = attr[i + 1]; + } else { + tmp[0] = attr[i]; + tmp[1] = attr[i + 1]; + tmp[2] = 0; + tmp[3] = 0; + nsvg__parseAttribs(p, tmp); + } + } + + if (s) { + nsvg__resetPath(p); + cpx = 0; cpy = 0; + cpx2 = 0; cpy2 = 0; + initPoint = 0; + closedFlag = 0; + nargs = 0; + + while (*s) { + item[0] = '\0'; + if ((cmd == 'A' || cmd == 'a') && (nargs == 3 || nargs == 4)) + s = nsvg__getNextPathItemWhenArcFlag(s, item); + if (!*item) + s = nsvg__getNextPathItem(s, item); + if (!*item) break; + if (cmd != '\0' && nsvg__isCoordinate(item)) { + if (nargs < 10) + args[nargs++] = (float)nsvg__atof(item); + if (nargs >= rargs) { + switch (cmd) { + case 'm': + case 'M': + nsvg__pathMoveTo(p, &cpx, &cpy, args, cmd == 'm' ? 1 : 0); + // Moveto can be followed by multiple coordinate pairs, + // which should be treated as linetos. + cmd = (cmd == 'm') ? 'l' : 'L'; + rargs = nsvg__getArgsPerElement(cmd); + cpx2 = cpx; cpy2 = cpy; + initPoint = 1; + break; + case 'l': + case 'L': + nsvg__pathLineTo(p, &cpx, &cpy, args, cmd == 'l' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'H': + case 'h': + nsvg__pathHLineTo(p, &cpx, &cpy, args, cmd == 'h' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'V': + case 'v': + nsvg__pathVLineTo(p, &cpx, &cpy, args, cmd == 'v' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'C': + case 'c': + nsvg__pathCubicBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'c' ? 1 : 0); + break; + case 'S': + case 's': + nsvg__pathCubicBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 's' ? 1 : 0); + break; + case 'Q': + case 'q': + nsvg__pathQuadBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'q' ? 1 : 0); + break; + case 'T': + case 't': + nsvg__pathQuadBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 't' ? 1 : 0); + break; + case 'A': + case 'a': + nsvg__pathArcTo(p, &cpx, &cpy, args, cmd == 'a' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + default: + if (nargs >= 2) { + cpx = args[nargs-2]; + cpy = args[nargs-1]; + cpx2 = cpx; cpy2 = cpy; + } + break; + } + nargs = 0; + } + } else { + cmd = item[0]; + if (cmd == 'M' || cmd == 'm') { + // Commit path. + if (p->npts > 0) + nsvg__addPath(p, closedFlag); + // Start new subpath. + nsvg__resetPath(p); + closedFlag = 0; + nargs = 0; + } else if (initPoint == 0) { + // Do not allow other commands until initial point has been set (moveTo called once). + cmd = '\0'; + } + if (cmd == 'Z' || cmd == 'z') { + closedFlag = 1; + // Commit path. + if (p->npts > 0) { + // Move current point to first point + cpx = p->pts[0]; + cpy = p->pts[1]; + cpx2 = cpx; cpy2 = cpy; + nsvg__addPath(p, closedFlag); + } + // Start new subpath. + nsvg__resetPath(p); + nsvg__moveTo(p, cpx, cpy); + closedFlag = 0; + nargs = 0; + } + rargs = nsvg__getArgsPerElement(cmd); + if (rargs == -1) { + // Command not recognized + cmd = '\0'; + rargs = 0; + } + } + } + // Commit path. + if (p->npts) + nsvg__addPath(p, closedFlag); + } + + nsvg__addShape(p); +} + +static void nsvg__parseRect(NSVGparser* p, const char** attr) +{ + float x = 0.0f; + float y = 0.0f; + float w = 0.0f; + float h = 0.0f; + float rx = -1.0f; // marks not set + float ry = -1.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "x") == 0) x = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y") == 0) y = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "width") == 0) w = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p)); + if (strcmp(attr[i], "height") == 0) h = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p)); + if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); + if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); + } + } + + if (rx < 0.0f && ry > 0.0f) rx = ry; + if (ry < 0.0f && rx > 0.0f) ry = rx; + if (rx < 0.0f) rx = 0.0f; + if (ry < 0.0f) ry = 0.0f; + if (rx > w/2.0f) rx = w/2.0f; + if (ry > h/2.0f) ry = h/2.0f; + + if (w != 0.0f && h != 0.0f) { + nsvg__resetPath(p); + + if (rx < 0.00001f || ry < 0.0001f) { + nsvg__moveTo(p, x, y); + nsvg__lineTo(p, x+w, y); + nsvg__lineTo(p, x+w, y+h); + nsvg__lineTo(p, x, y+h); + } else { + // Rounded rectangle + nsvg__moveTo(p, x+rx, y); + nsvg__lineTo(p, x+w-rx, y); + nsvg__cubicBezTo(p, x+w-rx*(1-NSVG_KAPPA90), y, x+w, y+ry*(1-NSVG_KAPPA90), x+w, y+ry); + nsvg__lineTo(p, x+w, y+h-ry); + nsvg__cubicBezTo(p, x+w, y+h-ry*(1-NSVG_KAPPA90), x+w-rx*(1-NSVG_KAPPA90), y+h, x+w-rx, y+h); + nsvg__lineTo(p, x+rx, y+h); + nsvg__cubicBezTo(p, x+rx*(1-NSVG_KAPPA90), y+h, x, y+h-ry*(1-NSVG_KAPPA90), x, y+h-ry); + nsvg__lineTo(p, x, y+ry); + nsvg__cubicBezTo(p, x, y+ry*(1-NSVG_KAPPA90), x+rx*(1-NSVG_KAPPA90), y, x+rx, y); + } + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseCircle(NSVGparser* p, const char** attr) +{ + float cx = 0.0f; + float cy = 0.0f; + float r = 0.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "r") == 0) r = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualLength(p))); + } + } + + if (r > 0.0f) { + nsvg__resetPath(p); + + nsvg__moveTo(p, cx+r, cy); + nsvg__cubicBezTo(p, cx+r, cy+r*NSVG_KAPPA90, cx+r*NSVG_KAPPA90, cy+r, cx, cy+r); + nsvg__cubicBezTo(p, cx-r*NSVG_KAPPA90, cy+r, cx-r, cy+r*NSVG_KAPPA90, cx-r, cy); + nsvg__cubicBezTo(p, cx-r, cy-r*NSVG_KAPPA90, cx-r*NSVG_KAPPA90, cy-r, cx, cy-r); + nsvg__cubicBezTo(p, cx+r*NSVG_KAPPA90, cy-r, cx+r, cy-r*NSVG_KAPPA90, cx+r, cy); + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseEllipse(NSVGparser* p, const char** attr) +{ + float cx = 0.0f; + float cy = 0.0f; + float rx = 0.0f; + float ry = 0.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); + if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); + } + } + + if (rx > 0.0f && ry > 0.0f) { + + nsvg__resetPath(p); + + nsvg__moveTo(p, cx+rx, cy); + nsvg__cubicBezTo(p, cx+rx, cy+ry*NSVG_KAPPA90, cx+rx*NSVG_KAPPA90, cy+ry, cx, cy+ry); + nsvg__cubicBezTo(p, cx-rx*NSVG_KAPPA90, cy+ry, cx-rx, cy+ry*NSVG_KAPPA90, cx-rx, cy); + nsvg__cubicBezTo(p, cx-rx, cy-ry*NSVG_KAPPA90, cx-rx*NSVG_KAPPA90, cy-ry, cx, cy-ry); + nsvg__cubicBezTo(p, cx+rx*NSVG_KAPPA90, cy-ry, cx+rx, cy-ry*NSVG_KAPPA90, cx+rx, cy); + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseLine(NSVGparser* p, const char** attr) +{ + float x1 = 0.0; + float y1 = 0.0; + float x2 = 0.0; + float y2 = 0.0; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "x1") == 0) x1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y1") == 0) y1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "x2") == 0) x2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y2") == 0) y2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + } + } + + nsvg__resetPath(p); + + nsvg__moveTo(p, x1, y1); + nsvg__lineTo(p, x2, y2); + + nsvg__addPath(p, 0); + + nsvg__addShape(p); +} + +static void nsvg__parsePoly(NSVGparser* p, const char** attr, int closeFlag) +{ + int i; + const char* s; + float args[2]; + int nargs, npts = 0; + char item[64]; + + nsvg__resetPath(p); + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "points") == 0) { + s = attr[i + 1]; + nargs = 0; + while (*s) { + s = nsvg__getNextPathItem(s, item); + args[nargs++] = (float)nsvg__atof(item); + if (nargs >= 2) { + if (npts == 0) + nsvg__moveTo(p, args[0], args[1]); + else + nsvg__lineTo(p, args[0], args[1]); + nargs = 0; + npts++; + } + } + } + } + } + + nsvg__addPath(p, (char)closeFlag); + + nsvg__addShape(p); +} + +static void nsvg__parseSVG(NSVGparser* p, const char** attr) +{ + int i; + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "width") == 0) { + p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); + } else if (strcmp(attr[i], "height") == 0) { + p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); + } else if (strcmp(attr[i], "viewBox") == 0) { + const char *s = attr[i + 1]; + char buf[64]; + s = nsvg__parseNumber(s, buf, 64); + p->viewMinx = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewMiny = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewWidth = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewHeight = nsvg__atof(buf); + } else if (strcmp(attr[i], "preserveAspectRatio") == 0) { + if (strstr(attr[i + 1], "none") != 0) { + // No uniform scaling + p->alignType = NSVG_ALIGN_NONE; + } else { + // Parse X align + if (strstr(attr[i + 1], "xMin") != 0) + p->alignX = NSVG_ALIGN_MIN; + else if (strstr(attr[i + 1], "xMid") != 0) + p->alignX = NSVG_ALIGN_MID; + else if (strstr(attr[i + 1], "xMax") != 0) + p->alignX = NSVG_ALIGN_MAX; + // Parse X align + if (strstr(attr[i + 1], "yMin") != 0) + p->alignY = NSVG_ALIGN_MIN; + else if (strstr(attr[i + 1], "yMid") != 0) + p->alignY = NSVG_ALIGN_MID; + else if (strstr(attr[i + 1], "yMax") != 0) + p->alignY = NSVG_ALIGN_MAX; + // Parse meet/slice + p->alignType = NSVG_ALIGN_MEET; + if (strstr(attr[i + 1], "slice") != 0) + p->alignType = NSVG_ALIGN_SLICE; + } + } + } + } +} + +static void nsvg__parseGradient(NSVGparser* p, const char** attr, signed char type) +{ + int i; + NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); + if (grad == NULL) return; + memset(grad, 0, sizeof(NSVGgradientData)); + grad->units = NSVG_OBJECT_SPACE; + grad->type = type; + if (grad->type == NSVG_PAINT_LINEAR_GRADIENT) { + grad->linear.x1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + grad->linear.y1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + grad->linear.x2 = nsvg__coord(100.0f, NSVG_UNITS_PERCENT); + grad->linear.y2 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + } else if (grad->type == NSVG_PAINT_RADIAL_GRADIENT) { + grad->radial.cx = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + grad->radial.cy = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + grad->radial.r = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + } + + nsvg__xformIdentity(grad->xform); + + for (i = 0; attr[i]; i += 2) { + if (strcmp(attr[i], "id") == 0) { + strncpy(grad->id, attr[i+1], 63); + grad->id[63] = '\0'; + } else if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "gradientUnits") == 0) { + if (strcmp(attr[i+1], "objectBoundingBox") == 0) + grad->units = NSVG_OBJECT_SPACE; + else + grad->units = NSVG_USER_SPACE; + } else if (strcmp(attr[i], "gradientTransform") == 0) { + nsvg__parseTransform(grad->xform, attr[i + 1]); + } else if (strcmp(attr[i], "cx") == 0) { + grad->radial.cx = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "cy") == 0) { + grad->radial.cy = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "r") == 0) { + grad->radial.r = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "fx") == 0) { + grad->radial.fx = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "fy") == 0) { + grad->radial.fy = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "x1") == 0) { + grad->linear.x1 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "y1") == 0) { + grad->linear.y1 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "x2") == 0) { + grad->linear.x2 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "y2") == 0) { + grad->linear.y2 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "spreadMethod") == 0) { + if (strcmp(attr[i+1], "pad") == 0) + grad->spread = NSVG_SPREAD_PAD; + else if (strcmp(attr[i+1], "reflect") == 0) + grad->spread = NSVG_SPREAD_REFLECT; + else if (strcmp(attr[i+1], "repeat") == 0) + grad->spread = NSVG_SPREAD_REPEAT; + } else if (strcmp(attr[i], "xlink:href") == 0) { + const char *href = attr[i+1]; + strncpy(grad->ref, href+1, 62); + grad->ref[62] = '\0'; + } + } + } + + grad->next = p->gradients; + p->gradients = grad; +} + +static void nsvg__parseGradientStop(NSVGparser* p, const char** attr) +{ + NSVGattrib* curAttr = nsvg__getAttr(p); + NSVGgradientData* grad; + NSVGgradientStop* stop; + int i, idx; + + curAttr->stopOffset = 0; + curAttr->stopColor = 0; + curAttr->stopOpacity = 1.0f; + + for (i = 0; attr[i]; i += 2) { + nsvg__parseAttr(p, attr[i], attr[i + 1]); + } + + // Add stop to the last gradient. + grad = p->gradients; + if (grad == NULL) return; + + grad->nstops++; + grad->stops = (NSVGgradientStop*)realloc(grad->stops, sizeof(NSVGgradientStop)*grad->nstops); + if (grad->stops == NULL) return; + + // Insert + idx = grad->nstops-1; + for (i = 0; i < grad->nstops-1; i++) { + if (curAttr->stopOffset < grad->stops[i].offset) { + idx = i; + break; + } + } + if (idx != grad->nstops-1) { + for (i = grad->nstops-1; i > idx; i--) + grad->stops[i] = grad->stops[i-1]; + } + + stop = &grad->stops[idx]; + stop->color = curAttr->stopColor; + stop->color |= (unsigned int)(curAttr->stopOpacity*255) << 24; + stop->offset = curAttr->stopOffset; +} + +static void nsvg__startElement(void* ud, const char* el, const char** attr) +{ + NSVGparser* p = (NSVGparser*)ud; + + if (p->defsFlag) { + // Skip everything but gradients in defs + if (strcmp(el, "linearGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); + } else if (strcmp(el, "radialGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); + } else if (strcmp(el, "stop") == 0) { + nsvg__parseGradientStop(p, attr); + } + return; + } + + if (strcmp(el, "g") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + } else if (strcmp(el, "path") == 0) { + if (p->pathFlag) // Do not allow nested paths. + return; + nsvg__pushAttr(p); + nsvg__parsePath(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "rect") == 0) { + nsvg__pushAttr(p); + nsvg__parseRect(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "circle") == 0) { + nsvg__pushAttr(p); + nsvg__parseCircle(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "ellipse") == 0) { + nsvg__pushAttr(p); + nsvg__parseEllipse(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "line") == 0) { + nsvg__pushAttr(p); + nsvg__parseLine(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "polyline") == 0) { + nsvg__pushAttr(p); + nsvg__parsePoly(p, attr, 0); + nsvg__popAttr(p); + } else if (strcmp(el, "polygon") == 0) { + nsvg__pushAttr(p); + nsvg__parsePoly(p, attr, 1); + nsvg__popAttr(p); + } else if (strcmp(el, "linearGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); + } else if (strcmp(el, "radialGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); + } else if (strcmp(el, "stop") == 0) { + nsvg__parseGradientStop(p, attr); + } else if (strcmp(el, "defs") == 0) { + p->defsFlag = 1; + } else if (strcmp(el, "svg") == 0) { + nsvg__parseSVG(p, attr); + } +} + +static void nsvg__endElement(void* ud, const char* el) +{ + NSVGparser* p = (NSVGparser*)ud; + + if (strcmp(el, "g") == 0) { + nsvg__popAttr(p); + } else if (strcmp(el, "path") == 0) { + p->pathFlag = 0; + } else if (strcmp(el, "defs") == 0) { + p->defsFlag = 0; + } +} + +static void nsvg__content(void* ud, const char* s) +{ + NSVG_NOTUSED(ud); + NSVG_NOTUSED(s); + // empty +} + +static void nsvg__imageBounds(NSVGparser* p, float* bounds) +{ + NSVGshape* shape; + shape = p->image->shapes; + if (shape == NULL) { + bounds[0] = bounds[1] = bounds[2] = bounds[3] = 0.0; + return; + } + bounds[0] = shape->bounds[0]; + bounds[1] = shape->bounds[1]; + bounds[2] = shape->bounds[2]; + bounds[3] = shape->bounds[3]; + for (shape = shape->next; shape != NULL; shape = shape->next) { + bounds[0] = nsvg__minf(bounds[0], shape->bounds[0]); + bounds[1] = nsvg__minf(bounds[1], shape->bounds[1]); + bounds[2] = nsvg__maxf(bounds[2], shape->bounds[2]); + bounds[3] = nsvg__maxf(bounds[3], shape->bounds[3]); + } +} + +static float nsvg__viewAlign(float content, float container, int type) +{ + if (type == NSVG_ALIGN_MIN) + return 0; + else if (type == NSVG_ALIGN_MAX) + return container - content; + // mid + return (container - content) * 0.5f; +} + +static void nsvg__scaleGradient(NSVGgradient* grad, float tx, float ty, float sx, float sy) +{ + float t[6]; + nsvg__xformSetTranslation(t, tx, ty); + nsvg__xformMultiply (grad->xform, t); + + nsvg__xformSetScale(t, sx, sy); + nsvg__xformMultiply (grad->xform, t); +} + +static void nsvg__scaleToViewbox(NSVGparser* p, const char* units) +{ + NSVGshape* shape; + NSVGpath* path; + float tx, ty, sx, sy, us, bounds[4], t[6], avgs; + int i; + float* pt; + + // Guess image size if not set completely. + nsvg__imageBounds(p, bounds); + + if (p->viewWidth == 0) { + if (p->image->width > 0) { + p->viewWidth = p->image->width; + } else { + p->viewMinx = bounds[0]; + p->viewWidth = bounds[2] - bounds[0]; + } + } + if (p->viewHeight == 0) { + if (p->image->height > 0) { + p->viewHeight = p->image->height; + } else { + p->viewMiny = bounds[1]; + p->viewHeight = bounds[3] - bounds[1]; + } + } + if (p->image->width == 0) + p->image->width = p->viewWidth; + if (p->image->height == 0) + p->image->height = p->viewHeight; + + tx = -p->viewMinx; + ty = -p->viewMiny; + sx = p->viewWidth > 0 ? p->image->width / p->viewWidth : 0; + sy = p->viewHeight > 0 ? p->image->height / p->viewHeight : 0; + // Unit scaling + us = 1.0f / nsvg__convertToPixels(p, nsvg__coord(1.0f, nsvg__parseUnits(units)), 0.0f, 1.0f); + + // Fix aspect ratio + if (p->alignType == NSVG_ALIGN_MEET) { + // fit whole image into viewbox + sx = sy = nsvg__minf(sx, sy); + tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; + ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; + } else if (p->alignType == NSVG_ALIGN_SLICE) { + // fill whole viewbox with image + sx = sy = nsvg__maxf(sx, sy); + tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; + ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; + } + + // Transform + sx *= us; + sy *= us; + avgs = (sx+sy) / 2.0f; + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + shape->bounds[0] = (shape->bounds[0] + tx) * sx; + shape->bounds[1] = (shape->bounds[1] + ty) * sy; + shape->bounds[2] = (shape->bounds[2] + tx) * sx; + shape->bounds[3] = (shape->bounds[3] + ty) * sy; + for (path = shape->paths; path != NULL; path = path->next) { + path->bounds[0] = (path->bounds[0] + tx) * sx; + path->bounds[1] = (path->bounds[1] + ty) * sy; + path->bounds[2] = (path->bounds[2] + tx) * sx; + path->bounds[3] = (path->bounds[3] + ty) * sy; + for (i =0; i < path->npts; i++) { + pt = &path->pts[i*2]; + pt[0] = (pt[0] + tx) * sx; + pt[1] = (pt[1] + ty) * sy; + } + } + + if (shape->fill.type == NSVG_PAINT_LINEAR_GRADIENT || shape->fill.type == NSVG_PAINT_RADIAL_GRADIENT) { + nsvg__scaleGradient(shape->fill.gradient, tx,ty, sx,sy); + memcpy(t, shape->fill.gradient->xform, sizeof(float)*6); + nsvg__xformInverse(shape->fill.gradient->xform, t); + } + if (shape->stroke.type == NSVG_PAINT_LINEAR_GRADIENT || shape->stroke.type == NSVG_PAINT_RADIAL_GRADIENT) { + nsvg__scaleGradient(shape->stroke.gradient, tx,ty, sx,sy); + memcpy(t, shape->stroke.gradient->xform, sizeof(float)*6); + nsvg__xformInverse(shape->stroke.gradient->xform, t); + } + + shape->strokeWidth *= avgs; + shape->strokeDashOffset *= avgs; + for (i = 0; i < shape->strokeDashCount; i++) + shape->strokeDashArray[i] *= avgs; + } +} + +static void nsvg__createGradients(NSVGparser* p) +{ + NSVGshape* shape; + + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + if (shape->fill.type == NSVG_PAINT_UNDEF) { + if (shape->fillGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->fill.gradient = nsvg__createGradient(p, shape->fillGradient, localBounds, shape->xform, &shape->fill.type); + } + if (shape->fill.type == NSVG_PAINT_UNDEF) { + shape->fill.type = NSVG_PAINT_NONE; + } + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + if (shape->strokeGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->stroke.gradient = nsvg__createGradient(p, shape->strokeGradient, localBounds, shape->xform, &shape->stroke.type); + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + shape->stroke.type = NSVG_PAINT_NONE; + } + } + } +} + +NSVGimage* nsvgParse(char* input, const char* units, float dpi) +{ + NSVGparser* p; + NSVGimage* ret = 0; + + p = nsvg__createParser(); + if (p == NULL) { + return NULL; + } + p->dpi = dpi; + + nsvg__parseXML(input, nsvg__startElement, nsvg__endElement, nsvg__content, p); + + // Create gradients after all definitions have been parsed + nsvg__createGradients(p); + + // Scale to viewBox + nsvg__scaleToViewbox(p, units); + + ret = p->image; + p->image = NULL; + + nsvg__deleteParser(p); + + return ret; +} + +NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) +{ + FILE* fp = NULL; + size_t size; + char* data = NULL; + NSVGimage* image = NULL; + + fp = fopen(filename, "rb"); + if (!fp) goto error; + fseek(fp, 0, SEEK_END); + size = ftell(fp); + fseek(fp, 0, SEEK_SET); + data = (char*)malloc(size+1); + if (data == NULL) goto error; + if (fread(data, 1, size, fp) != size) goto error; + data[size] = '\0'; // Must be null terminated. + fclose(fp); + image = nsvgParse(data, units, dpi); + free(data); + + return image; + +error: + if (fp) fclose(fp); + if (data) free(data); + if (image) nsvgDelete(image); + return NULL; +} + +NSVGpath* nsvgDuplicatePath(NSVGpath* p) +{ + NSVGpath* res = NULL; + + if (p == NULL) + return NULL; + + res = (NSVGpath*)malloc(sizeof(NSVGpath)); + if (res == NULL) goto error; + memset(res, 0, sizeof(NSVGpath)); + + res->pts = (float*)malloc(p->npts*2*sizeof(float)); + if (res->pts == NULL) goto error; + memcpy(res->pts, p->pts, p->npts * sizeof(float) * 2); + res->npts = p->npts; + + memcpy(res->bounds, p->bounds, sizeof(p->bounds)); + + res->closed = p->closed; + + return res; + +error: + if (res != NULL) { + free(res->pts); + free(res); + } + return NULL; +} + +void nsvgDelete(NSVGimage* image) +{ + NSVGshape *snext, *shape; + if (image == NULL) return; + shape = image->shapes; + while (shape != NULL) { + snext = shape->next; + nsvg__deletePaths(shape->paths); + nsvg__deletePaint(&shape->fill); + nsvg__deletePaint(&shape->stroke); + free(shape); + shape = snext; + } + free(image); +} + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#endif // NANOSVG_IMPLEMENTATION + +#endif // NANOSVG_H diff --git a/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h b/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h new file mode 100644 index 000000000..a3295098c --- /dev/null +++ b/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h @@ -0,0 +1,1459 @@ +/* + * Copyright (c) 2013-14 Mikko Mononen memon@inside.org + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * The polygon rasterization is heavily based on stb_truetype rasterizer + * by Sean Barrett - http://nothings.org/ + * + */ + +#ifndef NANOSVGRAST_H +#define NANOSVGRAST_H + +#include "nanosvg.h" + +#ifndef NANOSVGRAST_CPLUSPLUS +#ifdef __cplusplus +extern "C" { +#endif +#endif + +typedef struct NSVGrasterizer NSVGrasterizer; + +/* Example Usage: + // Load SVG + NSVGimage* image; + image = nsvgParseFromFile("test.svg", "px", 96); + + // Create rasterizer (can be used to render multiple images). + struct NSVGrasterizer* rast = nsvgCreateRasterizer(); + // Allocate memory for image + unsigned char* img = malloc(w*h*4); + // Rasterize + nsvgRasterize(rast, image, 0,0,1, img, w, h, w*4); +*/ + +// Allocated rasterizer context. +NSVGrasterizer* nsvgCreateRasterizer(void); + +// Rasterizes SVG image, returns RGBA image (non-premultiplied alpha) +// r - pointer to rasterizer context +// image - pointer to image to rasterize +// tx,ty - image offset (applied after scaling) +// scale - image scale +// dst - pointer to destination image data, 4 bytes per pixel (RGBA) +// w - width of the image to render +// h - height of the image to render +// stride - number of bytes per scaleline in the destination buffer +void nsvgRasterize(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride); + +// Deletes rasterizer context. +void nsvgDeleteRasterizer(NSVGrasterizer*); + + +#ifndef NANOSVGRAST_CPLUSPLUS +#ifdef __cplusplus +} +#endif +#endif + +#ifdef NANOSVGRAST_IMPLEMENTATION + +#include +#include +#include + +#define NSVG__SUBSAMPLES 5 +#define NSVG__FIXSHIFT 10 +#define NSVG__FIX (1 << NSVG__FIXSHIFT) +#define NSVG__FIXMASK (NSVG__FIX-1) +#define NSVG__MEMPAGE_SIZE 1024 + +typedef struct NSVGedge { + float x0,y0, x1,y1; + int dir; + struct NSVGedge* next; +} NSVGedge; + +typedef struct NSVGpoint { + float x, y; + float dx, dy; + float len; + float dmx, dmy; + unsigned char flags; +} NSVGpoint; + +typedef struct NSVGactiveEdge { + int x,dx; + float ey; + int dir; + struct NSVGactiveEdge *next; +} NSVGactiveEdge; + +typedef struct NSVGmemPage { + unsigned char mem[NSVG__MEMPAGE_SIZE]; + int size; + struct NSVGmemPage* next; +} NSVGmemPage; + +typedef struct NSVGcachedPaint { + signed char type; + char spread; + float xform[6]; + unsigned int colors[256]; +} NSVGcachedPaint; + +struct NSVGrasterizer +{ + float px, py; + + float tessTol; + float distTol; + + NSVGedge* edges; + int nedges; + int cedges; + + NSVGpoint* points; + int npoints; + int cpoints; + + NSVGpoint* points2; + int npoints2; + int cpoints2; + + NSVGactiveEdge* freelist; + NSVGmemPage* pages; + NSVGmemPage* curpage; + + unsigned char* scanline; + int cscanline; + + unsigned char* bitmap; + int width, height, stride; +}; + +NSVGrasterizer* nsvgCreateRasterizer(void) +{ + NSVGrasterizer* r = (NSVGrasterizer*)malloc(sizeof(NSVGrasterizer)); + if (r == NULL) goto error; + memset(r, 0, sizeof(NSVGrasterizer)); + + r->tessTol = 0.25f; + r->distTol = 0.01f; + + return r; + +error: + nsvgDeleteRasterizer(r); + return NULL; +} + +void nsvgDeleteRasterizer(NSVGrasterizer* r) +{ + NSVGmemPage* p; + + if (r == NULL) return; + + p = r->pages; + while (p != NULL) { + NSVGmemPage* next = p->next; + free(p); + p = next; + } + + if (r->edges) free(r->edges); + if (r->points) free(r->points); + if (r->points2) free(r->points2); + if (r->scanline) free(r->scanline); + + free(r); +} + +static NSVGmemPage* nsvg__nextPage(NSVGrasterizer* r, NSVGmemPage* cur) +{ + NSVGmemPage *newp; + + // If using existing chain, return the next page in chain + if (cur != NULL && cur->next != NULL) { + return cur->next; + } + + // Alloc new page + newp = (NSVGmemPage*)malloc(sizeof(NSVGmemPage)); + if (newp == NULL) return NULL; + memset(newp, 0, sizeof(NSVGmemPage)); + + // Add to linked list + if (cur != NULL) + cur->next = newp; + else + r->pages = newp; + + return newp; +} + +static void nsvg__resetPool(NSVGrasterizer* r) +{ + NSVGmemPage* p = r->pages; + while (p != NULL) { + p->size = 0; + p = p->next; + } + r->curpage = r->pages; +} + +static unsigned char* nsvg__alloc(NSVGrasterizer* r, int size) +{ + unsigned char* buf; + if (size > NSVG__MEMPAGE_SIZE) return NULL; + if (r->curpage == NULL || r->curpage->size+size > NSVG__MEMPAGE_SIZE) { + r->curpage = nsvg__nextPage(r, r->curpage); + } + buf = &r->curpage->mem[r->curpage->size]; + r->curpage->size += size; + return buf; +} + +static int nsvg__ptEquals(float x1, float y1, float x2, float y2, float tol) +{ + float dx = x2 - x1; + float dy = y2 - y1; + return dx*dx + dy*dy < tol*tol; +} + +static void nsvg__addPathPoint(NSVGrasterizer* r, float x, float y, int flags) +{ + NSVGpoint* pt; + + if (r->npoints > 0) { + pt = &r->points[r->npoints-1]; + if (nsvg__ptEquals(pt->x,pt->y, x,y, r->distTol)) { + pt->flags = (unsigned char)(pt->flags | flags); + return; + } + } + + if (r->npoints+1 > r->cpoints) { + r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; + r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); + if (r->points == NULL) return; + } + + pt = &r->points[r->npoints]; + pt->x = x; + pt->y = y; + pt->flags = (unsigned char)flags; + r->npoints++; +} + +static void nsvg__appendPathPoint(NSVGrasterizer* r, NSVGpoint pt) +{ + if (r->npoints+1 > r->cpoints) { + r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; + r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); + if (r->points == NULL) return; + } + r->points[r->npoints] = pt; + r->npoints++; +} + +static void nsvg__duplicatePoints(NSVGrasterizer* r) +{ + if (r->npoints > r->cpoints2) { + r->cpoints2 = r->npoints; + r->points2 = (NSVGpoint*)realloc(r->points2, sizeof(NSVGpoint) * r->cpoints2); + if (r->points2 == NULL) return; + } + + memcpy(r->points2, r->points, sizeof(NSVGpoint) * r->npoints); + r->npoints2 = r->npoints; +} + +static void nsvg__addEdge(NSVGrasterizer* r, float x0, float y0, float x1, float y1) +{ + NSVGedge* e; + + // Skip horizontal edges + if (y0 == y1) + return; + + if (r->nedges+1 > r->cedges) { + r->cedges = r->cedges > 0 ? r->cedges * 2 : 64; + r->edges = (NSVGedge*)realloc(r->edges, sizeof(NSVGedge) * r->cedges); + if (r->edges == NULL) return; + } + + e = &r->edges[r->nedges]; + r->nedges++; + + if (y0 < y1) { + e->x0 = x0; + e->y0 = y0; + e->x1 = x1; + e->y1 = y1; + e->dir = 1; + } else { + e->x0 = x1; + e->y0 = y1; + e->x1 = x0; + e->y1 = y0; + e->dir = -1; + } +} + +static float nsvg__normalize(float *x, float* y) +{ + float d = sqrtf((*x)*(*x) + (*y)*(*y)); + if (d > 1e-6f) { + float id = 1.0f / d; + *x *= id; + *y *= id; + } + return d; +} + +static float nsvg__absf(float x) { return x < 0 ? -x : x; } +static float nsvg__roundf(float x) { return (x >= 0) ? (float)floorf(x + 0.5f) : (float)ceilf(x - 0.5f); } + +static void nsvg__flattenCubicBez(NSVGrasterizer* r, + float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4, + int level, int type) +{ + float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; + float dx,dy,d2,d3; + + if (level > 10) return; + + x12 = (x1+x2)*0.5f; + y12 = (y1+y2)*0.5f; + x23 = (x2+x3)*0.5f; + y23 = (y2+y3)*0.5f; + x34 = (x3+x4)*0.5f; + y34 = (y3+y4)*0.5f; + x123 = (x12+x23)*0.5f; + y123 = (y12+y23)*0.5f; + + dx = x4 - x1; + dy = y4 - y1; + d2 = nsvg__absf(((x2 - x4) * dy - (y2 - y4) * dx)); + d3 = nsvg__absf(((x3 - x4) * dy - (y3 - y4) * dx)); + + if ((d2 + d3)*(d2 + d3) < r->tessTol * (dx*dx + dy*dy)) { + nsvg__addPathPoint(r, x4, y4, type); + return; + } + + x234 = (x23+x34)*0.5f; + y234 = (y23+y34)*0.5f; + x1234 = (x123+x234)*0.5f; + y1234 = (y123+y234)*0.5f; + + nsvg__flattenCubicBez(r, x1,y1, x12,y12, x123,y123, x1234,y1234, level+1, 0); + nsvg__flattenCubicBez(r, x1234,y1234, x234,y234, x34,y34, x4,y4, level+1, type); +} + +static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float scale) +{ + int i, j; + NSVGpath* path; + + for (path = shape->paths; path != NULL; path = path->next) { + r->npoints = 0; + // Flatten path + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); + for (i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, 0); + } + // Close path + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); + // Build edges + for (i = 0, j = r->npoints-1; i < r->npoints; j = i++) + nsvg__addEdge(r, r->points[j].x, r->points[j].y, r->points[i].x, r->points[i].y); + } +} + +enum NSVGpointFlags +{ + NSVG_PT_CORNER = 0x01, + NSVG_PT_BEVEL = 0x02, + NSVG_PT_LEFT = 0x04 +}; + +static void nsvg__initClosed(NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + float len = nsvg__normalize(&dx, &dy); + float px = p0->x + dx*len*0.5f, py = p0->y + dy*len*0.5f; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__buttCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) +{ + float w = lineWidth * 0.5f; + float px = p->x, py = p->y; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + + nsvg__addEdge(r, lx, ly, rx, ry); + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__squareCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) +{ + float w = lineWidth * 0.5f; + float px = p->x - dx*w, py = p->y - dy*w; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + + nsvg__addEdge(r, lx, ly, rx, ry); + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +#ifndef NSVG_PI +#define NSVG_PI (3.14159265358979323846264338327f) +#endif + +static void nsvg__roundCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int ncap, int connect) +{ + int i; + float w = lineWidth * 0.5f; + float px = p->x, py = p->y; + float dlx = dy, dly = -dx; + float lx = 0, ly = 0, rx = 0, ry = 0, prevx = 0, prevy = 0; + + for (i = 0; i < ncap; i++) { + float a = (float)i/(float)(ncap-1)*NSVG_PI; + float ax = cosf(a) * w, ay = sinf(a) * w; + float x = px - dlx*ax - dx*ay; + float y = py - dly*ax - dy*ay; + + if (i > 0) + nsvg__addEdge(r, prevx, prevy, x, y); + + prevx = x; + prevy = y; + + if (i == 0) { + lx = x; ly = y; + } else if (i == ncap-1) { + rx = x; ry = y; + } + } + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__bevelJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float lx0 = p1->x - (dlx0 * w), ly0 = p1->y - (dly0 * w); + float rx0 = p1->x + (dlx0 * w), ry0 = p1->y + (dly0 * w); + float lx1 = p1->x - (dlx1 * w), ly1 = p1->y - (dly1 * w); + float rx1 = p1->x + (dlx1 * w), ry1 = p1->y + (dly1 * w); + + nsvg__addEdge(r, lx0, ly0, left->x, left->y); + nsvg__addEdge(r, lx1, ly1, lx0, ly0); + + nsvg__addEdge(r, right->x, right->y, rx0, ry0); + nsvg__addEdge(r, rx0, ry0, rx1, ry1); + + left->x = lx1; left->y = ly1; + right->x = rx1; right->y = ry1; +} + +static void nsvg__miterJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float lx0, rx0, lx1, rx1; + float ly0, ry0, ly1, ry1; + + if (p1->flags & NSVG_PT_LEFT) { + lx0 = lx1 = p1->x - p1->dmx * w; + ly0 = ly1 = p1->y - p1->dmy * w; + nsvg__addEdge(r, lx1, ly1, left->x, left->y); + + rx0 = p1->x + (dlx0 * w); + ry0 = p1->y + (dly0 * w); + rx1 = p1->x + (dlx1 * w); + ry1 = p1->y + (dly1 * w); + nsvg__addEdge(r, right->x, right->y, rx0, ry0); + nsvg__addEdge(r, rx0, ry0, rx1, ry1); + } else { + lx0 = p1->x - (dlx0 * w); + ly0 = p1->y - (dly0 * w); + lx1 = p1->x - (dlx1 * w); + ly1 = p1->y - (dly1 * w); + nsvg__addEdge(r, lx0, ly0, left->x, left->y); + nsvg__addEdge(r, lx1, ly1, lx0, ly0); + + rx0 = rx1 = p1->x + p1->dmx * w; + ry0 = ry1 = p1->y + p1->dmy * w; + nsvg__addEdge(r, right->x, right->y, rx1, ry1); + } + + left->x = lx1; left->y = ly1; + right->x = rx1; right->y = ry1; +} + +static void nsvg__roundJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth, int ncap) +{ + int i, n; + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float a0 = atan2f(dly0, dlx0); + float a1 = atan2f(dly1, dlx1); + float da = a1 - a0; + float lx, ly, rx, ry; + + if (da < NSVG_PI) da += NSVG_PI*2; + if (da > NSVG_PI) da -= NSVG_PI*2; + + n = (int)ceilf((nsvg__absf(da) / NSVG_PI) * (float)ncap); + if (n < 2) n = 2; + if (n > ncap) n = ncap; + + lx = left->x; + ly = left->y; + rx = right->x; + ry = right->y; + + for (i = 0; i < n; i++) { + float u = (float)i/(float)(n-1); + float a = a0 + u*da; + float ax = cosf(a) * w, ay = sinf(a) * w; + float lx1 = p1->x - ax, ly1 = p1->y - ay; + float rx1 = p1->x + ax, ry1 = p1->y + ay; + + nsvg__addEdge(r, lx1, ly1, lx, ly); + nsvg__addEdge(r, rx, ry, rx1, ry1); + + lx = lx1; ly = ly1; + rx = rx1; ry = ry1; + } + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__straightJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float lx = p1->x - (p1->dmx * w), ly = p1->y - (p1->dmy * w); + float rx = p1->x + (p1->dmx * w), ry = p1->y + (p1->dmy * w); + + nsvg__addEdge(r, lx, ly, left->x, left->y); + nsvg__addEdge(r, right->x, right->y, rx, ry); + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static int nsvg__curveDivs(float r, float arc, float tol) +{ + float da = acosf(r / (r + tol)) * 2.0f; + int divs = (int)ceilf(arc / da); + if (divs < 2) divs = 2; + return divs; +} + +static void nsvg__expandStroke(NSVGrasterizer* r, NSVGpoint* points, int npoints, int closed, int lineJoin, int lineCap, float lineWidth) +{ + int ncap = nsvg__curveDivs(lineWidth*0.5f, NSVG_PI, r->tessTol); // Calculate divisions per half circle. + NSVGpoint left = {0,0,0,0,0,0,0,0}, right = {0,0,0,0,0,0,0,0}, firstLeft = {0,0,0,0,0,0,0,0}, firstRight = {0,0,0,0,0,0,0,0}; + NSVGpoint* p0, *p1; + int j, s, e; + + // Build stroke edges + if (closed) { + // Looping + p0 = &points[npoints-1]; + p1 = &points[0]; + s = 0; + e = npoints; + } else { + // Add cap + p0 = &points[0]; + p1 = &points[1]; + s = 1; + e = npoints-1; + } + + if (closed) { + nsvg__initClosed(&left, &right, p0, p1, lineWidth); + firstLeft = left; + firstRight = right; + } else { + // Add cap + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + nsvg__normalize(&dx, &dy); + if (lineCap == NSVG_CAP_BUTT) + nsvg__buttCap(r, &left, &right, p0, dx, dy, lineWidth, 0); + else if (lineCap == NSVG_CAP_SQUARE) + nsvg__squareCap(r, &left, &right, p0, dx, dy, lineWidth, 0); + else if (lineCap == NSVG_CAP_ROUND) + nsvg__roundCap(r, &left, &right, p0, dx, dy, lineWidth, ncap, 0); + } + + for (j = s; j < e; ++j) { + if (p1->flags & NSVG_PT_CORNER) { + if (lineJoin == NSVG_JOIN_ROUND) + nsvg__roundJoin(r, &left, &right, p0, p1, lineWidth, ncap); + else if (lineJoin == NSVG_JOIN_BEVEL || (p1->flags & NSVG_PT_BEVEL)) + nsvg__bevelJoin(r, &left, &right, p0, p1, lineWidth); + else + nsvg__miterJoin(r, &left, &right, p0, p1, lineWidth); + } else { + nsvg__straightJoin(r, &left, &right, p1, lineWidth); + } + p0 = p1++; + } + + if (closed) { + // Loop it + nsvg__addEdge(r, firstLeft.x, firstLeft.y, left.x, left.y); + nsvg__addEdge(r, right.x, right.y, firstRight.x, firstRight.y); + } else { + // Add cap + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + nsvg__normalize(&dx, &dy); + if (lineCap == NSVG_CAP_BUTT) + nsvg__buttCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); + else if (lineCap == NSVG_CAP_SQUARE) + nsvg__squareCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); + else if (lineCap == NSVG_CAP_ROUND) + nsvg__roundCap(r, &right, &left, p1, -dx, -dy, lineWidth, ncap, 1); + } +} + +static void nsvg__prepareStroke(NSVGrasterizer* r, float miterLimit, int lineJoin) +{ + int i, j; + NSVGpoint* p0, *p1; + + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + for (i = 0; i < r->npoints; i++) { + // Calculate segment direction and length + p0->dx = p1->x - p0->x; + p0->dy = p1->y - p0->y; + p0->len = nsvg__normalize(&p0->dx, &p0->dy); + // Advance + p0 = p1++; + } + + // calculate joins + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + for (j = 0; j < r->npoints; j++) { + float dlx0, dly0, dlx1, dly1, dmr2, cross; + dlx0 = p0->dy; + dly0 = -p0->dx; + dlx1 = p1->dy; + dly1 = -p1->dx; + // Calculate extrusions + p1->dmx = (dlx0 + dlx1) * 0.5f; + p1->dmy = (dly0 + dly1) * 0.5f; + dmr2 = p1->dmx*p1->dmx + p1->dmy*p1->dmy; + if (dmr2 > 0.000001f) { + float s2 = 1.0f / dmr2; + if (s2 > 600.0f) { + s2 = 600.0f; + } + p1->dmx *= s2; + p1->dmy *= s2; + } + + // Clear flags, but keep the corner. + p1->flags = (p1->flags & NSVG_PT_CORNER) ? NSVG_PT_CORNER : 0; + + // Keep track of left turns. + cross = p1->dx * p0->dy - p0->dx * p1->dy; + if (cross > 0.0f) + p1->flags |= NSVG_PT_LEFT; + + // Check to see if the corner needs to be beveled. + if (p1->flags & NSVG_PT_CORNER) { + if ((dmr2 * miterLimit*miterLimit) < 1.0f || lineJoin == NSVG_JOIN_BEVEL || lineJoin == NSVG_JOIN_ROUND) { + p1->flags |= NSVG_PT_BEVEL; + } + } + + p0 = p1++; + } +} + +static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float scale) +{ + int i, j, closed; + NSVGpath* path; + NSVGpoint* p0, *p1; + float miterLimit = shape->miterLimit; + int lineJoin = shape->strokeLineJoin; + int lineCap = shape->strokeLineCap; + float lineWidth = shape->strokeWidth * scale; + + for (path = shape->paths; path != NULL; path = path->next) { + // Flatten path + r->npoints = 0; + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, NSVG_PT_CORNER); + for (i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, NSVG_PT_CORNER); + } + if (r->npoints < 2) + continue; + + closed = path->closed; + + // If the first and last points are the same, remove the last, mark as closed path. + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + if (nsvg__ptEquals(p0->x,p0->y, p1->x,p1->y, r->distTol)) { + r->npoints--; + p0 = &r->points[r->npoints-1]; + closed = 1; + } + + if (shape->strokeDashCount > 0) { + int idash = 0, dashState = 1; + float totalDist = 0, dashLen, allDashLen, dashOffset; + NSVGpoint cur; + + if (closed) + nsvg__appendPathPoint(r, r->points[0]); + + // Duplicate points -> points2. + nsvg__duplicatePoints(r); + + r->npoints = 0; + cur = r->points2[0]; + nsvg__appendPathPoint(r, cur); + + // Figure out dash offset. + allDashLen = 0; + for (j = 0; j < shape->strokeDashCount; j++) + allDashLen += shape->strokeDashArray[j]; + if (shape->strokeDashCount & 1) + allDashLen *= 2.0f; + // Find location inside pattern + dashOffset = fmodf(shape->strokeDashOffset, allDashLen); + if (dashOffset < 0.0f) + dashOffset += allDashLen; + + while (dashOffset > shape->strokeDashArray[idash]) { + dashOffset -= shape->strokeDashArray[idash]; + idash = (idash + 1) % shape->strokeDashCount; + } + dashLen = (shape->strokeDashArray[idash] - dashOffset) * scale; + + for (j = 1; j < r->npoints2; ) { + float dx = r->points2[j].x - cur.x; + float dy = r->points2[j].y - cur.y; + float dist = sqrtf(dx*dx + dy*dy); + + if ((totalDist + dist) > dashLen) { + // Calculate intermediate point + float d = (dashLen - totalDist) / dist; + float x = cur.x + dx * d; + float y = cur.y + dy * d; + nsvg__addPathPoint(r, x, y, NSVG_PT_CORNER); + + // Stroke + if (r->npoints > 1 && dashState) { + nsvg__prepareStroke(r, miterLimit, lineJoin); + nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); + } + // Advance dash pattern + dashState = !dashState; + idash = (idash+1) % shape->strokeDashCount; + dashLen = shape->strokeDashArray[idash] * scale; + // Restart + cur.x = x; + cur.y = y; + cur.flags = NSVG_PT_CORNER; + totalDist = 0.0f; + r->npoints = 0; + nsvg__appendPathPoint(r, cur); + } else { + totalDist += dist; + cur = r->points2[j]; + nsvg__appendPathPoint(r, cur); + j++; + } + } + // Stroke any leftover path + if (r->npoints > 1 && dashState) + nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); + } else { + nsvg__prepareStroke(r, miterLimit, lineJoin); + nsvg__expandStroke(r, r->points, r->npoints, closed, lineJoin, lineCap, lineWidth); + } + } +} + +static int nsvg__cmpEdge(const void *p, const void *q) +{ + const NSVGedge* a = (const NSVGedge*)p; + const NSVGedge* b = (const NSVGedge*)q; + + if (a->y0 < b->y0) return -1; + if (a->y0 > b->y0) return 1; + return 0; +} + + +static NSVGactiveEdge* nsvg__addActive(NSVGrasterizer* r, NSVGedge* e, float startPoint) +{ + NSVGactiveEdge* z; + + if (r->freelist != NULL) { + // Restore from freelist. + z = r->freelist; + r->freelist = z->next; + } else { + // Alloc new edge. + z = (NSVGactiveEdge*)nsvg__alloc(r, sizeof(NSVGactiveEdge)); + if (z == NULL) return NULL; + } + + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); +// STBTT_assert(e->y0 <= start_point); + // round dx down to avoid going too far + if (dxdy < 0) + z->dx = (int)(-nsvg__roundf(NSVG__FIX * -dxdy)); + else + z->dx = (int)nsvg__roundf(NSVG__FIX * dxdy); + z->x = (int)nsvg__roundf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); +// z->x -= off_x * FIX; + z->ey = e->y1; + z->next = 0; + z->dir = e->dir; + + return z; +} + +static void nsvg__freeActive(NSVGrasterizer* r, NSVGactiveEdge* z) +{ + z->next = r->freelist; + r->freelist = z; +} + +static void nsvg__fillScanline(unsigned char* scanline, int len, int x0, int x1, int maxWeight, int* xmin, int* xmax) +{ + int i = x0 >> NSVG__FIXSHIFT; + int j = x1 >> NSVG__FIXSHIFT; + if (i < *xmin) *xmin = i; + if (j > *xmax) *xmax = j; + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = (unsigned char)(scanline[i] + ((x1 - x0) * maxWeight >> NSVG__FIXSHIFT)); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = (unsigned char)(scanline[i] + (((NSVG__FIX - (x0 & NSVG__FIXMASK)) * maxWeight) >> NSVG__FIXSHIFT)); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = (unsigned char)(scanline[j] + (((x1 & NSVG__FIXMASK) * maxWeight) >> NSVG__FIXSHIFT)); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = (unsigned char)(scanline[i] + maxWeight); + } + } +} + +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void nsvg__fillActiveEdges(unsigned char* scanline, int len, NSVGactiveEdge* e, int maxWeight, int* xmin, int* xmax, char fillRule) +{ + // non-zero winding fill + int x0 = 0, w = 0; + + if (fillRule == NSVG_FILLRULE_NONZERO) { + // Non-zero + while (e != NULL) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->dir; + } else { + int x1 = e->x; w += e->dir; + // if we went to zero, we need to draw + if (w == 0) + nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); + } + e = e->next; + } + } else if (fillRule == NSVG_FILLRULE_EVENODD) { + // Even-odd + while (e != NULL) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w = 1; + } else { + int x1 = e->x; w = 0; + nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); + } + e = e->next; + } + } +} + +static float nsvg__clampf(float a, float mn, float mx) { return a < mn ? mn : (a > mx ? mx : a); } + +static unsigned int nsvg__RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) +{ + return ((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16) | ((unsigned int)a << 24); +} + +static unsigned int nsvg__lerpRGBA(unsigned int c0, unsigned int c1, float u) +{ + int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); + int r = (((c0) & 0xff)*(256-iu) + (((c1) & 0xff)*iu)) >> 8; + int g = (((c0>>8) & 0xff)*(256-iu) + (((c1>>8) & 0xff)*iu)) >> 8; + int b = (((c0>>16) & 0xff)*(256-iu) + (((c1>>16) & 0xff)*iu)) >> 8; + int a = (((c0>>24) & 0xff)*(256-iu) + (((c1>>24) & 0xff)*iu)) >> 8; + return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); +} + +static unsigned int nsvg__applyOpacity(unsigned int c, float u) +{ + int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); + int r = (c) & 0xff; + int g = (c>>8) & 0xff; + int b = (c>>16) & 0xff; + int a = (((c>>24) & 0xff)*iu) >> 8; + return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); +} + +static inline int nsvg__div255(int x) +{ + return ((x+1) * 257) >> 16; +} + +static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* cover, int x, int y, + float tx, float ty, float scale, NSVGcachedPaint* cache) +{ + + if (cache->type == NSVG_PAINT_COLOR) { + int i, cr, cg, cb, ca; + cr = cache->colors[0] & 0xff; + cg = (cache->colors[0] >> 8) & 0xff; + cb = (cache->colors[0] >> 16) & 0xff; + ca = (cache->colors[0] >> 24) & 0xff; + + for (i = 0; i < count; i++) { + int r,g,b; + int a = nsvg__div255((int)cover[0] * ca); + int ia = 255 - a; + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + } + } else if (cache->type == NSVG_PAINT_LINEAR_GRADIENT) { + // TODO: spread modes. + // TODO: plenty of opportunities to optimize. + float fx, fy, dx, gy; + float* t = cache->xform; + int i, cr, cg, cb, ca; + unsigned int c; + + fx = ((float)x - tx) / scale; + fy = ((float)y - ty) / scale; + dx = 1.0f / scale; + + for (i = 0; i < count; i++) { + int r,g,b,a,ia; + gy = fx*t[1] + fy*t[3] + t[5]; + c = cache->colors[(int)nsvg__clampf(gy*255.0f, 0, 255.0f)]; + cr = (c) & 0xff; + cg = (c >> 8) & 0xff; + cb = (c >> 16) & 0xff; + ca = (c >> 24) & 0xff; + + a = nsvg__div255((int)cover[0] * ca); + ia = 255 - a; + + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + fx += dx; + } + } else if (cache->type == NSVG_PAINT_RADIAL_GRADIENT) { + // TODO: spread modes. + // TODO: plenty of opportunities to optimize. + // TODO: focus (fx,fy) + float fx, fy, dx, gx, gy, gd; + float* t = cache->xform; + int i, cr, cg, cb, ca; + unsigned int c; + + fx = ((float)x - tx) / scale; + fy = ((float)y - ty) / scale; + dx = 1.0f / scale; + + for (i = 0; i < count; i++) { + int r,g,b,a,ia; + gx = fx*t[0] + fy*t[2] + t[4]; + gy = fx*t[1] + fy*t[3] + t[5]; + gd = sqrtf(gx*gx + gy*gy); + c = cache->colors[(int)nsvg__clampf(gd*255.0f, 0, 255.0f)]; + cr = (c) & 0xff; + cg = (c >> 8) & 0xff; + cb = (c >> 16) & 0xff; + ca = (c >> 24) & 0xff; + + a = nsvg__div255((int)cover[0] * ca); + ia = 255 - a; + + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + fx += dx; + } + } +} + +static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, float scale, NSVGcachedPaint* cache, char fillRule) +{ + NSVGactiveEdge *active = NULL; + int y, s; + int e = 0; + int maxWeight = (255 / NSVG__SUBSAMPLES); // weight per vertical scanline + int xmin, xmax; + + for (y = 0; y < r->height; y++) { + memset(r->scanline, 0, r->width); + xmin = r->width; + xmax = 0; + for (s = 0; s < NSVG__SUBSAMPLES; ++s) { + // find center of pixel for this scanline + float scany = (float)(y*NSVG__SUBSAMPLES + s) + 0.5f; + NSVGactiveEdge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + NSVGactiveEdge *z = *step; + if (z->ey <= scany) { + *step = z->next; // delete from list +// NSVG__assert(z->valid); + nsvg__freeActive(r, z); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for (;;) { + int changed = 0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + NSVGactiveEdge* t = *step; + NSVGactiveEdge* q = t->next; + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e < r->nedges && r->edges[e].y0 <= scany) { + if (r->edges[e].y1 > scany) { + NSVGactiveEdge* z = nsvg__addActive(r, &r->edges[e], scany); + if (z == NULL) break; + // find insertion point + if (active == NULL) { + active = z; + } else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + NSVGactiveEdge* p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + e++; + } + + // now process all active edges in non-zero fashion + if (active != NULL) + nsvg__fillActiveEdges(r->scanline, r->width, active, maxWeight, &xmin, &xmax, fillRule); + } + // Blit + if (xmin < 0) xmin = 0; + if (xmax > r->width-1) xmax = r->width-1; + if (xmin <= xmax) { + nsvg__scanlineSolid(&r->bitmap[y * r->stride] + xmin*4, xmax-xmin+1, &r->scanline[xmin], xmin, y, tx,ty, scale, cache); + } + } + +} + +static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride) +{ + int x,y; + + // Unpremultiply + for (y = 0; y < h; y++) { + unsigned char *row = &image[y*stride]; + for (x = 0; x < w; x++) { + int r = row[0], g = row[1], b = row[2], a = row[3]; + if (a != 0) { + row[0] = (unsigned char)(r*255/a); + row[1] = (unsigned char)(g*255/a); + row[2] = (unsigned char)(b*255/a); + } + row += 4; + } + } + + // Defringe + for (y = 0; y < h; y++) { + unsigned char *row = &image[y*stride]; + for (x = 0; x < w; x++) { + int r = 0, g = 0, b = 0, a = row[3], n = 0; + if (a == 0) { + if (x-1 > 0 && row[-1] != 0) { + r += row[-4]; + g += row[-3]; + b += row[-2]; + n++; + } + if (x+1 < w && row[7] != 0) { + r += row[4]; + g += row[5]; + b += row[6]; + n++; + } + if (y-1 > 0 && row[-stride+3] != 0) { + r += row[-stride]; + g += row[-stride+1]; + b += row[-stride+2]; + n++; + } + if (y+1 < h && row[stride+3] != 0) { + r += row[stride]; + g += row[stride+1]; + b += row[stride+2]; + n++; + } + if (n > 0) { + row[0] = (unsigned char)(r/n); + row[1] = (unsigned char)(g/n); + row[2] = (unsigned char)(b/n); + } + } + row += 4; + } + } +} + + +static void nsvg__initPaint(NSVGcachedPaint* cache, NSVGpaint* paint, float opacity) +{ + int i, j; + NSVGgradient* grad; + + cache->type = paint->type; + + if (paint->type == NSVG_PAINT_COLOR) { + cache->colors[0] = nsvg__applyOpacity(paint->color, opacity); + return; + } + + grad = paint->gradient; + + cache->spread = grad->spread; + memcpy(cache->xform, grad->xform, sizeof(float)*6); + + if (grad->nstops == 0) { + for (i = 0; i < 256; i++) + cache->colors[i] = 0; + } else if (grad->nstops == 1) { + for (i = 0; i < 256; i++) + cache->colors[i] = nsvg__applyOpacity(grad->stops[i].color, opacity); + } else { + unsigned int ca, cb = 0; + float ua, ub, du, u; + int ia, ib, count; + + ca = nsvg__applyOpacity(grad->stops[0].color, opacity); + ua = nsvg__clampf(grad->stops[0].offset, 0, 1); + ub = nsvg__clampf(grad->stops[grad->nstops-1].offset, ua, 1); + ia = (int)(ua * 255.0f); + ib = (int)(ub * 255.0f); + for (i = 0; i < ia; i++) { + cache->colors[i] = ca; + } + + for (i = 0; i < grad->nstops-1; i++) { + ca = nsvg__applyOpacity(grad->stops[i].color, opacity); + cb = nsvg__applyOpacity(grad->stops[i+1].color, opacity); + ua = nsvg__clampf(grad->stops[i].offset, 0, 1); + ub = nsvg__clampf(grad->stops[i+1].offset, 0, 1); + ia = (int)(ua * 255.0f); + ib = (int)(ub * 255.0f); + count = ib - ia; + if (count <= 0) continue; + u = 0; + du = 1.0f / (float)count; + for (j = 0; j < count; j++) { + cache->colors[ia+j] = nsvg__lerpRGBA(ca,cb,u); + u += du; + } + } + + for (i = ib; i < 256; i++) + cache->colors[i] = cb; + } + +} + +/* +static void dumpEdges(NSVGrasterizer* r, const char* name) +{ + float xmin = 0, xmax = 0, ymin = 0, ymax = 0; + NSVGedge *e = NULL; + int i; + if (r->nedges == 0) return; + FILE* fp = fopen(name, "w"); + if (fp == NULL) return; + + xmin = xmax = r->edges[0].x0; + ymin = ymax = r->edges[0].y0; + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + xmin = nsvg__minf(xmin, e->x0); + xmin = nsvg__minf(xmin, e->x1); + xmax = nsvg__maxf(xmax, e->x0); + xmax = nsvg__maxf(xmax, e->x1); + ymin = nsvg__minf(ymin, e->y0); + ymin = nsvg__minf(ymin, e->y1); + ymax = nsvg__maxf(ymax, e->y0); + ymax = nsvg__maxf(ymax, e->y1); + } + + fprintf(fp, "", xmin, ymin, (xmax - xmin), (ymax - ymin)); + + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + fprintf(fp ,"", e->x0,e->y0, e->x1,e->y1); + } + + for (i = 0; i < r->npoints; i++) { + if (i+1 < r->npoints) + fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i+1].x, r->points[i+1].y); + fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i].flags == 0 ? "#f00" : "#0f0"); + } + + fprintf(fp, ""); + fclose(fp); +} +*/ + +void nsvgRasterize(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride) +{ + NSVGshape *shape = NULL; + NSVGedge *e = NULL; + NSVGcachedPaint cache; + int i; + + r->bitmap = dst; + r->width = w; + r->height = h; + r->stride = stride; + + if (w > r->cscanline) { + r->cscanline = w; + r->scanline = (unsigned char*)realloc(r->scanline, w); + if (r->scanline == NULL) return; + } + + for (i = 0; i < h; i++) + memset(&dst[i*stride], 0, w*4); + + for (shape = image->shapes; shape != NULL; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) + continue; + + if (shape->fill.type != NSVG_PAINT_NONE) { + nsvg__resetPool(r); + r->freelist = NULL; + r->nedges = 0; + + nsvg__flattenShape(r, shape, scale); + + // Scale and translate edges + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + e->x0 = tx + e->x0; + e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; + e->x1 = tx + e->x1; + e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; + } + + // Rasterize edges + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + + // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule + nsvg__initPaint(&cache, &shape->fill, shape->opacity); + + nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, shape->fillRule); + } + if (shape->stroke.type != NSVG_PAINT_NONE && (shape->strokeWidth * scale) > 0.01f) { + nsvg__resetPool(r); + r->freelist = NULL; + r->nedges = 0; + + nsvg__flattenShapeStroke(r, shape, scale); + +// dumpEdges(r, "edge.svg"); + + // Scale and translate edges + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + e->x0 = tx + e->x0; + e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; + e->x1 = tx + e->x1; + e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; + } + + // Rasterize edges + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + + // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule + nsvg__initPaint(&cache, &shape->stroke, shape->opacity); + + nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, NSVG_FILLRULE_NONZERO); + } + } + + nsvg__unpremultiplyAlpha(dst, w, h, stride); + + r->bitmap = NULL; + r->width = 0; + r->height = 0; + r->stride = 0; +} + +#endif // NANOSVGRAST_IMPLEMENTATION + +#endif // NANOSVGRAST_H diff --git a/libraries/ZWidget/src/core/pathfill.cpp b/libraries/ZWidget/src/core/pathfill.cpp new file mode 100644 index 000000000..cb604ef3f --- /dev/null +++ b/libraries/ZWidget/src/core/pathfill.cpp @@ -0,0 +1,613 @@ + +#include "core/pathfill.h" +#include +#include + +static const int AntialiasLevel = 8; +static const int MaskBlockSize = 16; +static const int MaskBufferSize = MaskBlockSize * MaskBlockSize; +static const int ScanlineBlockSize = MaskBlockSize * AntialiasLevel; + +class PathScanlineEdge +{ +public: + PathScanlineEdge() = default; + PathScanlineEdge(double x, bool up_direction) : x(x), up_direction(up_direction) { } + + double x = 0.0; + bool up_direction = false; +}; + +class PathScanline +{ +public: + std::vector edges; + std::vector pixels; + + void insert_sorted(PathScanlineEdge edge) + { + edges.push_back(edge); + + for (size_t pos = edges.size() - 1; pos > 0 && edges[pos - 1].x >= edge.x; pos--) + { + PathScanlineEdge temp = edges[pos - 1]; + edges[pos - 1] = edges[pos]; + edges[pos] = temp; + } + } +}; + +class PathRasterRange +{ +public: + void Begin(const PathScanline* scanline, PathFillMode mode); + void Next(); + + bool found = false; + int x0; + int x1; + +private: + const PathScanline* scanline = nullptr; + PathFillMode mode; + size_t i = 0; + int nonzero_rule = 0; +}; + +enum class PathFillBlockResult +{ + Empty, + Partial, + Full +}; + +class PathMaskBuffer +{ +public: + void BeginRow(PathScanline* scanlines, PathFillMode mode); + PathFillBlockResult FillBlock(int xpos); + + unsigned char MaskBufferData[MaskBufferSize]; + +private: + bool IsFullBlock(int xpos) const; + + PathRasterRange Range[ScanlineBlockSize]; +}; + +class PathFillRasterizer +{ +public: + void Rasterize(const PathFillDesc& path, uint8_t* dest, int width, int height); + +private: + void Clear(); + + void Begin(double x, double y); + void QuadraticBezier(double cp1_x, double cp1_y, double cp2_x, double cp2_y); + void CubicBezier(double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y); + void Line(double x, double y); + void End(bool close); + + void SubdivideBezier(int level, double cp0_x, double cp0_y, double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y, double t0, double t1); + static Point PointOnBezier(double cp0_x, double cp0_y, double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y, double t); + + void Fill(PathFillMode mode, uint8_t* dest, int dest_width, int dest_height); + + struct Extent + { + Extent() : left(INT_MAX), right(0) {} + int left; + int right; + }; + + Extent FindExtent(const PathScanline* scanline, int max_width); + + double start_x = 0.0; + double start_y = 0.0; + double last_x = 0.0; + double last_y = 0.0; + + int first_scanline = 0; + int last_scanline = 0; + + int width = 0; + int height = 0; + std::vector scanlines; + + PathMaskBuffer mask_blocks; +}; + +///////////////////////////////////////////////////////////////////////////// + +void PathFillDesc::Rasterize(uint8_t* dest, int width, int height, bool blend) +{ + if (!blend) + { + memset(dest, 0, width * height); + } + PathFillRasterizer rasterizer; + rasterizer.Rasterize(*this, dest, width, height); +} + +///////////////////////////////////////////////////////////////////////////// + +void PathFillRasterizer::Rasterize(const PathFillDesc& path, uint8_t* dest, int dest_width, int dest_height) +{ + Clear(); + + // For simplicity of the code, ensure the mask is always a multiple of MaskBlockSize + int block_width = ScanlineBlockSize * ((dest_width + MaskBlockSize - 1) / MaskBlockSize); + int block_height = ScanlineBlockSize * ((dest_height + MaskBlockSize - 1) / MaskBlockSize); + + if (width != block_width || height != block_height) + { + width = block_width; + height = block_height; + + scanlines.resize(block_height); + first_scanline = (int)scanlines.size(); + last_scanline = 0; + } + + for (const auto& subpath : path.subpaths) + { + Point start_point = subpath.points[0]; + Begin(start_point.x, start_point.y); + + size_t i = 1; + for (PathFillCommand command : subpath.commands) + { + if (command == PathFillCommand::line) + { + const Point& next_point = subpath.points[i]; + i++; + + Line(next_point.x, next_point.y); + } + else if (command == PathFillCommand::quadradic) + { + const Point& control = subpath.points[i]; + const Point& next_point = subpath.points[i + 1]; + i += 2; + + QuadraticBezier(control.x, control.y, next_point.x, next_point.y); + } + else if (command == PathFillCommand::cubic) + { + const Point& control1 = subpath.points[i]; + const Point& control2 = subpath.points[i + 1]; + const Point& next_point = subpath.points[i + 2]; + i += 3; + + CubicBezier(control1.x, control1.y, control2.x, control2.y, next_point.x, next_point.y); + } + } + + End(subpath.closed); + } + + Fill(path.fill_mode, dest, dest_width, dest_height); +} + +void PathFillRasterizer::Fill(PathFillMode mode, uint8_t* dest, int dest_width, int dest_height) +{ + if (scanlines.empty()) return; + + int start_y = first_scanline / ScanlineBlockSize * ScanlineBlockSize; + int end_y = (last_scanline + ScanlineBlockSize - 1) / ScanlineBlockSize * ScanlineBlockSize; + + for (int ypos = start_y; ypos < end_y; ypos += ScanlineBlockSize) + { + mask_blocks.BeginRow(&scanlines[ypos], mode); + + Extent extent = FindExtent(&scanlines[ypos], width); + for (int xpos = extent.left; xpos < extent.right; xpos += ScanlineBlockSize) + { + PathFillBlockResult result = mask_blocks.FillBlock(xpos); + + int dest_x = xpos / AntialiasLevel; + int dest_y = ypos / AntialiasLevel; + int count_x = std::min(dest_x + MaskBlockSize, dest_width) - dest_x; + int count_y = std::min(dest_y + MaskBlockSize, dest_height) - dest_y; + + if (result == PathFillBlockResult::Full) + { + for (int i = 0; i < count_y; i++) + { + uint8_t* dline = dest + dest_x + (dest_y + i) * dest_width; + memset(dline, 255, count_x); + } + } + else if (result == PathFillBlockResult::Partial) + { + for (int i = 0; i < count_y; i++) + { + const uint8_t* sline = mask_blocks.MaskBufferData + i * MaskBlockSize; + uint8_t* dline = dest + dest_x + (dest_y + i) * dest_width; + for (int j = 0; j < count_x; j++) + { + dline[j] = std::min((int)dline[j] + (int)sline[j], 255); + } + } + } + } + } +} + +PathFillRasterizer::Extent PathFillRasterizer::FindExtent(const PathScanline* scanline, int max_width) +{ + // Find scanline extents + Extent extent; + for (unsigned int cnt = 0; cnt < ScanlineBlockSize; cnt++, scanline++) + { + if (scanline->edges.empty()) + continue; + + extent.left = std::min(extent.left, (int)scanline->edges.front().x); + extent.right = std::max(extent.right, (int)scanline->edges.back().x); + } + extent.left = std::max(extent.left, 0); + extent.right = std::min(extent.right, max_width); + + return extent; +} + +void PathFillRasterizer::Clear() +{ + for (size_t y = first_scanline; y < last_scanline; y++) + { + auto& scanline = scanlines[y]; + if (!scanline.edges.empty()) + { + scanline.edges.clear(); + } + } + + first_scanline = (int)scanlines.size(); + last_scanline = 0; +} + +void PathFillRasterizer::Begin(double x, double y) +{ + start_x = last_x = x; + start_y = last_y = y; +} + +void PathFillRasterizer::End(bool close) +{ + if (close) + { + Line(start_x, start_y); + } +} + +void PathFillRasterizer::QuadraticBezier(double qcp1_x, double qcp1_y, double qcp2_x, double qcp2_y) +{ + double qcp0_x = last_x; + double qcp0_y = last_y; + + // Convert to cubic: + double cp1_x = qcp0_x + 2.0 * (qcp1_x - qcp0_x) / 3.0; + double cp1_y = qcp0_y + 2.0 * (qcp1_y - qcp0_y) / 3.0; + double cp2_x = qcp1_x + (qcp2_x - qcp1_x) / 3.0; + double cp2_y = qcp1_y + (qcp2_y - qcp1_y) / 3.0; + double cp3_x = qcp2_x; + double cp3_y = qcp2_y; + CubicBezier(cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y); +} + +void PathFillRasterizer::CubicBezier(double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y) +{ + double cp0_x = last_x; + double cp0_y = last_y; + + double estimated_length = + std::sqrt((cp1_x - cp0_x) * (cp1_x - cp0_x) + (cp1_y - cp0_y) * (cp1_y - cp0_y)) + + std::sqrt((cp1_x - cp0_x) * (cp1_x - cp0_x) + (cp1_y - cp0_y) * (cp1_y - cp0_y)) + + std::sqrt((cp1_x - cp0_x) * (cp1_x - cp0_x) + (cp1_y - cp0_y) * (cp1_y - cp0_y)); + + double min_segs = 10.0; + double segs = estimated_length / 5.0; + int steps = (int)std::ceil(std::sqrt(segs * segs * 0.3f + min_segs)); + for (int i = 0; i < steps; i++) + { + //Point sp = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, i / (double)steps); + Point ep = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, (i + 1) / (double)steps); + Line(ep.x, ep.y); + } + + // http://ciechanowski.me/blog/2014/02/18/drawing-bezier-curves/ + // http://antigrain.com/research/adaptive_bezier/ (best method, unfortunately GPL example code) +} + +void PathFillRasterizer::SubdivideBezier(int level, double cp0_x, double cp0_y, double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y, double t0, double t1) +{ + const double split_angle_cos = 0.99f; + + double tc = (t0 + t1) * 0.5f; + + Point sp = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, t0); + Point cp = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, tc); + Point ep = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, t1); + + Point sp2cp(cp.x - sp.x, cp.y - sp.y); + Point cp2ep(ep.x - cp.x, ep.y - cp.y); + + // Normalize + double len_sp2cp = std::sqrt(sp2cp.x * sp2cp.x + sp2cp.y * sp2cp.y); + double len_cp2ep = std::sqrt(cp2ep.x * cp2ep.x + cp2ep.y * cp2ep.y); + if (len_sp2cp > 0.0) { sp2cp.x /= len_sp2cp; sp2cp.y /= len_sp2cp; } + if (len_cp2ep > 0.0) { cp2ep.x /= len_cp2ep; cp2ep.y /= len_cp2ep; } + + double dot = sp2cp.x * cp2ep.x + sp2cp.y * cp2ep.y; + if (dot < split_angle_cos && level < 15) + { + SubdivideBezier(level + 1, cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, t0, tc); + SubdivideBezier(level + 1, cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, tc, t1); + } + else + { + Line(ep.x, ep.y); + } +} + +Point PathFillRasterizer::PointOnBezier(double cp0_x, double cp0_y, double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y, double t) +{ + const int num_cp = 4; + + double cp_x[4] = { cp0_x, cp1_x, cp2_x, cp3_x }; + double cp_y[4] = { cp0_y, cp1_y, cp2_y, cp3_y }; + + // Perform deCasteljau iterations: + // (linear interpolate between the control points) + double a = 1.0 - t; + double b = t; + for (int j = num_cp - 1; j > 0; j--) + { + for (int i = 0; i < j; i++) + { + cp_x[i] = a * cp_x[i] + b * cp_x[i + 1]; + cp_y[i] = a * cp_y[i] + b * cp_y[i + 1]; + } + } + + return Point(cp_x[0], cp_y[0]); +} + +void PathFillRasterizer::Line(double x1, double y1) +{ + double x0 = last_x; + double y0 = last_y; + + last_x = x1; + last_y = y1; + + x0 *= static_cast(AntialiasLevel); + x1 *= static_cast(AntialiasLevel); + y0 *= static_cast(AntialiasLevel); + y1 *= static_cast(AntialiasLevel); + + bool up_direction = y1 < y0; + double dy = y1 - y0; + + constexpr const double epsilon = std::numeric_limits::epsilon(); + if (dy < -epsilon || dy > epsilon) + { + int start_y = static_cast(std::floor(std::min(y0, y1) + 0.5f)); + int end_y = static_cast(std::floor(std::max(y0, y1) - 0.5f)) + 1; + + start_y = std::max(start_y, 0); + end_y = std::min(end_y, height); + + double rcp_dy = 1.0 / dy; + + first_scanline = std::min(first_scanline, start_y); + last_scanline = std::max(last_scanline, end_y); + + for (int y = start_y; y < end_y; y++) + { + double ypos = y + 0.5f; + double x = x0 + (x1 - x0) * (ypos - y0) * rcp_dy; + scanlines[y].insert_sorted(PathScanlineEdge(x, up_direction)); + } + } +} + +///////////////////////////////////////////////////////////////////////////// + +void PathRasterRange::Begin(const PathScanline* new_scanline, PathFillMode new_mode) +{ + scanline = new_scanline; + mode = new_mode; + found = false; + i = 0; + nonzero_rule = 0; + Next(); +} + +void PathRasterRange::Next() +{ + if (i + 1 >= scanline->edges.size()) + { + found = false; + return; + } + + if (mode == PathFillMode::alternate) + { + x0 = static_cast(scanline->edges[i].x + 0.5f); + x1 = static_cast(scanline->edges[i + 1].x - 0.5f) + 1; + i += 2; + found = true; + } + else + { + x0 = static_cast(scanline->edges[i].x + 0.5f); + nonzero_rule += scanline->edges[i].up_direction ? 1 : -1; + i++; + + while (i < scanline->edges.size()) + { + nonzero_rule += scanline->edges[i].up_direction ? 1 : -1; + x1 = static_cast(scanline->edges[i].x - 0.5f) + 1; + i++; + + if (nonzero_rule == 0) + { + found = true; + return; + } + } + found = false; + } +} + +///////////////////////////////////////////////////////////////////////// + +void PathMaskBuffer::BeginRow(PathScanline* scanlines, PathFillMode mode) +{ + for (unsigned int cnt = 0; cnt < ScanlineBlockSize; cnt++) + { + Range[cnt].Begin(&scanlines[cnt], mode); + } +} + +#if 0 +PathFillBlockResult PathMaskBuffer::FillBlock(int xpos) +{ + if (IsFullBlock(xpos)) + { + return PathFillBlockResult::Full; + } + + const int block_size = MaskBlockSize / 16 * MaskBlockSize; + __m128i block[block_size]; + + for (auto& elem : block) + elem = _mm_setzero_si128(); + + for (unsigned int cnt = 0; cnt < ScanlineBlockSize; cnt++) + { + __m128i* line = &block[MaskBlockSize / 16 * (cnt / AntialiasLevel)]; + + while (range[cnt].found) + { + int x0 = range[cnt].x0; + if (x0 >= xpos + ScanlineBlockSize) + break; + int x1 = range[cnt].x1; + + x0 = max(x0, xpos); + x1 = min(x1, xpos + ScanlineBlockSize); + + if (x0 >= x1) // Done segment + { + range[cnt].next(); + } + else + { + for (int sse_block = 0; sse_block < MaskBlockSize / 16; sse_block++) + { + for (int alias_cnt = 0; alias_cnt < (AntialiasLevel); alias_cnt++) + { + __m128i start = _mm_set1_epi8((x0 + alias_cnt - xpos) / AntialiasLevel - 16 * sse_block); + __m128i end = _mm_set1_epi8((x1 + alias_cnt - xpos) / AntialiasLevel - 16 * sse_block); + __m128i x = _mm_set_epi8(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0); + + __m128i left = _mm_cmplt_epi8(x, start); + __m128i right = _mm_cmplt_epi8(x, end); + __m128i mask = _mm_andnot_si128(left, right); + __m128i add_value = _mm_and_si128(mask, _mm_set1_epi8(256 / (AntialiasLevel * AntialiasLevel))); + + line[sse_block] = _mm_adds_epu8(line[sse_block], add_value); + } + } + + range[cnt].x0 = x1; // For next time + } + } + } + + __m128i empty_status = _mm_setzero_si128(); + for (auto& elem : block) + empty_status = _mm_or_si128(empty_status, elem); + + bool empty_block = _mm_movemask_epi8(_mm_cmpeq_epi32(empty_status, _mm_setzero_si128())) == 0xffff; + if (empty_block) return PathFillBlockResult::Empty; + + for (unsigned int cnt = 0; cnt < MaskBlockSize; cnt++) + { + __m128i* input = &block[MaskBlockSize / 16 * cnt]; + __m128i* output = (__m128i*)(MaskBufferData + cnt * mask_texture_size); + + for (int sse_block = 0; sse_block < MaskBlockSize / 16; sse_block++) + _mm_storeu_si128(&output[sse_block], input[sse_block]); + } + + return PathFillBlockResult::Partial; +} + +#else + +PathFillBlockResult PathMaskBuffer::FillBlock(int xpos) +{ + if (IsFullBlock(xpos)) + { + return PathFillBlockResult::Full; + } + + memset(MaskBufferData, 0, MaskBufferSize); + + bool empty_block = true; + for (unsigned int cnt = 0; cnt < ScanlineBlockSize; cnt++) + { + unsigned char* line = MaskBufferData + MaskBlockSize * (cnt / AntialiasLevel); + while (Range[cnt].found) + { + int x0 = Range[cnt].x0; + if (x0 >= xpos + ScanlineBlockSize) + break; + int x1 = Range[cnt].x1; + + x0 = std::max(x0, xpos); + x1 = std::min(x1, xpos + ScanlineBlockSize); + + if (x0 >= x1) // Done segment + { + Range[cnt].Next(); + } + else + { + empty_block = false; + for (int x = x0 - xpos; x < x1 - xpos; x++) + { + int pixel = line[x / AntialiasLevel]; + pixel = std::min(pixel + (256 / (AntialiasLevel * AntialiasLevel)), 255); + line[x / AntialiasLevel] = pixel; + } + Range[cnt].x0 = x1; // For next time + } + } + } + + return empty_block ? PathFillBlockResult::Empty : PathFillBlockResult::Partial; +} + +#endif + +bool PathMaskBuffer::IsFullBlock(int xpos) const +{ + for (auto& elem : Range) + { + if (!elem.found) + { + return false; + } + if ((elem.x0 > xpos) || (elem.x1 < (xpos + ScanlineBlockSize - 1))) + { + return false; + } + } + return true; +} diff --git a/libraries/ZWidget/src/core/picopng/LICENSE.txt b/libraries/ZWidget/src/core/picopng/LICENSE.txt new file mode 100644 index 000000000..a1f522f51 --- /dev/null +++ b/libraries/ZWidget/src/core/picopng/LICENSE.txt @@ -0,0 +1,18 @@ + // picoPNG version 20101224 + // Copyright (c) 2005-2010 Lode Vandevenne + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. diff --git a/libraries/ZWidget/src/core/picopng/picopng.cpp b/libraries/ZWidget/src/core/picopng/picopng.cpp new file mode 100644 index 000000000..97eb3b500 --- /dev/null +++ b/libraries/ZWidget/src/core/picopng/picopng.cpp @@ -0,0 +1,595 @@ +#include +#include +#include + +/* +decodePNG: The picoPNG function, decodes a PNG file buffer in memory, into a raw pixel buffer. +out_image: output parameter, this will contain the raw pixels after decoding. + By default the output is 32-bit RGBA color. + The std::vector is automatically resized to the correct size. +image_width: output_parameter, this will contain the width of the image in pixels. +image_height: output_parameter, this will contain the height of the image in pixels. +in_png: pointer to the buffer of the PNG file in memory. To get it from a file on + disk, load it and store it in a memory buffer yourself first. +in_size: size of the input PNG file in bytes. +convert_to_rgba32: optional parameter, true by default. + Set to true to get the output in RGBA 32-bit (8 bit per channel) color format + no matter what color type the original PNG image had. This gives predictable, + useable data from any random input PNG. + Set to false to do no color conversion at all. The result then has the same data + type as the PNG image, which can range from 1 bit to 64 bits per pixel. + Information about the color type or palette colors are not provided. You need + to know this information yourself to be able to use the data so this only + works for trusted PNG files. Use LodePNG instead of picoPNG if you need this information. +return: 0 if success, not 0 if some error occured. +*/ +int decodePNG(std::vector& out_image, unsigned long& image_width, unsigned long& image_height, const unsigned char* in_png, size_t in_size, bool convert_to_rgba32 = true) +{ + // picoPNG version 20101224 + // Copyright (c) 2005-2010 Lode Vandevenne + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + // picoPNG is a PNG decoder in one C++ function of around 500 lines. Use picoPNG for + // programs that need only 1 .cpp file. Since it's a single function, it's very limited, + // it can convert a PNG to raw pixel data either converted to 32-bit RGBA color or + // with no color conversion at all. For anything more complex, another tiny library + // is available: LodePNG (lodepng.c(pp)), which is a single source and header file. + // Apologies for the compact code style, it's to make this tiny. + + static const unsigned long LENBASE[29] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258}; + static const unsigned long LENEXTRA[29] = {0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; + static const unsigned long DISTBASE[30] = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577}; + static const unsigned long DISTEXTRA[30] = {0,0,0,0,1,1,2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; + static const unsigned long CLCL[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; //code length code lengths + struct Zlib //nested functions for zlib decompression + { + static unsigned long readBitFromStream(size_t& bitp, const unsigned char* bits) { unsigned long result = (bits[bitp >> 3] >> (bitp & 0x7)) & 1; bitp++; return result;} + static unsigned long readBitsFromStream(size_t& bitp, const unsigned char* bits, size_t nbits) + { + unsigned long result = 0; + for(size_t i = 0; i < nbits; i++) result += (readBitFromStream(bitp, bits)) << i; + return result; + } + struct HuffmanTree + { + int makeFromLengths(const std::vector& bitlen, unsigned long maxbitlen) + { //make tree given the lengths + unsigned long numcodes = (unsigned long)(bitlen.size()), treepos = 0, nodefilled = 0; + std::vector tree1d(numcodes), blcount(maxbitlen + 1, 0), nextcode(maxbitlen + 1, 0); + for(unsigned long bits = 0; bits < numcodes; bits++) blcount[bitlen[bits]]++; //count number of instances of each code length + for(unsigned long bits = 1; bits <= maxbitlen; bits++) nextcode[bits] = (nextcode[bits - 1] + blcount[bits - 1]) << 1; + for(unsigned long n = 0; n < numcodes; n++) if(bitlen[n] != 0) tree1d[n] = nextcode[bitlen[n]]++; //generate all the codes + tree2d.clear(); tree2d.resize(numcodes * 2, 32767); //32767 here means the tree2d isn't filled there yet + for(unsigned long n = 0; n < numcodes; n++) //the codes + for(unsigned long i = 0; i < bitlen[n]; i++) //the bits for this code + { + unsigned long bit = (tree1d[n] >> (bitlen[n] - i - 1)) & 1; + if(treepos > numcodes - 2) return 55; + if(tree2d[2 * treepos + bit] == 32767) //not yet filled in + { + if(i + 1 == bitlen[n]) { tree2d[2 * treepos + bit] = n; treepos = 0; } //last bit + else { tree2d[2 * treepos + bit] = ++nodefilled + numcodes; treepos = nodefilled; } //addresses are encoded as values > numcodes + } + else treepos = tree2d[2 * treepos + bit] - numcodes; //subtract numcodes from address to get address value + } + return 0; + } + int decode(bool& decoded, unsigned long& result, size_t& treepos, unsigned long bit) const + { //Decodes a symbol from the tree + unsigned long numcodes = (unsigned long)tree2d.size() / 2; + if(treepos >= numcodes) return 11; //error: you appeared outside the codetree + result = tree2d[2 * treepos + bit]; + decoded = (result < numcodes); + treepos = decoded ? 0 : result - numcodes; + return 0; + } + std::vector tree2d; //2D representation of a huffman tree: The one dimension is "0" or "1", the other contains all nodes and leaves of the tree. + }; + struct Inflator + { + int error; + void inflate(std::vector& out, const std::vector& in, size_t inpos = 0) + { + size_t bp = 0, pos = 0; //bit pointer and byte pointer + error = 0; + unsigned long BFINAL = 0; + while(!BFINAL && !error) + { + if(bp >> 3 >= in.size()) { error = 52; return; } //error, bit pointer will jump past memory + BFINAL = readBitFromStream(bp, &in[inpos]); + unsigned long BTYPE = readBitFromStream(bp, &in[inpos]); BTYPE += 2 * readBitFromStream(bp, &in[inpos]); + if(BTYPE == 3) { error = 20; return; } //error: invalid BTYPE + else if(BTYPE == 0) inflateNoCompression(out, &in[inpos], bp, pos, in.size()); + else inflateHuffmanBlock(out, &in[inpos], bp, pos, in.size(), BTYPE); + } + if(!error) out.resize(pos); //Only now we know the true size of out, resize it to that + } + void generateFixedTrees(HuffmanTree& tree, HuffmanTree& treeD) //get the tree of a deflated block with fixed tree + { + std::vector bitlen(288, 8), bitlenD(32, 5);; + for(size_t i = 144; i <= 255; i++) bitlen[i] = 9; + for(size_t i = 256; i <= 279; i++) bitlen[i] = 7; + tree.makeFromLengths(bitlen, 15); + treeD.makeFromLengths(bitlenD, 15); + } + HuffmanTree codetree, codetreeD, codelengthcodetree; //the code tree for Huffman codes, dist codes, and code length codes + unsigned long huffmanDecodeSymbol(const unsigned char* in, size_t& bp, const HuffmanTree& codetree, size_t inlength) + { //decode a single symbol from given list of bits with given code tree. return value is the symbol + bool decoded; unsigned long ct; + for(size_t treepos = 0;;) + { + if((bp & 0x07) == 0 && (bp >> 3) > inlength) { error = 10; return 0; } //error: end reached without endcode + error = codetree.decode(decoded, ct, treepos, readBitFromStream(bp, in)); if(error) return 0; //stop, an error happened + if(decoded) return ct; + } + } + void getTreeInflateDynamic(HuffmanTree& tree, HuffmanTree& treeD, const unsigned char* in, size_t& bp, size_t inlength) + { //get the tree of a deflated block with dynamic tree, the tree itself is also Huffman compressed with a known tree + std::vector bitlen(288, 0), bitlenD(32, 0); + if(bp >> 3 >= inlength - 2) { error = 49; return; } //the bit pointer is or will go past the memory + size_t HLIT = readBitsFromStream(bp, in, 5) + 257; //number of literal/length codes + 257 + size_t HDIST = readBitsFromStream(bp, in, 5) + 1; //number of dist codes + 1 + size_t HCLEN = readBitsFromStream(bp, in, 4) + 4; //number of code length codes + 4 + std::vector codelengthcode(19); //lengths of tree to decode the lengths of the dynamic tree + for(size_t i = 0; i < 19; i++) codelengthcode[CLCL[i]] = (i < HCLEN) ? readBitsFromStream(bp, in, 3) : 0; + error = codelengthcodetree.makeFromLengths(codelengthcode, 7); if(error) return; + size_t i = 0, replength; + while(i < HLIT + HDIST) + { + unsigned long code = huffmanDecodeSymbol(in, bp, codelengthcodetree, inlength); if(error) return; + if(code <= 15) { if(i < HLIT) bitlen[i++] = code; else bitlenD[i++ - HLIT] = code; } //a length code + else if(code == 16) //repeat previous + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 3 + readBitsFromStream(bp, in, 2); + unsigned long value; //set value to the previous code + if((i - 1) < HLIT) value = bitlen[i - 1]; + else value = bitlenD[i - HLIT - 1]; + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 13; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = value; else bitlenD[i++ - HLIT] = value; + } + } + else if(code == 17) //repeat "0" 3-10 times + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 3 + readBitsFromStream(bp, in, 3); + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 14; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = 0; else bitlenD[i++ - HLIT] = 0; + } + } + else if(code == 18) //repeat "0" 11-138 times + { + if(bp >> 3 >= inlength) { error = 50; return; } //error, bit pointer jumps past memory + replength = 11 + readBitsFromStream(bp, in, 7); + for(size_t n = 0; n < replength; n++) //repeat this value in the next lengths + { + if(i >= HLIT + HDIST) { error = 15; return; } //error: i is larger than the amount of codes + if(i < HLIT) bitlen[i++] = 0; else bitlenD[i++ - HLIT] = 0; + } + } + else { error = 16; return; } //error: somehow an unexisting code appeared. This can never happen. + } + if(bitlen[256] == 0) { error = 64; return; } //the length of the end code 256 must be larger than 0 + error = tree.makeFromLengths(bitlen, 15); if(error) return; //now we've finally got HLIT and HDIST, so generate the code trees, and the function is done + error = treeD.makeFromLengths(bitlenD, 15); if(error) return; + } + void inflateHuffmanBlock(std::vector& out, const unsigned char* in, size_t& bp, size_t& pos, size_t inlength, unsigned long btype) + { + if(btype == 1) { generateFixedTrees(codetree, codetreeD); } + else if(btype == 2) { getTreeInflateDynamic(codetree, codetreeD, in, bp, inlength); if(error) return; } + for(;;) + { + unsigned long code = huffmanDecodeSymbol(in, bp, codetree, inlength); if(error) return; + if(code == 256) return; //end code + else if(code <= 255) //literal symbol + { + if(pos >= out.size()) out.resize((pos + 1) * 2); //reserve more room + out[pos++] = (unsigned char)(code); + } + else if(code >= 257 && code <= 285) //length code + { + size_t length = LENBASE[code - 257], numextrabits = LENEXTRA[code - 257]; + if((bp >> 3) >= inlength) { error = 51; return; } //error, bit pointer will jump past memory + length += readBitsFromStream(bp, in, numextrabits); + unsigned long codeD = huffmanDecodeSymbol(in, bp, codetreeD, inlength); if(error) return; + if(codeD > 29) { error = 18; return; } //error: invalid dist code (30-31 are never used) + unsigned long dist = DISTBASE[codeD], numextrabitsD = DISTEXTRA[codeD]; + if((bp >> 3) >= inlength) { error = 51; return; } //error, bit pointer will jump past memory + dist += readBitsFromStream(bp, in, numextrabitsD); + size_t start = pos, back = start - dist; //backwards + if(pos + length >= out.size()) out.resize((pos + length) * 2); //reserve more room + for(size_t i = 0; i < length; i++) { out[pos++] = out[back++]; if(back >= start) back = start - dist; } + } + } + } + void inflateNoCompression(std::vector& out, const unsigned char* in, size_t& bp, size_t& pos, size_t inlength) + { + while((bp & 0x7) != 0) bp++; //go to first boundary of byte + size_t p = bp / 8; + if(p >= inlength - 4) { error = 52; return; } //error, bit pointer will jump past memory + unsigned long LEN = in[p] + 256 * in[p + 1], NLEN = in[p + 2] + 256 * in[p + 3]; p += 4; + if(LEN + NLEN != 65535) { error = 21; return; } //error: NLEN is not one's complement of LEN + if(pos + LEN >= out.size()) out.resize(pos + LEN); + if(p + LEN > inlength) { error = 23; return; } //error: reading outside of in buffer + for(unsigned long n = 0; n < LEN; n++) out[pos++] = in[p++]; //read LEN bytes of literal data + bp = p * 8; + } + }; + int decompress(std::vector& out, const std::vector& in) //returns error value + { + Inflator inflator; + if(in.size() < 2) { return 53; } //error, size of zlib data too small + if((in[0] * 256 + in[1]) % 31 != 0) { return 24; } //error: 256 * in[0] + in[1] must be a multiple of 31, the FCHECK value is supposed to be made that way + unsigned long CM = in[0] & 15, CINFO = (in[0] >> 4) & 15, FDICT = (in[1] >> 5) & 1; + if(CM != 8 || CINFO > 7) { return 25; } //error: only compression method 8: inflate with sliding window of 32k is supported by the PNG spec + if(FDICT != 0) { return 26; } //error: the specification of PNG says about the zlib stream: "The additional flags shall not specify a preset dictionary." + inflator.inflate(out, in, 2); + return inflator.error; //note: adler32 checksum was skipped and ignored + } + }; + struct PNG //nested functions for PNG decoding + { + struct Info + { + unsigned long width, height, colorType, bitDepth, compressionMethod, filterMethod, interlaceMethod, key_r, key_g, key_b; + bool key_defined; //is a transparent color key given? + std::vector palette; + } info; + int error; + void decode(std::vector& out, const unsigned char* in, size_t size, bool convert_to_rgba32) + { + error = 0; + if(size == 0 || in == 0) { error = 48; return; } //the given data is empty + readPngHeader(&in[0], size); if(error) return; + size_t pos = 33; //first byte of the first chunk after the header + std::vector idat; //the data from idat chunks + bool IEND = false, known_type = true; + info.key_defined = false; + while(!IEND) //loop through the chunks, ignoring unknown chunks and stopping at IEND chunk. IDAT data is put at the start of the in buffer + { + if(pos + 8 >= size) { error = 30; return; } //error: size of the in buffer too small to contain next chunk + size_t chunkLength = read32bitInt(&in[pos]); pos += 4; + if(chunkLength > 2147483647) { error = 63; return; } + if(pos + chunkLength >= size) { error = 35; return; } //error: size of the in buffer too small to contain next chunk + if(in[pos + 0] == 'I' && in[pos + 1] == 'D' && in[pos + 2] == 'A' && in[pos + 3] == 'T') //IDAT chunk, containing compressed image data + { + idat.insert(idat.end(), &in[pos + 4], &in[pos + 4 + chunkLength]); + pos += (4 + chunkLength); + } + else if(in[pos + 0] == 'I' && in[pos + 1] == 'E' && in[pos + 2] == 'N' && in[pos + 3] == 'D') { pos += 4; IEND = true; } + else if(in[pos + 0] == 'P' && in[pos + 1] == 'L' && in[pos + 2] == 'T' && in[pos + 3] == 'E') //palette chunk (PLTE) + { + pos += 4; //go after the 4 letters + info.palette.resize(4 * (chunkLength / 3)); + if(info.palette.size() > (4 * 256)) { error = 38; return; } //error: palette too big + for(size_t i = 0; i < info.palette.size(); i += 4) + { + for(size_t j = 0; j < 3; j++) info.palette[i + j] = in[pos++]; //RGB + info.palette[i + 3] = 255; //alpha + } + } + else if(in[pos + 0] == 't' && in[pos + 1] == 'R' && in[pos + 2] == 'N' && in[pos + 3] == 'S') //palette transparency chunk (tRNS) + { + pos += 4; //go after the 4 letters + if(info.colorType == 3) + { + if(4 * chunkLength > info.palette.size()) { error = 39; return; } //error: more alpha values given than there are palette entries + for(size_t i = 0; i < chunkLength; i++) info.palette[4 * i + 3] = in[pos++]; + } + else if(info.colorType == 0) + { + if(chunkLength != 2) { error = 40; return; } //error: this chunk must be 2 bytes for greyscale image + info.key_defined = 1; info.key_r = info.key_g = info.key_b = 256 * in[pos] + in[pos + 1]; pos += 2; + } + else if(info.colorType == 2) + { + if(chunkLength != 6) { error = 41; return; } //error: this chunk must be 6 bytes for RGB image + info.key_defined = 1; + info.key_r = 256 * in[pos] + in[pos + 1]; pos += 2; + info.key_g = 256 * in[pos] + in[pos + 1]; pos += 2; + info.key_b = 256 * in[pos] + in[pos + 1]; pos += 2; + } + else { error = 42; return; } //error: tRNS chunk not allowed for other color models + } + else //it's not an implemented chunk type, so ignore it: skip over the data + { + if(!(in[pos + 0] & 32)) { error = 69; return; } //error: unknown critical chunk (5th bit of first byte of chunk type is 0) + pos += (chunkLength + 4); //skip 4 letters and uninterpreted data of unimplemented chunk + known_type = false; + } + pos += 4; //step over CRC (which is ignored) + } + unsigned long bpp = getBpp(info); + std::vector scanlines(((info.width * (info.height * bpp + 7)) / 8) + info.height); //now the out buffer will be filled + Zlib zlib; //decompress with the Zlib decompressor + error = zlib.decompress(scanlines, idat); if(error) return; //stop if the zlib decompressor returned an error + size_t bytewidth = (bpp + 7) / 8, outlength = (info.height * info.width * bpp + 7) / 8; + out.resize(outlength); //time to fill the out buffer + unsigned char* out_ = outlength ? &out[0] : 0; //use a regular pointer to the std::vector for faster code if compiled without optimization + if(info.interlaceMethod == 0) //no interlace, just filter + { + size_t linestart = 0, linelength = (info.width * bpp + 7) / 8; //length in bytes of a scanline, excluding the filtertype byte + if(bpp >= 8) //byte per byte + for(unsigned long y = 0; y < info.height; y++) + { + unsigned long filterType = scanlines[linestart]; + const unsigned char* prevline = (y == 0) ? 0 : &out_[(y - 1) * info.width * bytewidth]; + unFilterScanline(&out_[linestart - y], &scanlines[linestart + 1], prevline, bytewidth, filterType, linelength); if(error) return; + linestart += (1 + linelength); //go to start of next scanline + } + else //less than 8 bits per pixel, so fill it up bit per bit + { + std::vector templine((info.width * bpp + 7) >> 3); //only used if bpp < 8 + for(size_t y = 0, obp = 0; y < info.height; y++) + { + unsigned long filterType = scanlines[linestart]; + const unsigned char* prevline = (y == 0) ? 0 : &out_[(y - 1) * info.width * bytewidth]; + unFilterScanline(&templine[0], &scanlines[linestart + 1], prevline, bytewidth, filterType, linelength); if(error) return; + for(size_t bp = 0; bp < info.width * bpp;) setBitOfReversedStream(obp, out_, readBitFromReversedStream(bp, &templine[0])); + linestart += (1 + linelength); //go to start of next scanline + } + } + } + else //interlaceMethod is 1 (Adam7) + { + size_t passw[7] = { (info.width + 7) / 8, (info.width + 3) / 8, (info.width + 3) / 4, (info.width + 1) / 4, (info.width + 1) / 2, (info.width + 0) / 2, (info.width + 0) / 1 }; + size_t passh[7] = { (info.height + 7) / 8, (info.height + 7) / 8, (info.height + 3) / 8, (info.height + 3) / 4, (info.height + 1) / 4, (info.height + 1) / 2, (info.height + 0) / 2 }; + size_t passstart[7] = {0}; + size_t pattern[28] = {0,4,0,2,0,1,0,0,0,4,0,2,0,1,8,8,4,4,2,2,1,8,8,8,4,4,2,2}; //values for the adam7 passes + for(int i = 0; i < 6; i++) passstart[i + 1] = passstart[i] + passh[i] * ((passw[i] ? 1 : 0) + (passw[i] * bpp + 7) / 8); + std::vector scanlineo((info.width * bpp + 7) / 8), scanlinen((info.width * bpp + 7) / 8); //"old" and "new" scanline + for(int i = 0; i < 7; i++) + adam7Pass(&out_[0], &scanlinen[0], &scanlineo[0], &scanlines[passstart[i]], info.width, pattern[i], pattern[i + 7], pattern[i + 14], pattern[i + 21], passw[i], passh[i], bpp); + } + if(convert_to_rgba32 && (info.colorType != 6 || info.bitDepth != 8)) //conversion needed + { + std::vector data = out; + error = convert(out, &data[0], info, info.width, info.height); + } + } + void readPngHeader(const unsigned char* in, size_t inlength) //read the information from the header and store it in the Info + { + if(inlength < 29) { error = 27; return; } //error: the data length is smaller than the length of the header + if(in[0] != 137 || in[1] != 80 || in[2] != 78 || in[3] != 71 || in[4] != 13 || in[5] != 10 || in[6] != 26 || in[7] != 10) { error = 28; return; } //no PNG signature + if(in[12] != 'I' || in[13] != 'H' || in[14] != 'D' || in[15] != 'R') { error = 29; return; } //error: it doesn't start with a IHDR chunk! + info.width = read32bitInt(&in[16]); info.height = read32bitInt(&in[20]); + info.bitDepth = in[24]; info.colorType = in[25]; + info.compressionMethod = in[26]; if(in[26] != 0) { error = 32; return; } //error: only compression method 0 is allowed in the specification + info.filterMethod = in[27]; if(in[27] != 0) { error = 33; return; } //error: only filter method 0 is allowed in the specification + info.interlaceMethod = in[28]; if(in[28] > 1) { error = 34; return; } //error: only interlace methods 0 and 1 exist in the specification + error = checkColorValidity(info.colorType, info.bitDepth); + } + void unFilterScanline(unsigned char* recon, const unsigned char* scanline, const unsigned char* precon, size_t bytewidth, unsigned long filterType, size_t length) + { + switch(filterType) + { + case 0: for(size_t i = 0; i < length; i++) recon[i] = scanline[i]; break; + case 1: + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + recon[i - bytewidth]; + break; + case 2: + if(precon) for(size_t i = 0; i < length; i++) recon[i] = scanline[i] + precon[i]; + else for(size_t i = 0; i < length; i++) recon[i] = scanline[i]; + break; + case 3: + if(precon) + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i] + precon[i] / 2; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + ((recon[i - bytewidth] + precon[i]) / 2); + } + else + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + recon[i - bytewidth] / 2; + } + break; + case 4: + if(precon) + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i] + paethPredictor(0, precon[i], 0); + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + paethPredictor(recon[i - bytewidth], precon[i], precon[i - bytewidth]); + } + else + { + for(size_t i = 0; i < bytewidth; i++) recon[i] = scanline[i]; + for(size_t i = bytewidth; i < length; i++) recon[i] = scanline[i] + paethPredictor(recon[i - bytewidth], 0, 0); + } + break; + default: error = 36; return; //error: unexisting filter type given + } + } + void adam7Pass(unsigned char* out, unsigned char* linen, unsigned char* lineo, const unsigned char* in, unsigned long w, size_t passleft, size_t passtop, size_t spacex, size_t spacey, size_t passw, size_t passh, unsigned long bpp) + { //filter and reposition the pixels into the output when the image is Adam7 interlaced. This function can only do it after the full image is already decoded. The out buffer must have the correct allocated memory size already. + if(passw == 0) return; + size_t bytewidth = (bpp + 7) / 8, linelength = 1 + ((bpp * passw + 7) / 8); + for(unsigned long y = 0; y < passh; y++) + { + unsigned char filterType = in[y * linelength], *prevline = (y == 0) ? 0 : lineo; + unFilterScanline(linen, &in[y * linelength + 1], prevline, bytewidth, filterType, (w * bpp + 7) / 8); if(error) return; + if(bpp >= 8) for(size_t i = 0; i < passw; i++) for(size_t b = 0; b < bytewidth; b++) //b = current byte of this pixel + out[bytewidth * w * (passtop + spacey * y) + bytewidth * (passleft + spacex * i) + b] = linen[bytewidth * i + b]; + else for(size_t i = 0; i < passw; i++) + { + size_t obp = bpp * w * (passtop + spacey * y) + bpp * (passleft + spacex * i), bp = i * bpp; + for(size_t b = 0; b < bpp; b++) setBitOfReversedStream(obp, out, readBitFromReversedStream(bp, &linen[0])); + } + unsigned char* temp = linen; linen = lineo; lineo = temp; //swap the two buffer pointers "line old" and "line new" + } + } + static unsigned long readBitFromReversedStream(size_t& bitp, const unsigned char* bits) { unsigned long result = (bits[bitp >> 3] >> (7 - (bitp & 0x7))) & 1; bitp++; return result;} + static unsigned long readBitsFromReversedStream(size_t& bitp, const unsigned char* bits, unsigned long nbits) + { + unsigned long result = 0; + for(size_t i = nbits - 1; i < nbits; i--) result += ((readBitFromReversedStream(bitp, bits)) << i); + return result; + } + void setBitOfReversedStream(size_t& bitp, unsigned char* bits, unsigned long bit) { bits[bitp >> 3] |= (bit << (7 - (bitp & 0x7))); bitp++; } + unsigned long read32bitInt(const unsigned char* buffer) { return (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]; } + int checkColorValidity(unsigned long colorType, unsigned long bd) //return type is a LodePNG error code + { + if((colorType == 2 || colorType == 4 || colorType == 6)) { if(!(bd == 8 || bd == 16)) return 37; else return 0; } + else if(colorType == 0) { if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 || bd == 16)) return 37; else return 0; } + else if(colorType == 3) { if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 )) return 37; else return 0; } + else return 31; //unexisting color type + } + unsigned long getBpp(const Info& info) + { + if(info.colorType == 2) return (3 * info.bitDepth); + else if(info.colorType >= 4) return (info.colorType - 2) * info.bitDepth; + else return info.bitDepth; + } + int convert(std::vector& out, const unsigned char* in, Info& infoIn, unsigned long w, unsigned long h) + { //converts from any color type to 32-bit. return value = LodePNG error code + size_t numpixels = w * h, bp = 0; + out.resize(numpixels * 4); + unsigned char* out_ = out.empty() ? 0 : &out[0]; //faster if compiled without optimization + if(infoIn.bitDepth == 8 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[i]; + out_[4 * i + 3] = (infoIn.key_defined && in[i] == infoIn.key_r) ? 0 : 255; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 2) //RGB color + for(size_t i = 0; i < numpixels; i++) + { + for(size_t c = 0; c < 3; c++) out_[4 * i + c] = in[3 * i + c]; + out_[4 * i + 3] = (infoIn.key_defined == 1 && in[3 * i + 0] == infoIn.key_r && in[3 * i + 1] == infoIn.key_g && in[3 * i + 2] == infoIn.key_b) ? 0 : 255; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 3) //indexed color (palette) + for(size_t i = 0; i < numpixels; i++) + { + if(4U * in[i] >= infoIn.palette.size()) return 46; + for(size_t c = 0; c < 4; c++) out_[4 * i + c] = infoIn.palette[4 * in[i] + c]; //get rgb colors from the palette + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 4) //greyscale with alpha + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[2 * i + 0]; + out_[4 * i + 3] = in[2 * i + 1]; + } + else if(infoIn.bitDepth == 8 && infoIn.colorType == 6) for(size_t i = 0; i < numpixels; i++) for(size_t c = 0; c < 4; c++) out_[4 * i + c] = in[4 * i + c]; //RGB with alpha + else if(infoIn.bitDepth == 16 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[2 * i]; + out_[4 * i + 3] = (infoIn.key_defined && 256U * in[i] + in[i + 1] == infoIn.key_r) ? 0 : 255; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 2) //RGB color + for(size_t i = 0; i < numpixels; i++) + { + for(size_t c = 0; c < 3; c++) out_[4 * i + c] = in[6 * i + 2 * c]; + out_[4 * i + 3] = (infoIn.key_defined && 256U*in[6*i+0]+in[6*i+1] == infoIn.key_r && 256U*in[6*i+2]+in[6*i+3] == infoIn.key_g && 256U*in[6*i+4]+in[6*i+5] == infoIn.key_b) ? 0 : 255; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 4) //greyscale with alpha + for(size_t i = 0; i < numpixels; i++) + { + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = in[4 * i]; //most significant byte + out_[4 * i + 3] = in[4 * i + 2]; + } + else if(infoIn.bitDepth == 16 && infoIn.colorType == 6) for(size_t i = 0; i < numpixels; i++) for(size_t c = 0; c < 4; c++) out_[4 * i + c] = in[8 * i + 2 * c]; //RGB with alpha + else if(infoIn.bitDepth < 8 && infoIn.colorType == 0) //greyscale + for(size_t i = 0; i < numpixels; i++) + { + unsigned long value = (readBitsFromReversedStream(bp, in, infoIn.bitDepth) * 255) / ((1 << infoIn.bitDepth) - 1); //scale value from 0 to 255 + out_[4 * i + 0] = out_[4 * i + 1] = out_[4 * i + 2] = (unsigned char)(value); + out_[4 * i + 3] = (infoIn.key_defined && value && ((1U << infoIn.bitDepth) - 1U) == infoIn.key_r && ((1U << infoIn.bitDepth) - 1U)) ? 0 : 255; + } + else if(infoIn.bitDepth < 8 && infoIn.colorType == 3) //palette + for(size_t i = 0; i < numpixels; i++) + { + unsigned long value = readBitsFromReversedStream(bp, in, infoIn.bitDepth); + if(4 * value >= infoIn.palette.size()) return 47; + for(size_t c = 0; c < 4; c++) out_[4 * i + c] = infoIn.palette[4 * value + c]; //get rgb colors from the palette + } + return 0; + } + unsigned char paethPredictor(short a, short b, short c) //Paeth predicter, used by PNG filter type 4 + { + short p = a + b - c, pa = p > a ? (p - a) : (a - p), pb = p > b ? (p - b) : (b - p), pc = p > c ? (p - c) : (c - p); + return (unsigned char)((pa <= pb && pa <= pc) ? a : pb <= pc ? b : c); + } + }; + PNG decoder; decoder.decode(out_image, in_png, in_size, convert_to_rgba32); + image_width = decoder.info.width; image_height = decoder.info.height; + return decoder.error; +} + +#if 0 + + + +//an example using the PNG loading function: + +#include +#include + +void loadFile(std::vector& buffer, const std::string& filename) //designed for loading files from hard disk in an std::vector +{ + std::ifstream file(filename.c_str(), std::ios::in|std::ios::binary|std::ios::ate); + + //get filesize + std::streamsize size = 0; + if(file.seekg(0, std::ios::end).good()) size = file.tellg(); + if(file.seekg(0, std::ios::beg).good()) size -= file.tellg(); + + //read contents of the file into the vector + if(size > 0) + { + buffer.resize((size_t)size); + file.read((char*)(&buffer[0]), size); + } + else buffer.clear(); +} + +int main(int argc, char *argv[]) +{ + const char* filename = argc > 1 ? argv[1] : "test.png"; + + //load and decode + std::vector buffer, image; + loadFile(buffer, filename); + unsigned long w, h; + int error = decodePNG(image, w, h, buffer.empty() ? 0 : &buffer[0], (unsigned long)buffer.size()); + + //if there's an error, display it + if(error != 0) std::cout << "error: " << error << std::endl; + + //the pixels are now in the vector "image", use it as texture, draw it, ... + + if(image.size() > 4) std::cout << "width: " << w << " height: " << h << " first pixel: " << std::hex << int(image[0]) << int(image[1]) << int(image[2]) << int(image[3]) << std::endl; +} + +/* + //this is test code, it displays the pixels of a 1 bit PNG. To use it, set the flag convert_to_rgba32 to false and load a 1-bit PNG image with a small size (so that its ASCII representation can fit in a console window) + for(int y = 0; y < h; y++) + { + for(int x = 0; x < w; x++) + { + int i = y * h + x; + std::cout << (((image[i/8] >> (7-i%8)) & 1) ? '.' : '#'); + } + std::cout << std::endl; + } +*/ + +#endif diff --git a/libraries/ZWidget/src/core/picopng/picopng.h b/libraries/ZWidget/src/core/picopng/picopng.h new file mode 100644 index 000000000..87dc961be --- /dev/null +++ b/libraries/ZWidget/src/core/picopng/picopng.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +int decodePNG(std::vector& out_image, unsigned long& image_width, unsigned long& image_height, const unsigned char* in_png, size_t in_size, bool convert_to_rgba32 = true); diff --git a/libraries/ZWidget/src/core/span_layout.cpp b/libraries/ZWidget/src/core/span_layout.cpp new file mode 100644 index 000000000..1b68f6ef7 --- /dev/null +++ b/libraries/ZWidget/src/core/span_layout.cpp @@ -0,0 +1,883 @@ + +#include "core/span_layout.h" +#include "core/canvas.h" +#include "core/widget.h" +#include "core/font.h" +#include "core/image.h" + +SpanLayout::SpanLayout() +{ +} + +SpanLayout::~SpanLayout() +{ +} + +void SpanLayout::Clear() +{ + objects.clear(); + text.clear(); + lines.clear(); +} + +std::vector SpanLayout::GetRectById(int id) const +{ + std::vector segment_rects; + + double x = position.x; + double y = position.y; + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + const Line& line = lines[line_index]; + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + const LineSegment& segment = line.segments[segment_index]; + if (segment.id == id) + { + segment_rects.push_back(Rect(x + segment.x_position, y, segment.width, y + line.height)); + } + } + y += line.height; + } + + return segment_rects; +} + +void SpanLayout::DrawLayout(Canvas* canvas) +{ + double x = position.x; + double y = position.y; + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + Line& line = lines[line_index]; + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + switch (segment.type) + { + case object_text: + DrawLayoutText(canvas, line, segment, x, y); + break; + case object_image: + DrawLayoutImage(canvas, line, segment, x, y); + break; + case object_component: + break; + } + + } + + if (line_index + 1 == lines.size() && !line.segments.empty()) + { + LineSegment& segment = line.segments.back(); + if (cursor_visible && segment.end <= cursor_pos) + { + switch (segment.type) + { + case object_text: + { + double cursor_x = x + segment.x_position + canvas->measureText(segment.font, text.substr(segment.start, segment.end - segment.start)).width; + double cursor_width = 1; + canvas->fillRect(Rect::ltrb(cursor_x, y + line.ascender - segment.ascender, cursor_width, y + line.ascender + segment.descender), cursor_color); + } + break; + } + } + } + + y += line.height; + } +} + +void SpanLayout::DrawLayoutEllipsis(Canvas* canvas, const Rect& content_rect) +{ + is_ellipsis_draw = true; + ellipsis_content_rect = content_rect; + try + { + is_ellipsis_draw = false; + DrawLayout(canvas); + } + catch (...) + { + is_ellipsis_draw = false; + throw; + } +} + +void SpanLayout::DrawLayoutImage(Canvas* canvas, Line& line, LineSegment& segment, double x, double y) +{ + canvas->drawImage(segment.image, Point(x + segment.x_position, y + line.ascender - segment.ascender)); +} + +void SpanLayout::DrawLayoutText(Canvas* canvas, Line& line, LineSegment& segment, double x, double y) +{ + std::string segment_text = text.substr(segment.start, segment.end - segment.start); + + int length = (int)segment_text.length(); + int s1 = clamp((int)sel_start - (int)segment.start, 0, length); + int s2 = clamp((int)sel_end - (int)segment.start, 0, length); + + if (s1 != s2) + { + double xx = x + segment.x_position; + double xx0 = xx + canvas->measureText(segment.font, segment_text.substr(0, s1)).width; + double xx1 = xx0 + canvas->measureText(segment.font, segment_text.substr(s1, s2 - s1)).width; + double sel_width = canvas->measureText(segment.font, segment_text.substr(s1, s2 - s1)).width; + + canvas->fillRect(Rect::ltrb(xx0, y + line.ascender - segment.ascender, xx1, y + line.ascender + segment.descender), sel_background); + + if (cursor_visible && cursor_pos >= segment.start && cursor_pos < segment.end) + { + double cursor_x = x + segment.x_position + canvas->measureText(segment.font, text.substr(segment.start, cursor_pos - segment.start)).width; + double cursor_width = cursor_overwrite_mode ? canvas->measureText(segment.font, text.substr(cursor_pos, 1)).width : 1; + canvas->fillRect(Rect::ltrb(cursor_x, y + line.ascender - segment.ascender, cursor_x + cursor_width, y + line.ascender + segment.descender), cursor_color); + } + + if (s1 > 0) + { + if (is_ellipsis_draw) + canvas->drawTextEllipsis(segment.font, Point(xx, y + line.ascender), ellipsis_content_rect, segment_text.substr(0, s1), segment.color); + else + canvas->drawText(segment.font, Point(xx, y + line.ascender), segment_text.substr(0, s1), segment.color); + } + if (is_ellipsis_draw) + canvas->drawTextEllipsis(segment.font, Point(xx0, y + line.ascender), ellipsis_content_rect, segment_text.substr(s1, s2 - s1), sel_foreground); + else + canvas->drawText(segment.font, Point(xx0, y + line.ascender), segment_text.substr(s1, s2 - s1), sel_foreground); + xx += sel_width; + if (s2 < length) + { + if (is_ellipsis_draw) + canvas->drawTextEllipsis(segment.font, Point(xx1, y + line.ascender), ellipsis_content_rect, segment_text.substr(s2), segment.color); + else + canvas->drawText(segment.font, Point(xx1, y + line.ascender), segment_text.substr(s2), segment.color); + } + } + else + { + if (cursor_visible && cursor_pos >= segment.start && cursor_pos < segment.end) + { + double cursor_x = x + segment.x_position + canvas->measureText(segment.font, text.substr(segment.start, cursor_pos - segment.start)).width; + double cursor_width = cursor_overwrite_mode ? canvas->measureText(segment.font, text.substr(cursor_pos, 1)).width : 1; + canvas->fillRect(Rect::ltrb(cursor_x, y + line.ascender - segment.ascender, cursor_x + cursor_width, y + line.ascender + segment.descender), cursor_color); + } + + if (is_ellipsis_draw) + canvas->drawTextEllipsis(segment.font, Point(x + segment.x_position, y + line.ascender), ellipsis_content_rect, segment_text, segment.color); + else + canvas->drawText(segment.font, Point(x + segment.x_position, y + line.ascender), segment_text, segment.color); + } +} + +SpanLayout::HitTestResult SpanLayout::HitTest(Canvas* canvas, const Point& pos) +{ + SpanLayout::HitTestResult result; + + if (lines.empty()) + { + result.type = SpanLayout::HitTestResult::no_objects_available; + return result; + } + + double x = position.x; + double y = position.y; + + // Check if we are outside to the top + if (pos.y < y) + { + result.type = SpanLayout::HitTestResult::outside_top; + result.object_id = lines[0].segments[0].id; + result.offset = 0; + return result; + } + + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + Line& line = lines[line_index]; + + // Check if we found current line + if (pos.y >= y && pos.y <= y + line.height) + { + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + + // Check if we are outside to the left + if (segment_index == 0 && pos.x < x) + { + result.type = SpanLayout::HitTestResult::outside_left; + result.object_id = segment.id; + result.offset = segment.start; + return result; + } + + // Check if we are inside a segment + if (pos.x >= x + segment.x_position && pos.x <= x + segment.x_position + segment.width) + { + std::string segment_text = text.substr(segment.start, segment.end - segment.start); + Point hit_point(pos.x - x - segment.x_position, 0); + size_t offset = segment.start + canvas->getCharacterIndex(segment.font, segment_text, hit_point); + + result.type = SpanLayout::HitTestResult::inside; + result.object_id = segment.id; + result.offset = offset; + return result; + } + + // Check if we are outside to the right + if (segment_index == line.segments.size() - 1 && pos.x > x + segment.x_position + segment.width) + { + result.type = SpanLayout::HitTestResult::outside_right; + result.object_id = segment.id; + result.offset = segment.end; + return result; + } + } + } + + y += line.height; + } + + // We are outside to the bottom + const Line& last_line = lines[lines.size() - 1]; + const LineSegment& last_segment = last_line.segments[last_line.segments.size() - 1]; + + result.type = SpanLayout::HitTestResult::outside_bottom; + result.object_id = last_segment.id; + result.offset = last_segment.end; + return result; +} + +Size SpanLayout::GetSize() const +{ + return GetRect().size(); +} + +Rect SpanLayout::GetRect() const +{ + double x = position.x; + double y = position.y; + + const double max_value = 0x70000000; + double left = max_value; + double top = max_value; + double right = -max_value; + double bottom = -max_value; + + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + const Line& line = lines[line_index]; + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + const LineSegment& segment = line.segments[segment_index]; + Rect area(Point(x + segment.x_position, y), Size(segment.width, line.height)); + + left = std::min(left, area.left()); + right = std::max(right, area.right()); + top = std::min(top, area.top()); + bottom = std::max(bottom, area.bottom()); + } + y += line.height; + } + if (left > right) + left = right = position.x; + + if (top > bottom) + top = bottom = position.y; + + return Rect::ltrb(left, top, right, bottom); +} + +void SpanLayout::AddText(const std::string& more_text, std::shared_ptr font, const Colorf& color, int id) +{ + SpanObject object; + object.type = object_text; + object.start = text.length(); + object.end = object.start + more_text.length(); + object.font = font; + object.color = color; + object.id = id; + objects.push_back(object); + text += more_text; +} + +void SpanLayout::AddImage(std::shared_ptr image, double baseline_offset, int id) +{ + SpanObject object; + object.type = object_image; + object.image = image; + object.baseline_offset = baseline_offset; + object.id = id; + object.start = text.length(); + object.end = object.start + 1; + objects.push_back(object); + text += "*"; +} + +void SpanLayout::AddWidget(Widget* component, double baseline_offset, int id) +{ + SpanObject object; + object.type = object_component; + object.component = component; + object.baseline_offset = baseline_offset; + object.id = id; + object.start = text.length(); + object.end = object.start + 1; + objects.push_back(object); + text += "*"; +} + +void SpanLayout::Layout(Canvas* canvas, double max_width) +{ + LayoutLines(canvas, max_width); + + switch (alignment) + { + case span_right: AlignRight(max_width); break; + case span_center: AlignCenter(max_width); break; + case span_justify: AlignJustify(max_width); break; + case span_left: + default: break; + } +} + +void SpanLayout::SetPosition(const Point& pos) +{ + position = pos; +} + +SpanLayout::TextSizeResult SpanLayout::FindTextSize(Canvas* canvas, const TextBlock& block, size_t object_index) +{ + std::shared_ptr font = objects[object_index].font; + if (layout_cache.object_index != (int)object_index) + { + layout_cache.object_index = (int)object_index; + layout_cache.metrics = canvas->getFontMetrics(font); + } + + TextSizeResult result; + result.start = block.start; + size_t pos = block.start; + double x_position = 0; + while (pos != block.end) + { + size_t end = std::min(objects[object_index].end, block.end); + std::string subtext = text.substr(pos, end - pos); + + Size text_size = canvas->measureText(font, subtext).size(); + + result.width += text_size.width; + result.height = std::max(result.height, layout_cache.metrics.height + layout_cache.metrics.external_leading); + result.ascender = std::max(result.ascender, layout_cache.metrics.ascent); + result.descender = std::max(result.descender, layout_cache.metrics.descent); + + LineSegment segment; + segment.type = object_text; + segment.start = pos; + segment.end = end; + segment.font = objects[object_index].font; + segment.color = objects[object_index].color; + segment.id = objects[object_index].id; + segment.x_position = x_position; + segment.width = text_size.width; + segment.ascender = layout_cache.metrics.ascent; + segment.descender = layout_cache.metrics.descent; + x_position += text_size.width; + result.segments.push_back(segment); + + pos = end; + if (pos == objects[object_index].end) + { + object_index++; + result.objects_traversed++; + + if (object_index < objects.size()) + { + layout_cache.object_index = (int)object_index; + font = objects[object_index].font; + layout_cache.metrics = canvas->getFontMetrics(font); + } + } + } + result.end = pos; + return result; +} + +std::vector SpanLayout::FindTextBlocks() +{ + std::vector blocks; + std::vector::iterator block_object_it; + + // Find first object that is not text: + for (block_object_it = objects.begin(); block_object_it != objects.end() && (*block_object_it).type == object_text; ++block_object_it); + + std::string::size_type pos = 0; + while (pos < text.size()) + { + // Find end of text block: + std::string::size_type end_pos; + switch (text[pos]) + { + case ' ': + case '\t': + case '\n': + end_pos = text.find_first_not_of(text[pos], pos); + break; + default: + end_pos = text.find_first_of(" \t\n", pos); + break; + } + + if (end_pos == std::string::npos) + end_pos = text.length(); + + // If we traversed past an object that is not text: + if (block_object_it != objects.end() && (*block_object_it).start < end_pos) + { + // End text block + end_pos = (*block_object_it).start; + if (end_pos > pos) + { + TextBlock block; + block.start = pos; + block.end = end_pos; + blocks.push_back(block); + } + + // Create object block: + pos = end_pos; + end_pos = pos + 1; + + TextBlock block; + block.start = pos; + block.end = end_pos; + blocks.push_back(block); + + // Find next object that is not text: + for (++block_object_it; block_object_it != objects.end() && (*block_object_it).type == object_text; ++block_object_it); + } + else + { + if (end_pos > pos) + { + TextBlock block; + block.start = pos; + block.end = end_pos; + blocks.push_back(block); + } + } + + pos = end_pos; + } + + return blocks; +} + +void SpanLayout::SetAlign(SpanAlign align) +{ + alignment = align; +} + +void SpanLayout::LayoutLines(Canvas* canvas, double max_width) +{ + lines.clear(); + if (objects.empty()) + return; + + layout_cache.metrics = {}; + layout_cache.object_index = -1; + + CurrentLine current_line; + std::vector blocks = FindTextBlocks(); + for (std::vector::size_type block_index = 0; block_index < blocks.size(); block_index++) + { + if (objects[current_line.object_index].type == object_text) + LayoutText(canvas, blocks, block_index, current_line, max_width); + else + LayoutBlock(current_line, max_width, blocks, block_index); + } + NextLine(current_line); +} + +void SpanLayout::LayoutBlock(CurrentLine& current_line, double max_width, std::vector& blocks, std::vector::size_type block_index) +{ + if (objects[current_line.object_index].float_type == float_none) + LayoutInlineBlock(current_line, max_width, blocks, block_index); + else + LayoutFloatBlock(current_line, max_width); + + current_line.object_index++; +} + +void SpanLayout::LayoutInlineBlock(CurrentLine& current_line, double max_width, std::vector& blocks, std::vector::size_type block_index) +{ + Size size; + LineSegment segment; + if (objects[current_line.object_index].type == object_image) + { + size = Size(objects[current_line.object_index].image->GetWidth(), objects[current_line.object_index].image->GetHeight()); + segment.type = object_image; + segment.image = objects[current_line.object_index].image; + } + else if (objects[current_line.object_index].type == object_component) + { + size = objects[current_line.object_index].component->GetSize(); + segment.type = object_component; + segment.component = objects[current_line.object_index].component; + } + + if (current_line.x_position + size.width > max_width) + NextLine(current_line); + + segment.x_position = current_line.x_position; + segment.width = size.width; + segment.start = blocks[block_index].start; + segment.end = blocks[block_index].end; + segment.id = objects[current_line.object_index].id; + segment.ascender = size.height - objects[current_line.object_index].baseline_offset; + current_line.cur_line.segments.push_back(segment); + current_line.cur_line.height = std::max(current_line.cur_line.height, size.height + objects[current_line.object_index].baseline_offset); + current_line.cur_line.ascender = std::max(current_line.cur_line.ascender, segment.ascender); + current_line.x_position += size.width; +} + +void SpanLayout::LayoutFloatBlock(CurrentLine& current_line, double max_width) +{ + FloatBox floatbox; + floatbox.type = objects[current_line.object_index].type; + floatbox.image = objects[current_line.object_index].image; + floatbox.component = objects[current_line.object_index].component; + floatbox.id = objects[current_line.object_index].id; + if (objects[current_line.object_index].type == object_image) + floatbox.rect = Rect::xywh(0, current_line.y_position, floatbox.image->GetWidth(), floatbox.image->GetHeight()); + else if (objects[current_line.object_index].type == object_component) + floatbox.rect = Rect::xywh(0, current_line.y_position, floatbox.component->GetWidth(), floatbox.component->GetHeight()); + + if (objects[current_line.object_index].float_type == float_left) + floats_left.push_back(FloatBoxLeft(floatbox, max_width)); + else + floats_right.push_back(FloatBoxRight(floatbox, max_width)); + + ReflowLine(current_line, max_width); +} + +void SpanLayout::ReflowLine(CurrentLine& step, double max_width) +{ +} + +SpanLayout::FloatBox SpanLayout::FloatBoxLeft(FloatBox box, double max_width) +{ + return FloatBoxAny(box, max_width, floats_left); +} + +SpanLayout::FloatBox SpanLayout::FloatBoxRight(FloatBox box, double max_width) +{ + return FloatBoxAny(box, max_width, floats_right); +} + +SpanLayout::FloatBox SpanLayout::FloatBoxAny(FloatBox box, double max_width, const std::vector& floats1) +{ + bool restart; + do + { + restart = false; + for (size_t i = 0; i < floats1.size(); i++) + { + double top = std::max(floats1[i].rect.top(), box.rect.top()); + double bottom = std::min(floats1[i].rect.bottom(), box.rect.bottom()); + if (bottom > top && box.rect.left() < floats1[i].rect.right()) + { + Size s = box.rect.size(); + box.rect.x = floats1[i].rect.x; + box.rect.width = s.width; + + if (!BoxFitsOnLine(box, max_width)) + { + box.rect.x = 0; + box.rect.width = s.width; + box.rect.y = floats1[i].rect.bottom(); + box.rect.height = s.height; + restart = true; + break; + } + } + } + } while (restart); + return box; +} + +bool SpanLayout::BoxFitsOnLine(const FloatBox& box, double max_width) +{ + for (size_t i = 0; i < floats_right.size(); i++) + { + double top = std::max(floats_right[i].rect.top(), box.rect.top()); + double bottom = std::min(floats_right[i].rect.bottom(), box.rect.bottom()); + if (bottom > top) + { + if (box.rect.right() + floats_right[i].rect.right() > max_width) + return false; + } + } + return true; +} + +void SpanLayout::LayoutText(Canvas* canvas, std::vector blocks, std::vector::size_type block_index, CurrentLine& current_line, double max_width) +{ + TextSizeResult text_size_result = FindTextSize(canvas, blocks[block_index], current_line.object_index); + current_line.object_index += text_size_result.objects_traversed; + + current_line.cur_line.width = current_line.x_position; + + if (IsNewline(blocks[block_index])) + { + current_line.cur_line.height = std::max(current_line.cur_line.height, text_size_result.height); + current_line.cur_line.ascender = std::max(current_line.cur_line.ascender, text_size_result.ascender); + NextLine(current_line); + } + else + { + if (!FitsOnLine(current_line.x_position, text_size_result, max_width) && !IsWhitespace(blocks[block_index])) + { + if (LargerThanLine(text_size_result, max_width)) + { + // force line breaks to make it fit + ForcePlaceLineSegments(current_line, text_size_result, max_width); + } + else + { + NextLine(current_line); + PlaceLineSegments(current_line, text_size_result); + } + } + else + { + PlaceLineSegments(current_line, text_size_result); + } + } +} + +void SpanLayout::NextLine(CurrentLine& current_line) +{ + current_line.cur_line.width = current_line.x_position; + for (std::vector::reverse_iterator it = current_line.cur_line.segments.rbegin(); it != current_line.cur_line.segments.rend(); ++it) + { + LineSegment& segment = *it; + if (segment.type == object_text) + { + std::string s = text.substr(segment.start, segment.end - segment.start); + if (s.find_first_not_of(" \t\r\n") != std::string::npos) + { + current_line.cur_line.width = segment.x_position + segment.width; + break; + } + else + { + // We remove the width so that GetRect() reports the correct sizes + segment.width = 0; + } + } + else + { + current_line.cur_line.width = segment.x_position + segment.width; + break; + } + } + + double height = current_line.cur_line.height; + lines.push_back(current_line.cur_line); + current_line.cur_line = Line(); + current_line.x_position = 0; + current_line.y_position += height; +} + +void SpanLayout::PlaceLineSegments(CurrentLine& current_line, TextSizeResult& text_size_result) +{ + for (std::vector::iterator it = text_size_result.segments.begin(); it != text_size_result.segments.end(); ++it) + { + LineSegment segment = *it; + segment.x_position += current_line.x_position; + current_line.cur_line.segments.push_back(segment); + } + current_line.x_position += text_size_result.width; + current_line.cur_line.height = std::max(current_line.cur_line.height, text_size_result.height); + current_line.cur_line.ascender = std::max(current_line.cur_line.ascender, text_size_result.ascender); +} + +void SpanLayout::ForcePlaceLineSegments(CurrentLine& current_line, TextSizeResult& text_size_result, double max_width) +{ + if (current_line.x_position != 0) + NextLine(current_line); + + // to do: do this properly - for now we just place the entire block on one line + PlaceLineSegments(current_line, text_size_result); +} + +bool SpanLayout::IsNewline(const TextBlock& block) +{ + return block.start != block.end && text[block.start] == '\n'; +} + +bool SpanLayout::IsWhitespace(const TextBlock& block) +{ + return block.start != block.end && text[block.start] == ' '; +} + +bool SpanLayout::FitsOnLine(double x_position, const TextSizeResult& text_size_result, double max_width) +{ + return x_position + text_size_result.width <= max_width; +} + +bool SpanLayout::LargerThanLine(const TextSizeResult& text_size_result, double max_width) +{ + return text_size_result.width > max_width; +} + +void SpanLayout::AlignRight(double max_width) +{ + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + Line& line = lines[line_index]; + double offset = max_width - line.width; + if (offset < 0) offset = 0; + + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + segment.x_position += offset; + } + } +} + +void SpanLayout::AlignCenter(double max_width) +{ + for (std::vector::size_type line_index = 0; line_index < lines.size(); line_index++) + { + Line& line = lines[line_index]; + double offset = (max_width - line.width) / 2; + if (offset < 0) offset = 0; + + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + segment.x_position += offset; + } + } +} + +void SpanLayout::AlignJustify(double max_width) +{ + // Note, we do not justify the last line + for (std::vector::size_type line_index = 0; line_index + 1 < lines.size(); line_index++) + { + Line& line = lines[line_index]; + double offset = max_width - line.width; + if (offset < 0) offset = 0; + + if (line.segments.size() <= 1) // Do not justify line if only one word exists + continue; + + for (std::vector::size_type segment_index = 0; segment_index < line.segments.size(); segment_index++) + { + LineSegment& segment = line.segments[segment_index]; + segment.x_position += (offset * segment_index) / (line.segments.size() - 1); + } + } +} + +Size SpanLayout::FindPreferredSize(Canvas* canvas) +{ + LayoutLines(canvas, 0x70000000); // Feed it with a very long length so it ends up on one line + return GetRect().size(); +} + +void SpanLayout::SetSelectionRange(std::string::size_type start, std::string::size_type end) +{ + sel_start = start; + sel_end = end; + if (sel_end < sel_start) + sel_end = sel_start; +} + +void SpanLayout::SetSelectionColors(const Colorf& foreground, const Colorf& background) +{ + sel_foreground = foreground; + sel_background = background; +} + +void SpanLayout::ShowCursor() +{ + cursor_visible = true; +} + +void SpanLayout::HideCursor() +{ + cursor_visible = false; +} + +void SpanLayout::SetCursorPos(std::string::size_type pos) +{ + cursor_pos = pos; +} + +void SpanLayout::SetCursorOverwriteMode(bool enable) +{ + cursor_overwrite_mode = enable; +} + +void SpanLayout::SetCursorColor(const Colorf& color) +{ + cursor_color = color; +} + +std::string SpanLayout::GetCombinedText() const +{ + return text; +} + +void SpanLayout::SetComponentGeometry() +{ + double x = position.x; + double y = position.y; + for (size_t i = 0; i < lines.size(); i++) + { + for (size_t j = 0; j < lines[i].segments.size(); j++) + { + if (lines[i].segments[j].type == object_component) + { + Point pos(x + lines[i].segments[j].x_position, y + lines[i].ascender - lines[i].segments[j].ascender); + Size size = lines[i].segments[j].component->GetSize(); + Rect rect(pos, size); + lines[i].segments[j].component->SetFrameGeometry(rect); + } + } + y += lines[i].height; + } +} + +double SpanLayout::GetFirstBaselineOffset() +{ + if (!lines.empty()) + { + return lines.front().ascender; + } + else + { + return 0; + } +} + +double SpanLayout::GetLastBaselineOffset() +{ + if (!lines.empty()) + { + double y = 0; + for (size_t i = 0; i + 1 < lines.size(); i++) + y += lines[i].height; + return y + lines.back().ascender; + } + else + { + return 0; + } +} diff --git a/libraries/ZWidget/src/core/theme.cpp b/libraries/ZWidget/src/core/theme.cpp new file mode 100644 index 000000000..cd553315c --- /dev/null +++ b/libraries/ZWidget/src/core/theme.cpp @@ -0,0 +1,412 @@ + +#include "core/theme.h" +#include "core/widget.h" +#include "core/canvas.h" + +void WidgetStyle::SetBool(const std::string& state, const std::string& propertyName, bool value) +{ + StyleProperties[state][propertyName] = value; +} + +void WidgetStyle::SetInt(const std::string& state, const std::string& propertyName, int value) +{ + StyleProperties[state][propertyName] = value; +} + +void WidgetStyle::SetDouble(const std::string& state, const std::string& propertyName, double value) +{ + StyleProperties[state][propertyName] = value; +} + +void WidgetStyle::SetString(const std::string& state, const std::string& propertyName, const std::string& value) +{ + StyleProperties[state][propertyName] = value; +} + +void WidgetStyle::SetColor(const std::string& state, const std::string& propertyName, const Colorf& value) +{ + StyleProperties[state][propertyName] = value; +} + +const WidgetStyle::PropertyVariant* WidgetStyle::FindProperty(const std::string& state, const std::string& propertyName) const +{ + const WidgetStyle* style = this; + do + { + // Look for property in the specific state + auto stateIt = style->StyleProperties.find(state); + if (stateIt != style->StyleProperties.end()) + { + auto it = stateIt->second.find(propertyName); + if (it != stateIt->second.end()) + return &it->second; + } + + // Fall back to the widget main style + if (state != std::string()) + { + stateIt = style->StyleProperties.find(std::string()); + if (stateIt != style->StyleProperties.end()) + { + auto it = stateIt->second.find(propertyName); + if (it != stateIt->second.end()) + return &it->second; + } + } + + style = style->ParentStyle; + } while (style); + return nullptr; +} + +bool WidgetStyle::GetBool(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : false; +} + +int WidgetStyle::GetInt(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : 0; +} + +double WidgetStyle::GetDouble(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : 0.0; +} + +std::string WidgetStyle::GetString(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : std::string(); +} + +Colorf WidgetStyle::GetColor(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : Colorf::transparent(); +} + +///////////////////////////////////////////////////////////////////////////// + +void BasicWidgetStyle::Paint(Widget* widget, Canvas* canvas, Size size) +{ + Colorf bgcolor = widget->GetStyleColor("background-color"); + if (bgcolor.a > 0.0f) + canvas->fillRect(Rect::xywh(0.0, 0.0, size.width, size.height), bgcolor); + + Colorf borderleft = widget->GetStyleColor("border-left-color"); + Colorf bordertop = widget->GetStyleColor("border-top-color"); + Colorf borderright = widget->GetStyleColor("border-right-color"); + Colorf borderbottom = widget->GetStyleColor("border-bottom-color"); + + double borderwidth = widget->GridFitSize(1.0); + + if (bordertop.a > 0.0f) + canvas->fillRect(Rect::xywh(0.0, 0.0, size.width, borderwidth), bordertop); + if (borderbottom.a > 0.0f) + canvas->fillRect(Rect::xywh(0.0, size.height - borderwidth, size.width, borderwidth), borderbottom); + if (borderleft.a > 0.0f) + canvas->fillRect(Rect::xywh(0.0, 0.0, borderwidth, size.height), borderleft); + if (borderright.a > 0.0f) + canvas->fillRect(Rect::xywh(size.width - borderwidth, 0.0, borderwidth, size.height), borderright); +} + +///////////////////////////////////////////////////////////////////////////// + +static std::unique_ptr CurrentTheme; + +WidgetStyle* WidgetTheme::RegisterStyle(std::unique_ptr widgetStyle, const std::string& widgetClass) +{ + auto& style = Styles[widgetClass]; + style = std::move(widgetStyle); + return style.get(); +} + +WidgetStyle* WidgetTheme::GetStyle(const std::string& widgetClass) +{ + auto it = Styles.find(widgetClass); + return it != Styles.end() ? it->second.get() : nullptr; +} + +void WidgetTheme::SetTheme(std::unique_ptr theme) +{ + CurrentTheme = std::move(theme); +} + +WidgetTheme* WidgetTheme::GetTheme() +{ + return CurrentTheme.get(); +} + +///////////////////////////////////////////////////////////////////////////// + +DarkWidgetTheme::DarkWidgetTheme() +{ + auto widget = RegisterStyle(std::make_unique(), "widget"); + auto textlabel = RegisterStyle(std::make_unique(widget), "textlabel"); + auto pushbutton = RegisterStyle(std::make_unique(widget), "pushbutton"); + auto lineedit = RegisterStyle(std::make_unique(widget), "lineedit"); + auto textedit = RegisterStyle(std::make_unique(widget), "textedit"); + auto listview = RegisterStyle(std::make_unique(widget), "listview"); + auto scrollbar = RegisterStyle(std::make_unique(widget), "scrollbar"); + auto tabbar = RegisterStyle(std::make_unique(widget), "tabbar"); + auto tabbar_tab = RegisterStyle(std::make_unique(widget), "tabbar-tab"); + auto tabbar_spacer = RegisterStyle(std::make_unique(widget), "tabbar-spacer"); + auto tabwidget_stack = RegisterStyle(std::make_unique(widget), "tabwidget-stack"); + auto checkbox_label = RegisterStyle(std::make_unique(widget), "checkbox-label"); + auto menubar = RegisterStyle(std::make_unique(widget), "menubar"); + auto menubaritem = RegisterStyle(std::make_unique(widget), "menubaritem"); + auto menu = RegisterStyle(std::make_unique(widget), "menu"); + auto menuitem = RegisterStyle(std::make_unique(widget), "menuitem"); + auto toolbar = RegisterStyle(std::make_unique(widget), "toolbar"); + auto toolbarbutton = RegisterStyle(std::make_unique(widget), "toolbarbutton"); + auto statusbar = RegisterStyle(std::make_unique(widget), "statusbar"); + + widget->SetString("font-family", "NotoSans"); + widget->SetColor("color", Colorf::fromRgba8(226, 223, 219)); + widget->SetColor("window-background", Colorf::fromRgba8(51, 51, 51)); + widget->SetColor("window-border", Colorf::fromRgba8(51, 51, 51)); + widget->SetColor("window-caption-color", Colorf::fromRgba8(33, 33, 33)); + widget->SetColor("window-caption-text-color", Colorf::fromRgba8(226, 223, 219)); + + pushbutton->SetDouble("noncontent-left", 10.0); + pushbutton->SetDouble("noncontent-top", 5.0); + pushbutton->SetDouble("noncontent-right", 10.0); + pushbutton->SetDouble("noncontent-bottom", 5.0); + pushbutton->SetColor("background-color", Colorf::fromRgba8(68, 68, 68)); + pushbutton->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + pushbutton->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + pushbutton->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + pushbutton->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + pushbutton->SetColor("hover", "background-color", Colorf::fromRgba8(78, 78, 78)); + pushbutton->SetColor("down", "background-color", Colorf::fromRgba8(88, 88, 88)); + + lineedit->SetDouble("noncontent-left", 5.0); + lineedit->SetDouble("noncontent-top", 3.0); + lineedit->SetDouble("noncontent-right", 5.0); + lineedit->SetDouble("noncontent-bottom", 3.0); + lineedit->SetColor("background-color", Colorf::fromRgba8(38, 38, 38)); + lineedit->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("selection-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("no-focus-selection-color", Colorf::fromRgba8(68, 68, 68)); + + textedit->SetDouble("noncontent-left", 8.0); + textedit->SetDouble("noncontent-top", 8.0); + textedit->SetDouble("noncontent-right", 8.0); + textedit->SetDouble("noncontent-bottom", 8.0); + textedit->SetColor("background-color", Colorf::fromRgba8(38, 38, 38)); + textedit->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + textedit->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + textedit->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + textedit->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + + listview->SetDouble("noncontent-left", 10.0); + listview->SetDouble("noncontent-top", 10.0); + listview->SetDouble("noncontent-right", 3.0); + listview->SetDouble("noncontent-bottom", 10.0); + listview->SetColor("background-color", Colorf::fromRgba8(38, 38, 38)); + listview->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + listview->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + listview->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + listview->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + listview->SetColor("selection-color", Colorf::fromRgba8(100, 100, 100)); + + scrollbar->SetColor("track-color", Colorf::fromRgba8(33, 33, 33)); + scrollbar->SetColor("thumb-color", Colorf::fromRgba8(58, 58, 58)); + + tabbar->SetDouble("spacer-left", 20.0); + tabbar->SetDouble("spacer-right", 20.0); + tabbar->SetColor("background-color", Colorf::fromRgba8(33, 33, 33)); + + tabbar_tab->SetDouble("noncontent-left", 15.0); + tabbar_tab->SetDouble("noncontent-right", 15.0); + tabbar_tab->SetDouble("noncontent-top", 1.0); + tabbar_tab->SetDouble("noncontent-bottom", 1.0); + tabbar_tab->SetColor("background-color", Colorf::fromRgba8(38, 38, 38)); + tabbar_tab->SetColor("border-left-color", Colorf::fromRgba8(68, 68, 68)); + tabbar_tab->SetColor("border-top-color", Colorf::fromRgba8(68, 68, 68)); + tabbar_tab->SetColor("border-right-color", Colorf::fromRgba8(68, 68, 68)); + tabbar_tab->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + tabbar_tab->SetColor("hover", "background-color", Colorf::fromRgba8(45, 45, 45)); + tabbar_tab->SetColor("active", "background-color", Colorf::fromRgba8(51, 51, 51)); + tabbar_tab->SetColor("active", "border-left-color", Colorf::fromRgba8(100, 100, 100)); + tabbar_tab->SetColor("active", "border-top-color", Colorf::fromRgba8(100, 100, 100)); + tabbar_tab->SetColor("active", "border-right-color", Colorf::fromRgba8(100, 100, 100)); + tabbar_tab->SetColor("active", "border-bottom-color", Colorf::transparent()); + + tabbar_spacer->SetDouble("noncontent-bottom", 1.0); + tabbar_spacer->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + + tabwidget_stack->SetDouble("noncontent-left", 20.0); + tabwidget_stack->SetDouble("noncontent-top", 5.0); + tabwidget_stack->SetDouble("noncontent-right", 20.0); + tabwidget_stack->SetDouble("noncontent-bottom", 5.0); + + checkbox_label->SetColor("checked-outer-border-color", Colorf::fromRgba8(100, 100, 100)); + checkbox_label->SetColor("checked-inner-border-color", Colorf::fromRgba8(51, 51, 51)); + checkbox_label->SetColor("checked-color", Colorf::fromRgba8(226, 223, 219)); + checkbox_label->SetColor("unchecked-outer-border-color", Colorf::fromRgba8(99, 99, 99)); + checkbox_label->SetColor("unchecked-inner-border-color", Colorf::fromRgba8(51, 51, 51)); + + menubar->SetColor("background-color", Colorf::fromRgba8(33, 33, 33)); + toolbar->SetColor("background-color", Colorf::fromRgba8(33, 33, 33)); + statusbar->SetColor("background-color", Colorf::fromRgba8(33, 33, 33)); + + toolbarbutton->SetColor("hover", "background-color", Colorf::fromRgba8(78, 78, 78)); + toolbarbutton->SetColor("down", "background-color", Colorf::fromRgba8(88, 88, 88)); + + menubaritem->SetColor("color", Colorf::fromRgba8(226, 223, 219)); + menubaritem->SetColor("hover", "background-color", Colorf::fromRgba8(78, 78, 78)); + menubaritem->SetColor("hover", "color", Colorf::fromRgba8(0, 0, 0)); + menubaritem->SetColor("down", "background-color", Colorf::fromRgba8(88, 88, 88)); + menubaritem->SetColor("down", "color", Colorf::fromRgba8(0, 0, 0)); + + menu->SetDouble("noncontent-left", 5.0); + menu->SetDouble("noncontent-top", 5.0); + menu->SetDouble("noncontent-right", 5.0); + menu->SetDouble("noncontent-bottom", 5.0); + menu->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + menu->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + menu->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + menu->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + + menuitem->SetColor("hover", "background-color", Colorf::fromRgba8(78, 78, 78)); + menuitem->SetColor("down", "background-color", Colorf::fromRgba8(88, 88, 88)); +} + +///////////////////////////////////////////////////////////////////////////// + +LightWidgetTheme::LightWidgetTheme() +{ + auto widget = RegisterStyle(std::make_unique(), "widget"); + auto textlabel = RegisterStyle(std::make_unique(widget), "textlabel"); + auto pushbutton = RegisterStyle(std::make_unique(widget), "pushbutton"); + auto lineedit = RegisterStyle(std::make_unique(widget), "lineedit"); + auto textedit = RegisterStyle(std::make_unique(widget), "textedit"); + auto listview = RegisterStyle(std::make_unique(widget), "listview"); + auto scrollbar = RegisterStyle(std::make_unique(widget), "scrollbar"); + auto tabbar = RegisterStyle(std::make_unique(widget), "tabbar"); + auto tabbar_tab = RegisterStyle(std::make_unique(widget), "tabbar-tab"); + auto tabbar_spacer = RegisterStyle(std::make_unique(widget), "tabbar-spacer"); + auto tabwidget_stack = RegisterStyle(std::make_unique(widget), "tabwidget-stack"); + auto checkbox_label = RegisterStyle(std::make_unique(widget), "checkbox-label"); + auto menubar = RegisterStyle(std::make_unique(widget), "menubar"); + auto menubaritem = RegisterStyle(std::make_unique(widget), "menubaritem"); + auto menu = RegisterStyle(std::make_unique(widget), "menu"); + auto menuitem = RegisterStyle(std::make_unique(widget), "menuitem"); + + widget->SetString("font-family", "NotoSans"); + widget->SetColor("color", Colorf::fromRgba8(0, 0, 0)); + widget->SetColor("window-background", Colorf::fromRgba8(240, 240, 240)); + widget->SetColor("window-border", Colorf::fromRgba8(100, 100, 100)); + widget->SetColor("window-caption-color", Colorf::fromRgba8(70, 70, 70)); + widget->SetColor("window-caption-text-color", Colorf::fromRgba8(226, 223, 219)); + + pushbutton->SetDouble("noncontent-left", 10.0); + pushbutton->SetDouble("noncontent-top", 5.0); + pushbutton->SetDouble("noncontent-right", 10.0); + pushbutton->SetDouble("noncontent-bottom", 5.0); + pushbutton->SetColor("background-color", Colorf::fromRgba8(210, 210, 210)); + pushbutton->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + pushbutton->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + pushbutton->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + pushbutton->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + pushbutton->SetColor("hover", "background-color", Colorf::fromRgba8(200, 200, 200)); + pushbutton->SetColor("down", "background-color", Colorf::fromRgba8(190, 190, 190)); + + lineedit->SetDouble("noncontent-left", 5.0); + lineedit->SetDouble("noncontent-top", 3.0); + lineedit->SetDouble("noncontent-right", 5.0); + lineedit->SetDouble("noncontent-bottom", 3.0); + lineedit->SetColor("background-color", Colorf::fromRgba8(255, 255, 255)); + lineedit->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + lineedit->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + lineedit->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + lineedit->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + lineedit->SetColor("selection-color", Colorf::fromRgba8(210, 210, 255)); + lineedit->SetColor("no-focus-selection-color", Colorf::fromRgba8(240, 240, 255)); + + textedit->SetDouble("noncontent-left", 8.0); + textedit->SetDouble("noncontent-top", 8.0); + textedit->SetDouble("noncontent-right", 8.0); + textedit->SetDouble("noncontent-bottom", 8.0); + textedit->SetColor("background-color", Colorf::fromRgba8(255, 255, 255)); + textedit->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + textedit->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + textedit->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + textedit->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + + listview->SetDouble("noncontent-left", 10.0); + listview->SetDouble("noncontent-top", 10.0); + listview->SetDouble("noncontent-right", 3.0); + listview->SetDouble("noncontent-bottom", 10.0); + listview->SetColor("background-color", Colorf::fromRgba8(230, 230, 230)); + listview->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + listview->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + listview->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + listview->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + listview->SetColor("selection-color", Colorf::fromRgba8(200, 200, 200)); + + scrollbar->SetColor("track-color", Colorf::fromRgba8(210, 210, 220)); + scrollbar->SetColor("thumb-color", Colorf::fromRgba8(180, 180, 180)); + + tabbar->SetDouble("spacer-left", 20.0); + tabbar->SetDouble("spacer-right", 20.0); + tabbar->SetColor("background-color", Colorf::fromRgba8(210, 210, 210)); + + tabbar_tab->SetDouble("noncontent-left", 15.0); + tabbar_tab->SetDouble("noncontent-right", 15.0); + tabbar_tab->SetDouble("noncontent-top", 1.0); + tabbar_tab->SetDouble("noncontent-bottom", 1.0); + tabbar_tab->SetColor("background-color", Colorf::fromRgba8(220, 220, 220)); + tabbar_tab->SetColor("border-left-color", Colorf::fromRgba8(200, 200, 200)); + tabbar_tab->SetColor("border-top-color", Colorf::fromRgba8(200, 200, 200)); + tabbar_tab->SetColor("border-right-color", Colorf::fromRgba8(200, 200, 200)); + tabbar_tab->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + tabbar_tab->SetColor("hover", "background-color", Colorf::fromRgba8(210, 210, 210)); + tabbar_tab->SetColor("active", "background-color", Colorf::fromRgba8(240, 240, 240)); + tabbar_tab->SetColor("active", "border-left-color", Colorf::fromRgba8(155, 155, 155)); + tabbar_tab->SetColor("active", "border-top-color", Colorf::fromRgba8(155, 155, 155)); + tabbar_tab->SetColor("active", "border-right-color", Colorf::fromRgba8(155, 155, 155)); + tabbar_tab->SetColor("active", "border-bottom-color", Colorf::transparent()); + + tabbar_spacer->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + + tabwidget_stack->SetDouble("noncontent-left", 20.0); + tabwidget_stack->SetDouble("noncontent-top", 5.0); + tabwidget_stack->SetDouble("noncontent-right", 20.0); + tabwidget_stack->SetDouble("noncontent-bottom", 5.0); + + checkbox_label->SetColor("checked-outer-border-color", Colorf::fromRgba8(155, 155, 155)); + checkbox_label->SetColor("checked-inner-border-color", Colorf::fromRgba8(200, 200, 200)); + checkbox_label->SetColor("checked-color", Colorf::fromRgba8(50, 50, 50)); + checkbox_label->SetColor("unchecked-outer-border-color", Colorf::fromRgba8(156, 156, 156)); + checkbox_label->SetColor("unchecked-inner-border-color", Colorf::fromRgba8(200, 200, 200)); + + menubar->SetColor("background-color", Colorf::fromRgba8(70, 70, 70)); + + menubaritem->SetColor("color", Colorf::fromRgba8(226, 223, 219)); + menubaritem->SetColor("hover", "background-color", Colorf::fromRgba8(200, 200, 200)); + menubaritem->SetColor("hover", "color", Colorf::fromRgba8(0, 0, 0)); + menubaritem->SetColor("down", "background-color", Colorf::fromRgba8(190, 190, 190)); + menubaritem->SetColor("down", "color", Colorf::fromRgba8(0, 0, 0)); + + menu->SetDouble("noncontent-left", 5.0); + menu->SetDouble("noncontent-top", 5.0); + menu->SetDouble("noncontent-right", 5.0); + menu->SetDouble("noncontent-bottom", 5.0); + menu->SetColor("background-color", Colorf::fromRgba8(255, 255, 255)); + menu->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + menu->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + menu->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + menu->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + + menuitem->SetColor("hover", "background-color", Colorf::fromRgba8(200, 200, 200)); + menuitem->SetColor("down", "background-color", Colorf::fromRgba8(190, 190, 190)); +} diff --git a/libraries/ZWidget/src/core/timer.cpp b/libraries/ZWidget/src/core/timer.cpp new file mode 100644 index 000000000..5ebe2612f --- /dev/null +++ b/libraries/ZWidget/src/core/timer.cpp @@ -0,0 +1,45 @@ + +#include "core/timer.h" +#include "core/widget.h" +#include "window/window.h" + +Timer::Timer(Widget* owner) : OwnerObj(owner) +{ + PrevTimerObj = owner->FirstTimerObj; + if (PrevTimerObj) + PrevTimerObj->PrevTimerObj = this; + owner->FirstTimerObj = this; +} + +Timer::~Timer() +{ + Stop(); + + if (PrevTimerObj) + PrevTimerObj->NextTimerObj = NextTimerObj; + if (NextTimerObj) + NextTimerObj->PrevTimerObj = PrevTimerObj; + if (OwnerObj->FirstTimerObj == this) + OwnerObj->FirstTimerObj = NextTimerObj; +} + +void Timer::Start(int timeoutMilliseconds, bool repeat) +{ + Stop(); + + TimerId = DisplayWindow::StartTimer(timeoutMilliseconds, [=]() { + if (!repeat) + Stop(); + if (FuncExpired) + FuncExpired(); + }); +} + +void Timer::Stop() +{ + if (TimerId != 0) + { + DisplayWindow::StopTimer(TimerId); + TimerId = 0; + } +} diff --git a/libraries/ZWidget/src/core/truetypefont.cpp b/libraries/ZWidget/src/core/truetypefont.cpp new file mode 100644 index 000000000..757929e3e --- /dev/null +++ b/libraries/ZWidget/src/core/truetypefont.cpp @@ -0,0 +1,1201 @@ + +// #define DUMP_GLYPH + +#include "core/truetypefont.h" +#include "core/pathfill.h" +#include +#include +#include + +#ifdef DUMP_GLYPH +#include +#endif + +TrueTypeFont::TrueTypeFont(std::shared_ptr initdata, int ttcFontIndex) : data(std::move(initdata)) +{ + if (data->size() > 0x7fffffff) + throw std::runtime_error("TTF file is larger than 2 gigabytes!"); + + TrueTypeFileReader reader(data->data(), data->size()); + ttf_Tag versionTag = reader.ReadTag(); + reader.Seek(0); + + if (memcmp(versionTag.data(), "ttcf", 4) == 0) // TTC header + { + ttcHeader.Load(reader); + if (ttcFontIndex >= (int)ttcHeader.numFonts) + throw std::runtime_error("TTC font index out of bounds"); + reader.Seek(ttcHeader.tableDirectoryOffsets[ttcFontIndex]); + } + + directory.Load(reader); + + if (!directory.ContainsTTFOutlines()) + throw std::runtime_error("Only truetype outline fonts are supported"); + + // Load required tables: + + reader = directory.GetReader(data->data(), data->size(), "head"); + head.Load(reader); + + reader = directory.GetReader(data->data(), data->size(), "hhea"); + hhea.Load(reader); + + reader = directory.GetReader(data->data(), data->size(), "maxp"); + maxp.Load(reader); + + reader = directory.GetReader(data->data(), data->size(), "hmtx"); + hmtx.Load(hhea, maxp, reader); + + reader = directory.GetReader(data->data(), data->size(), "name"); + name.Load(reader); + + reader = directory.GetReader(data->data(), data->size(), "OS/2"); + os2.Load(reader); + + reader = directory.GetReader(data->data(), data->size(), "cmap"); + cmap.Load(reader); + + LoadCharacterMapEncoding(reader); + + // Load TTF Outlines: + + reader = directory.GetReader(data->data(), data->size(), "loca"); + loca.Load(head, maxp, reader); + + glyf = directory.GetRecord("glyf"); + +#ifdef DUMP_GLYPH + LoadGlyph(GetGlyphIndex('6'), 13.0); +#endif +} + +TrueTypeTextMetrics TrueTypeFont::GetTextMetrics(double height) const +{ + double scale = height / head.unitsPerEm; + + TrueTypeTextMetrics metrics; + metrics.ascender = os2.sTypoAscender * scale; + metrics.descender = os2.sTypoDescender * scale; + metrics.lineGap = os2.sTypoLineGap * scale; + return metrics; +} + +TrueTypeGlyph TrueTypeFont::LoadGlyph(uint32_t glyphIndex, double height) const +{ + double scale = height / head.unitsPerEm; + double scaleX = 3.0f; + double scaleY = -1.0f; + + ttf_uint16 advanceWidth = 0; + ttf_int16 lsb = 0; + if (glyphIndex >= hhea.numberOfHMetrics) + { + advanceWidth = hmtx.hMetrics[hhea.numberOfHMetrics - 1].advanceWidth; + lsb = hmtx.leftSideBearings[glyphIndex - hhea.numberOfHMetrics]; + } + else + { + advanceWidth = hmtx.hMetrics[glyphIndex].advanceWidth; + lsb = hmtx.hMetrics[glyphIndex].lsb; + } + + // Glyph is missing if the offset is the same as the next glyph (0 bytes glyph length) + bool missing = glyphIndex + 1 < loca.offsets.size() ? loca.offsets[glyphIndex] == loca.offsets[glyphIndex + 1] : false; + if (missing) + { + TrueTypeGlyph glyph; + + // TBD: gridfit or not? + glyph.advanceWidth = (int)std::round(advanceWidth * scale * scaleX); + glyph.leftSideBearing = (int)std::round(lsb * scale * scaleX); + glyph.yOffset = 0; + + return glyph; + } + + TTF_SimpleGlyph g; + LoadGlyph(g, glyphIndex); + + // Create glyph path: + PathFillDesc path; + path.fill_mode = PathFillMode::winding; + + int startPoint = 0; + int numberOfContours = (int)g.endPtsOfContours.size(); + for (int i = 0; i < numberOfContours; i++) + { + int endPoint = g.endPtsOfContours[i]; + if (endPoint < startPoint) + throw std::runtime_error("Invalid glyph"); + + bool prevIsControlPoint; + bool isControlPoint = !(g.flags[endPoint] & TTF_ON_CURVE_POINT); + bool nextIsControlPoint = !(g.flags[startPoint] & TTF_ON_CURVE_POINT); + if (isControlPoint) + { + Point nextpoint(g.points[startPoint].x, g.points[startPoint].y); + if (nextIsControlPoint) + { + Point curpoint(g.points[endPoint].x, g.points[endPoint].y); + Point midpoint = (curpoint + nextpoint) / 2; + path.MoveTo(midpoint * scale); + prevIsControlPoint = isControlPoint; + } + else + { + path.MoveTo(Point(g.points[startPoint].x, g.points[startPoint].y) * scale); + prevIsControlPoint = false; + } + } + else + { + path.MoveTo(Point(g.points[endPoint].x, g.points[endPoint].y) * scale); + prevIsControlPoint = isControlPoint; + } + + int pos = startPoint; + while (pos <= endPoint) + { + int nextpos = pos + 1 <= endPoint ? pos + 1 : startPoint; + bool isControlPoint = !(g.flags[pos] & TTF_ON_CURVE_POINT); + bool nextIsControlPoint = !(g.flags[nextpos] & TTF_ON_CURVE_POINT); + Point curpoint(g.points[pos].x, g.points[pos].y); + if (isControlPoint) + { + Point nextpoint(g.points[nextpos].x, g.points[nextpos].y); + if (nextIsControlPoint) + { + Point midpoint = (curpoint + nextpoint) / 2; + path.BezierTo(curpoint * scale, midpoint * scale); + prevIsControlPoint = isControlPoint; + pos++; + } + else + { + path.BezierTo(curpoint * scale, nextpoint * scale); + prevIsControlPoint = false; + pos += 2; + } + } + else + { + if (!prevIsControlPoint) + path.LineTo(curpoint * scale); + prevIsControlPoint = isControlPoint; + pos++; + } + } + path.Close(); + + startPoint = endPoint + 1; + } + + // Transform and find the final bounding box + Point bboxMin, bboxMax; + if (!path.subpaths.front().points.empty()) + { + bboxMin = path.subpaths.front().points.front(); + bboxMax = path.subpaths.front().points.front(); + bboxMin.x *= scaleX; + bboxMin.y *= scaleY; + bboxMax.x *= scaleX; + bboxMax.y *= scaleY; + for (auto& subpath : path.subpaths) + { + for (auto& point : subpath.points) + { + point.x *= scaleX; + point.y *= scaleY; + bboxMin.x = std::min(bboxMin.x, point.x); + bboxMin.y = std::min(bboxMin.y, point.y); + bboxMax.x = std::max(bboxMax.x, point.x); + bboxMax.y = std::max(bboxMax.y, point.y); + } + } + } + + bboxMin.x = std::floor(bboxMin.x); + bboxMin.y = std::floor(bboxMin.y); + + // Reposition glyph to bitmap so it begins at (0,0) for our bitmap + for (auto& subpath : path.subpaths) + { + for (auto& point : subpath.points) + { + point.x -= bboxMin.x; + point.y -= bboxMin.y; + } + } + +#ifdef DUMP_GLYPH + std::string svgxmlstart = R"( + + + +)"; + + std::ofstream out("c:\\development\\glyph.svg"); + out << svgxmlstart; + + for (auto& subpath : path.subpaths) + { + size_t pos = 0; + out << "M" << subpath.points[pos].x << " " << subpath.points[pos].y << " "; + pos++; + for (PathFillCommand cmd : subpath.commands) + { + int count = 0; + if (cmd == PathFillCommand::line) + { + out << "L"; + count = 1; + } + else if (cmd == PathFillCommand::quadradic) + { + out << "Q"; + count = 2; + } + else if (cmd == PathFillCommand::cubic) + { + out << "C"; + count = 3; + } + + for (int i = 0; i < count; i++) + { + out << subpath.points[pos].x << " " << subpath.points[pos].y << " "; + pos++; + } + } + if (subpath.closed) + out << "Z"; + } + + out << svgxmlend; + out.close(); +#endif + + TrueTypeGlyph glyph; + + // Rasterize the glyph + glyph.width = (int)std::floor(bboxMax.x - bboxMin.x) + 1; + glyph.height = (int)std::floor(bboxMax.y - bboxMin.y) + 1; + glyph.grayscale.reset(new uint8_t[glyph.width * glyph.height]); + uint8_t* grayscale = glyph.grayscale.get(); + path.Rasterize(grayscale, glyph.width, glyph.height); + + // TBD: gridfit or not? + glyph.advanceWidth = (int)std::round(advanceWidth * scale * scaleX); + glyph.leftSideBearing = (int)std::round(bboxMin.x); + glyph.yOffset = (int)std::round(bboxMin.y); + + return glyph; +} + +void TrueTypeFont::LoadGlyph(TTF_SimpleGlyph& g, uint32_t glyphIndex, int compositeDepth) const +{ + if (glyphIndex >= loca.offsets.size()) + throw std::runtime_error("Glyph index out of bounds"); + + TrueTypeFileReader reader = glyf.GetReader(data->data(), data->size()); + reader.Seek(loca.offsets[glyphIndex]); + + ttf_int16 numberOfContours = reader.ReadInt16(); + ttf_int16 xMin = reader.ReadInt16(); + ttf_int16 yMin = reader.ReadInt16(); + ttf_int16 xMax = reader.ReadInt16(); + ttf_int16 yMax = reader.ReadInt16(); + + if (numberOfContours > 0) // Simple glyph + { + int pointsOffset = (int)g.points.size(); + for (ttf_uint16 i = 0; i < numberOfContours; i++) + g.endPtsOfContours.push_back(pointsOffset + reader.ReadUInt16()); + + ttf_uint16 instructionLength = reader.ReadUInt16(); + std::vector instructions; + instructions.resize(instructionLength); + reader.Read(instructions.data(), instructions.size()); + + int numPoints = (int)g.endPtsOfContours.back() - pointsOffset + 1; + g.points.resize(pointsOffset + numPoints); + + while (g.flags.size() < g.points.size()) + { + ttf_uint8 flag = reader.ReadUInt8(); + if (flag & TTF_REPEAT_FLAG) + { + ttf_uint8 repeatcount = reader.ReadUInt8(); + for (ttf_uint8 i = 0; i < repeatcount; i++) + g.flags.push_back(flag); + } + g.flags.push_back(flag); + } + + for (int i = pointsOffset; i < pointsOffset + numPoints; i++) + { + if (g.flags[i] & TTF_X_SHORT_VECTOR) + { + ttf_int16 x = reader.ReadUInt8(); + g.points[i].x = (float)((g.flags[i] & TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR) ? x : -x); + } + else if (g.flags[i] & TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR) + { + g.points[i].x = 0.0f; + } + else + { + g.points[i].x = (float)reader.ReadInt16(); + } + } + + for (int i = pointsOffset; i < pointsOffset + numPoints; i++) + { + if (g.flags[i] & TTF_Y_SHORT_VECTOR) + { + ttf_int16 y = reader.ReadUInt8(); + g.points[i].y = (float)((g.flags[i] & TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR) ? y : -y); + } + else if (g.flags[i] & TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR) + { + g.points[i].y = 0.0f; + } + else + { + g.points[i].y = (float)reader.ReadInt16(); + } + } + + // Convert from relative coordinates to absolute + for (int i = pointsOffset + 1; i < pointsOffset + numPoints; i++) + { + g.points[i].x += g.points[i - 1].x; + g.points[i].y += g.points[i - 1].y; + } + } + else if (numberOfContours < 0) // Composite glyph + { + if (compositeDepth == 8) + throw std::runtime_error("Composite glyph recursion exceeded"); + + int parentPointsOffset = (int)g.points.size(); + + bool weHaveInstructions = false; + while (true) + { + ttf_uint16 flags = reader.ReadUInt16(); + ttf_uint16 childGlyphIndex = reader.ReadUInt16(); + + int argument1, argument2; + if (flags & TTF_ARG_1_AND_2_ARE_WORDS) + { + if (flags & TTF_ARGS_ARE_XY_VALUES) + { + argument1 = reader.ReadInt16(); + argument2 = reader.ReadInt16(); + } + else + { + argument1 = reader.ReadUInt16(); + argument2 = reader.ReadUInt16(); + } + } + else + { + if (flags & TTF_ARGS_ARE_XY_VALUES) + { + argument1 = reader.ReadInt8(); + argument2 = reader.ReadInt8(); + } + else + { + argument1 = reader.ReadUInt8(); + argument2 = reader.ReadUInt8(); + } + } + + float mat2x2[4]; + bool transform = true; + if (flags & TTF_WE_HAVE_A_SCALE) + { + float scale = F2DOT14_ToFloat(reader.ReadF2DOT14()); + mat2x2[0] = scale; + mat2x2[1] = 0; + mat2x2[2] = 0; + mat2x2[3] = scale; + } + else if (flags & TTF_WE_HAVE_AN_X_AND_Y_SCALE) + { + mat2x2[0] = F2DOT14_ToFloat(reader.ReadF2DOT14()); + mat2x2[1] = 0; + mat2x2[2] = 0; + mat2x2[3] = F2DOT14_ToFloat(reader.ReadF2DOT14()); + } + else if (flags & TTF_WE_HAVE_A_TWO_BY_TWO) + { + mat2x2[0] = F2DOT14_ToFloat(reader.ReadF2DOT14()); + mat2x2[1] = F2DOT14_ToFloat(reader.ReadF2DOT14()); + mat2x2[2] = F2DOT14_ToFloat(reader.ReadF2DOT14()); + mat2x2[3] = F2DOT14_ToFloat(reader.ReadF2DOT14()); + } + else + { + transform = false; + } + + int childPointsOffset = (int)g.points.size(); + LoadGlyph(g, childGlyphIndex, compositeDepth + 1); + + if (transform) + { + for (int i = childPointsOffset; i < g.points.size(); i++) + { + float x = g.points[i].x * mat2x2[0] + g.points[i].y * mat2x2[1]; + float y = g.points[i].x * mat2x2[2] + g.points[i].y * mat2x2[3]; + g.points[i].x = x; + g.points[i].y = y; + } + } + + float dx, dy; + + if (flags & TTF_ARGS_ARE_XY_VALUES) + { + dx = (float)argument1; + dy = (float)argument2; + + // Spec states we must fall back to TTF_UNSCALED_COMPONENT_OFFSET if both flags are set + if ((flags & (TTF_SCALED_COMPONENT_OFFSET | TTF_UNSCALED_COMPONENT_OFFSET)) == TTF_SCALED_COMPONENT_OFFSET) + { + float x = dx * mat2x2[0] + dy * mat2x2[1]; + float y = dx * mat2x2[2] + dy * mat2x2[3]; + dx = x; + dy = y; + } + + if (flags & TTF_ROUND_XY_TO_GRID) + { + // To do: round the offset to the pixel grid + } + } + else + { + int parentPointIndex = parentPointsOffset + argument1; + int childPointIndex = childPointsOffset + argument2; + + if ((size_t)parentPointIndex >= g.points.size() || (size_t)childPointIndex >= g.points.size()) + throw std::runtime_error("Invalid glyph offset"); + + dx = g.points[parentPointIndex].x - g.points[childPointIndex].x; + dy = g.points[parentPointIndex].y - g.points[childPointIndex].y; + } + + for (int i = childPointsOffset; i < g.points.size(); i++) + { + g.points[i].x += dx; + g.points[i].y += dy; + } + + if (flags & TTF_USE_MY_METRICS) + { + // To do: this affects lsb + rsb calculations somehow + } + + if (flags & TTF_WE_HAVE_INSTRUCTIONS) + { + weHaveInstructions = true; + } + + if (!(flags & TTF_MORE_COMPONENTS)) + break; + } + + if (weHaveInstructions) + { + ttf_uint16 instructionLength = reader.ReadUInt16(); + std::vector instructions; + instructions.resize(instructionLength); + reader.Read(instructions.data(), instructions.size()); + } + } +} + +float TrueTypeFont::F2DOT14_ToFloat(ttf_F2DOT14 v) +{ + int a = ((ttf_int16)v) >> 14; + int b = (v & 0x3fff); + return a + b / (float)0x4000; +} + +uint32_t TrueTypeFont::GetGlyphIndex(uint32_t c) const +{ + auto it = std::lower_bound(Ranges.begin(), Ranges.end(), c, [](const TTF_GlyphRange& range, uint32_t c) { return range.endCharCode < c; }); + if (it != Ranges.end() && c >= it->startCharCode && c <= it->endCharCode) + { + return it->startGlyphID + (c - it->startCharCode); + } + + it = std::lower_bound(ManyToOneRanges.begin(), ManyToOneRanges.end(), c, [](const TTF_GlyphRange& range, uint32_t c) { return range.endCharCode < c; }); + if (it != ManyToOneRanges.end() && c >= it->startCharCode && c <= it->endCharCode) + { + return it->startGlyphID; + } + return 0; +} + +void TrueTypeFont::LoadCharacterMapEncoding(TrueTypeFileReader& reader) +{ + // Look for the best encoding available that we support + + TTF_EncodingRecord record; + if (!record.subtableOffset) record = cmap.GetEncoding(3, 12); + if (!record.subtableOffset) record = cmap.GetEncoding(0, 4); + if (!record.subtableOffset) record = cmap.GetEncoding(3, 1); + if (!record.subtableOffset) record = cmap.GetEncoding(0, 3); + if (!record.subtableOffset) + throw std::runtime_error("No supported cmap encoding found in truetype file"); + + reader.Seek(record.subtableOffset); + + ttf_uint16 format = reader.ReadUInt16(); + if (format == 4) + { + TTF_CMapSubtable4 subformat; + subformat.Load(reader); + + for (uint16_t i = 0; i < subformat.segCount; i++) + { + ttf_uint16 startCode = subformat.startCode[i]; + ttf_uint16 endCode = subformat.endCode[i]; + ttf_uint16 idDelta = subformat.idDelta[i]; + ttf_uint16 idRangeOffset = subformat.idRangeOffsets[i]; + if (idRangeOffset == 0) + { + ttf_uint16 glyphId = startCode + idDelta; // Note: relies on modulo 65536 + + TTF_GlyphRange range; + range.startCharCode = startCode; + range.endCharCode = endCode; + range.startGlyphID = glyphId; + Ranges.push_back(range); + } + else if (startCode <= endCode) + { + TTF_GlyphRange range; + range.startCharCode = startCode; + bool firstGlyph = true; + for (ttf_uint16 c = startCode; c <= endCode; c++) + { + int offset = idRangeOffset / 2 + (c - startCode) - ((int)subformat.segCount - i); + if (offset >= 0 && offset < subformat.glyphIdArray.size()) + { + int glyphId = subformat.glyphIdArray[offset]; + if (firstGlyph) + { + range.startGlyphID = glyphId; + firstGlyph = false; + } + else if (range.startGlyphID + (c - range.startCharCode) != glyphId) + { + range.endCharCode = c - 1; + Ranges.push_back(range); + range.startCharCode = c; + range.startGlyphID = glyphId; + } + } + } + if (!firstGlyph) + { + range.endCharCode = endCode; + Ranges.push_back(range); + } + } + } + } + else if (format == 12 || format == 3) + { + TTF_CMapSubtable12 subformat; + subformat.Load(reader); + Ranges = std::move(subformat.groups); + } + else if (format == 13 || format == 3) + { + TTF_CMapSubtable13 subformat; + subformat.Load(reader); + ManyToOneRanges = std::move(subformat.groups); + } +} + +std::vector TrueTypeFont::GetFontNames(const std::shared_ptr& data) +{ + if (data->size() > 0x7fffffff) + throw std::runtime_error("TTF file is larger than 2 gigabytes!"); + + TrueTypeFileReader reader(data->data(), data->size()); + ttf_Tag versionTag = reader.ReadTag(); + reader.Seek(0); + + std::vector names; + if (memcmp(versionTag.data(), "ttcf", 4) == 0) // TTC header + { + TTC_Header ttcHeader; + ttcHeader.Load(reader); + + for (size_t i = 0; i < ttcHeader.tableDirectoryOffsets.size(); i++) + { + reader.Seek(ttcHeader.tableDirectoryOffsets[i]); + + TTF_TableDirectory directory; + directory.Load(reader); + + TTF_NamingTable name; + auto name_reader = directory.GetReader(data->data(), data->size(), "name"); + name.Load(name_reader); + + names.push_back(name.GetFontName()); + } + } + else + { + TTF_TableDirectory directory; + directory.Load(reader); + + TTF_NamingTable name; + auto name_reader = directory.GetReader(data->data(), data->size(), "name"); + name.Load(name_reader); + + names.push_back(name.GetFontName()); + } + return names; +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_CMapSubtable0::Load(TrueTypeFileReader& reader) +{ + length = reader.ReadUInt16(); + language = reader.ReadUInt16(); + glyphIdArray.resize(256); + reader.Read(glyphIdArray.data(), glyphIdArray.size()); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_CMapSubtable4::Load(TrueTypeFileReader& reader) +{ + length = reader.ReadUInt16(); + language = reader.ReadUInt16(); + + segCount = reader.ReadUInt16() / 2; + ttf_uint16 searchRange = reader.ReadUInt16(); + ttf_uint16 entrySelector = reader.ReadUInt16(); + ttf_uint16 rangeShift = reader.ReadUInt16(); + + endCode.reserve(segCount); + startCode.reserve(segCount); + idDelta.reserve(segCount); + idRangeOffsets.reserve(segCount); + for (ttf_uint16 i = 0; i < segCount; i++) endCode.push_back(reader.ReadUInt16()); + reservedPad = reader.ReadUInt16(); + for (ttf_uint16 i = 0; i < segCount; i++) startCode.push_back(reader.ReadUInt16()); + for (ttf_uint16 i = 0; i < segCount; i++) idDelta.push_back(reader.ReadInt16()); + for (ttf_uint16 i = 0; i < segCount; i++) idRangeOffsets.push_back(reader.ReadUInt16()); + + int glyphIdArraySize = ((int)length - (8 + (int)segCount * 4) * sizeof(ttf_uint16)) / 2; + if (glyphIdArraySize < 0) + throw std::runtime_error("Invalid TTF cmap subtable 4 length"); + glyphIdArray.reserve(glyphIdArraySize); + for (int i = 0; i < glyphIdArraySize; i++) glyphIdArray.push_back(reader.ReadUInt16()); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_CMapSubtable12::Load(TrueTypeFileReader& reader) +{ + reserved = reader.ReadUInt16(); + length = reader.ReadUInt32(); + language = reader.ReadUInt32(); + numGroups = reader.ReadUInt32(); + for (ttf_uint32 i = 0; i < numGroups; i++) + { + TTF_GlyphRange range; + range.startCharCode = reader.ReadUInt32(); + range.endCharCode = reader.ReadUInt32(); + range.startGlyphID = reader.ReadUInt32(); + groups.push_back(range); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_TableRecord::Load(TrueTypeFileReader& reader) +{ + tableTag = reader.ReadTag(); + checksum = reader.ReadUInt32(); + offset = reader.ReadOffset32(); + length = reader.ReadUInt32(); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_TableDirectory::Load(TrueTypeFileReader& reader) +{ + sfntVersion = reader.ReadUInt32(); + numTables = reader.ReadUInt16(); + + // opentype spec says we can't use these for security reasons, so we pretend they never was part of the header + ttf_uint16 searchRange = reader.ReadUInt16(); + ttf_uint16 entrySelector = reader.ReadUInt16(); + ttf_uint16 rangeShift = reader.ReadUInt16(); + + for (ttf_uint16 i = 0; i < numTables; i++) + { + TTF_TableRecord record; + record.Load(reader); + tableRecords.push_back(record); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTC_Header::Load(TrueTypeFileReader& reader) +{ + ttcTag = reader.ReadTag(); + majorVersion = reader.ReadUInt16(); + minorVersion = reader.ReadUInt16(); + + if (!(majorVersion == 1 && minorVersion == 0) && !(majorVersion == 2 && minorVersion == 0)) + throw std::runtime_error("Unsupported TTC header version"); + + numFonts = reader.ReadUInt32(); + for (ttf_uint16 i = 0; i < numFonts; i++) + { + tableDirectoryOffsets.push_back(reader.ReadOffset32()); + } + + if (majorVersion == 2 && minorVersion == 0) + { + dsigTag = reader.ReadUInt32(); + dsigLength = reader.ReadUInt32(); + dsigOffset = reader.ReadUInt32(); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_CMap::Load(TrueTypeFileReader& reader) +{ + version = reader.ReadUInt16(); + numTables = reader.ReadUInt16(); + + for (ttf_uint16 i = 0; i < numTables; i++) + { + TTF_EncodingRecord record; + record.platformID = reader.ReadUInt16(); + record.encodingID = reader.ReadUInt16(); + record.subtableOffset = reader.ReadOffset32(); + encodingRecords.push_back(record); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_FontHeader::Load(TrueTypeFileReader& reader) +{ + majorVersion = reader.ReadUInt16(); + minorVersion = reader.ReadUInt16(); + fontRevision = reader.ReadFixed(); + checksumAdjustment = reader.ReadUInt32(); + magicNumber = reader.ReadUInt32(); + flags = reader.ReadUInt16(); + unitsPerEm = reader.ReadUInt16(); + created = reader.ReadLONGDATETIME(); + modified = reader.ReadLONGDATETIME(); + xMin = reader.ReadInt16(); + yMin = reader.ReadInt16(); + xMax = reader.ReadInt16(); + yMax = reader.ReadInt16(); + macStyle = reader.ReadUInt16(); + lowestRecPPEM = reader.ReadUInt16(); + fontDirectionHint = reader.ReadInt16(); + indexToLocFormat = reader.ReadInt16(); + glyphDataFormat = reader.ReadInt16(); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_HorizontalHeader::Load(TrueTypeFileReader& reader) +{ + majorVersion = reader.ReadUInt16(); + minorVersion = reader.ReadUInt16(); + ascender = reader.ReadFWORD(); + descender = reader.ReadFWORD(); + lineGap = reader.ReadFWORD(); + advanceWidthMax = reader.ReadUFWORD(); + minLeftSideBearing = reader.ReadFWORD(); + minRightSideBearing = reader.ReadFWORD(); + xMaxExtent = reader.ReadFWORD(); + caretSlopeRise = reader.ReadInt16(); + caretSlopeRun = reader.ReadInt16(); + caretOffset = reader.ReadInt16(); + reserved0 = reader.ReadInt16(); + reserved1 = reader.ReadInt16(); + reserved2 = reader.ReadInt16(); + reserved3 = reader.ReadInt16(); + metricDataFormat = reader.ReadInt16(); + numberOfHMetrics = reader.ReadUInt16(); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_HorizontalMetrics::Load(const TTF_HorizontalHeader& hhea, const TTF_MaximumProfile& maxp, TrueTypeFileReader& reader) +{ + for (ttf_uint16 i = 0; i < hhea.numberOfHMetrics; i++) + { + longHorMetric metric; + metric.advanceWidth = reader.ReadUInt16(); + metric.lsb = reader.ReadInt16(); + hMetrics.push_back(metric); + } + + int count = (int)maxp.numGlyphs - (int)hhea.numberOfHMetrics; + if (count < 0) + throw std::runtime_error("Invalid TTF file"); + + for (int i = 0; i < count; i++) + { + leftSideBearings.push_back(reader.ReadInt16()); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_MaximumProfile::Load(TrueTypeFileReader& reader) +{ + version = reader.ReadVersion16Dot16(); + numGlyphs = reader.ReadUInt16(); + + if (version == (1 << 16)) // v1 only + { + maxPoints = reader.ReadUInt16(); + maxContours = reader.ReadUInt16(); + maxCompositePoints = reader.ReadUInt16(); + maxCompositeContours = reader.ReadUInt16(); + maxZones = reader.ReadUInt16(); + maxTwilightPoints = reader.ReadUInt16(); + maxStorage = reader.ReadUInt16(); + maxFunctionDefs = reader.ReadUInt16(); + maxInstructionDefs = reader.ReadUInt16(); + maxStackElements = reader.ReadUInt16(); + maxSizeOfInstructions = reader.ReadUInt16(); + maxComponentElements = reader.ReadUInt16(); + maxComponentDepth = reader.ReadUInt16(); + } +} + +///////////////////////////////////////////////////////////////////////////// + +static std::string unicode_to_utf8(unsigned int value) +{ + char text[8]; + + if ((value < 0x80) && (value > 0)) + { + text[0] = (char)value; + text[1] = 0; + } + else if (value < 0x800) + { + text[0] = (char)(0xc0 | (value >> 6)); + text[1] = (char)(0x80 | (value & 0x3f)); + text[2] = 0; + } + else if (value < 0x10000) + { + text[0] = (char)(0xe0 | (value >> 12)); + text[1] = (char)(0x80 | ((value >> 6) & 0x3f)); + text[2] = (char)(0x80 | (value & 0x3f)); + text[3] = 0; + } + else if (value < 0x200000) + { + text[0] = (char)(0xf0 | (value >> 18)); + text[1] = (char)(0x80 | ((value >> 12) & 0x3f)); + text[2] = (char)(0x80 | ((value >> 6) & 0x3f)); + text[3] = (char)(0x80 | (value & 0x3f)); + text[4] = 0; + } + else if (value < 0x4000000) + { + text[0] = (char)(0xf8 | (value >> 24)); + text[1] = (char)(0x80 | ((value >> 18) & 0x3f)); + text[2] = (char)(0x80 | ((value >> 12) & 0x3f)); + text[3] = (char)(0x80 | ((value >> 6) & 0x3f)); + text[4] = (char)(0x80 | (value & 0x3f)); + text[5] = 0; + } + else if (value < 0x80000000) + { + text[0] = (char)(0xfc | (value >> 30)); + text[1] = (char)(0x80 | ((value >> 24) & 0x3f)); + text[2] = (char)(0x80 | ((value >> 18) & 0x3f)); + text[3] = (char)(0x80 | ((value >> 12) & 0x3f)); + text[4] = (char)(0x80 | ((value >> 6) & 0x3f)); + text[5] = (char)(0x80 | (value & 0x3f)); + text[6] = 0; + } + else + { + text[0] = 0; // Invalid wchar value + } + return text; +} + +void TTF_NamingTable::Load(TrueTypeFileReader& reader) +{ + version = reader.ReadUInt16(); + count = reader.ReadUInt16(); + storageOffset = reader.ReadOffset16(); + for (ttf_uint16 i = 0; i < count; i++) + { + NameRecord record; + record.platformID = reader.ReadUInt16(); + record.encodingID = reader.ReadUInt16(); + record.languageID = reader.ReadUInt16(); + record.nameID = reader.ReadUInt16(); + record.length = reader.ReadUInt16(); + record.stringOffset = reader.ReadOffset16(); + nameRecord.push_back(record); + } + + if (version == 1) + { + langTagCount = reader.ReadUInt16(); + for (ttf_uint16 i = 0; i < langTagCount; i++) + { + LangTagRecord record; + record.length = reader.ReadUInt16(); + record.langTagOffset = reader.ReadOffset16(); + langTagRecord.push_back(record); + } + } + + for (NameRecord& record : nameRecord) + { + if (record.length > 0 && record.platformID == 3 && record.encodingID == 1) + { + reader.Seek(storageOffset + record.stringOffset); + ttf_uint16 ucs2len = record.length / 2; + for (ttf_uint16 i = 0; i < ucs2len; i++) + { + record.text += unicode_to_utf8(reader.ReadUInt16()); + } + } + } +} + +TTCFontName TTF_NamingTable::GetFontName() const +{ + TTCFontName fname; + for (const auto& record : nameRecord) + { + if (record.nameID == 1) fname.FamilyName = record.text; + if (record.nameID == 2) fname.SubfamilyName = record.text; + if (record.nameID == 3) fname.UniqueID = record.text; + if (record.nameID == 4) fname.FullName = record.text; + if (record.nameID == 5) fname.VersionString = record.text; + if (record.nameID == 6) fname.PostscriptName = record.text; + } + return fname; +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_OS2Windows::Load(TrueTypeFileReader& reader) +{ + version = reader.ReadUInt16(); + xAvgCharWidth = reader.ReadInt16(); + usWeightClass = reader.ReadUInt16(); + usWidthClass = reader.ReadUInt16(); + fsType = reader.ReadUInt16(); + ySubscriptXSize = reader.ReadInt16(); + ySubscriptYSize = reader.ReadInt16(); + ySubscriptXOffset = reader.ReadInt16(); + ySubscriptYOffset = reader.ReadInt16(); + ySuperscriptXSize = reader.ReadInt16(); + ySuperscriptYSize = reader.ReadInt16(); + ySuperscriptXOffset = reader.ReadInt16(); + ySuperscriptYOffset = reader.ReadInt16(); + yStrikeoutSize = reader.ReadInt16(); + yStrikeoutPosition = reader.ReadInt16(); + sFamilyClass = reader.ReadInt16(); + for (int i = 0; i < 10; i++) + { + panose[i] = reader.ReadUInt8(); + } + ulUnicodeRange1 = reader.ReadUInt32(); + ulUnicodeRange2 = reader.ReadUInt32(); + ulUnicodeRange3 = reader.ReadUInt32(); + ulUnicodeRange4 = reader.ReadUInt32(); + achVendID = reader.ReadTag(); + fsSelection = reader.ReadUInt16(); + usFirstCharIndex = reader.ReadUInt16(); + usLastCharIndex = reader.ReadUInt16(); + sTypoAscender = reader.ReadInt16(); + sTypoDescender = reader.ReadInt16(); + sTypoLineGap = reader.ReadInt16(); + if (!reader.IsEndOfData()) // may be missing in v0 due to a bug in Apple's TTF documentation + { + usWinAscent = reader.ReadUInt16(); + usWinDescent = reader.ReadUInt16(); + } + if (version >= 1) + { + ulCodePageRange1 = reader.ReadUInt32(); + ulCodePageRange2 = reader.ReadUInt32(); + } + if (version >= 2) + { + sxHeight = reader.ReadInt16(); + sCapHeight = reader.ReadInt16(); + usDefaultChar = reader.ReadUInt16(); + usBreakChar = reader.ReadUInt16(); + usMaxContext = reader.ReadUInt16(); + } + if (version >= 5) + { + usLowerOpticalPointSize = reader.ReadUInt16(); + usUpperOpticalPointSize = reader.ReadUInt16(); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_IndexToLocation::Load(const TTF_FontHeader& head, const TTF_MaximumProfile& maxp, TrueTypeFileReader& reader) +{ + int count = (int)maxp.numGlyphs + 1; + if (head.indexToLocFormat == 0) + { + offsets.reserve(count); + for (int i = 0; i < count; i++) + { + offsets.push_back((ttf_Offset32)reader.ReadOffset16() * 2); + } + } + else + { + offsets.reserve(count); + for (int i = 0; i < count; i++) + { + offsets.push_back(reader.ReadOffset32()); + } + } +} + +///////////////////////////////////////////////////////////////////////////// + +ttf_uint8 TrueTypeFileReader::ReadUInt8() +{ + ttf_uint8 v; Read(&v, 1); return v; +} + +ttf_uint16 TrueTypeFileReader::ReadUInt16() +{ + ttf_uint8 v[2]; Read(v, 2); return (((ttf_uint16)v[0]) << 8) | (ttf_uint16)v[1]; +} + +ttf_uint24 TrueTypeFileReader::ReadUInt24() +{ + ttf_uint8 v[3]; Read(v, 3); return (((ttf_uint32)v[0]) << 16) | (((ttf_uint32)v[1]) << 8) | (ttf_uint32)v[2]; +} + +ttf_uint32 TrueTypeFileReader::ReadUInt32() +{ + ttf_uint8 v[4]; Read(v, 4); return (((ttf_uint32)v[0]) << 24) | (((ttf_uint32)v[1]) << 16) | (((ttf_uint32)v[2]) << 8) | (ttf_uint32)v[3]; +} + +ttf_int8 TrueTypeFileReader::ReadInt8() +{ + return ReadUInt8(); +} + +ttf_int16 TrueTypeFileReader::ReadInt16() +{ + return ReadUInt16(); +} + +ttf_int32 TrueTypeFileReader::ReadInt32() +{ + return ReadUInt32(); +} + +ttf_Fixed TrueTypeFileReader::ReadFixed() +{ + return ReadUInt32(); +} + +ttf_UFWORD TrueTypeFileReader::ReadUFWORD() +{ + return ReadUInt16(); +} + +ttf_FWORD TrueTypeFileReader::ReadFWORD() +{ + return ReadUInt16(); +} + +ttf_F2DOT14 TrueTypeFileReader::ReadF2DOT14() +{ + return ReadUInt16(); +} + +ttf_LONGDATETIME TrueTypeFileReader::ReadLONGDATETIME() +{ + ttf_uint8 v[8]; Read(v, 8); + return + (((ttf_LONGDATETIME)v[0]) << 56) | (((ttf_LONGDATETIME)v[1]) << 48) | (((ttf_LONGDATETIME)v[2]) << 40) | (((ttf_LONGDATETIME)v[3]) << 32) | + (((ttf_LONGDATETIME)v[4]) << 24) | (((ttf_LONGDATETIME)v[5]) << 16) | (((ttf_LONGDATETIME)v[6]) << 8) | (ttf_LONGDATETIME)v[7]; +} + +ttf_Tag TrueTypeFileReader::ReadTag() +{ + ttf_Tag v; Read(v.data(), v.size()); return v; +} + +ttf_Offset16 TrueTypeFileReader::ReadOffset16() +{ + return ReadUInt16(); +} + +ttf_Offset24 TrueTypeFileReader::ReadOffset24() +{ + return ReadUInt24(); +} + +ttf_Offset32 TrueTypeFileReader::ReadOffset32() +{ + return ReadUInt32(); +} + +ttf_Version16Dot16 TrueTypeFileReader::ReadVersion16Dot16() +{ + return ReadUInt32(); +} + +void TrueTypeFileReader::Seek(size_t newpos) +{ + if (newpos > size) + throw std::runtime_error("Invalid TTF file"); + + pos = newpos; +} + +void TrueTypeFileReader::Read(void* output, size_t count) +{ + if (pos + count > size) + throw std::runtime_error("Unexpected end of TTF file"); + memcpy(output, data + pos, count); + pos += count; +} diff --git a/libraries/ZWidget/src/core/truetypefont.h b/libraries/ZWidget/src/core/truetypefont.h new file mode 100644 index 000000000..53d8837fc --- /dev/null +++ b/libraries/ZWidget/src/core/truetypefont.h @@ -0,0 +1,527 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +typedef uint8_t ttf_uint8; +typedef uint16_t ttf_uint16; +typedef uint32_t ttf_uint24; // 24-bit unsigned integer +typedef uint32_t ttf_uint32; + +typedef int8_t ttf_int8; +typedef int16_t ttf_int16; +typedef int32_t ttf_int32; + +typedef uint32_t ttf_Fixed; // 32-bit signed fixed-point number (16.16) +typedef uint16_t ttf_UFWORD; // uint16 that describes a quantity in font design units +typedef int16_t ttf_FWORD; // int16 that describes a quantity in font design units +typedef uint16_t ttf_F2DOT14; // 16-bit signed fixed number with the low 14 bits of fraction (2.14) +typedef uint64_t ttf_LONGDATETIME; // number of seconds since 12:00 midnight, January 1, 1904, UTC + +typedef std::array ttf_Tag; // 4 byte identifier + +typedef uint16_t ttf_Offset16; // Short offset to a table, same as uint16, NULL offset = 0x0000 +typedef uint32_t ttf_Offset24; // 24-bit offset to a table, same as uint24, NULL offset = 0x000000 +typedef uint32_t ttf_Offset32; // Long offset to a table, same as uint32, NULL offset = 0x00000000 + +typedef uint32_t ttf_Version16Dot16; // Packed 32-bit value with major and minor version numbers + +class TrueTypeFileReader +{ +public: + TrueTypeFileReader() = default; + TrueTypeFileReader(const void* data, size_t size) : data(static_cast(data)), size(size) { } + + bool IsEndOfData() const { return pos == size; } + + ttf_uint8 ReadUInt8(); + ttf_uint16 ReadUInt16(); + ttf_uint24 ReadUInt24(); + ttf_uint32 ReadUInt32(); + ttf_int8 ReadInt8(); + ttf_int16 ReadInt16(); + ttf_int32 ReadInt32(); + ttf_Fixed ReadFixed(); + ttf_UFWORD ReadUFWORD(); + ttf_FWORD ReadFWORD(); + ttf_F2DOT14 ReadF2DOT14(); + ttf_LONGDATETIME ReadLONGDATETIME(); + ttf_Tag ReadTag(); + ttf_Offset16 ReadOffset16(); + ttf_Offset24 ReadOffset24(); + ttf_Offset32 ReadOffset32(); + ttf_Version16Dot16 ReadVersion16Dot16(); + + void Seek(size_t newpos); + void Read(void* output, size_t count); + +private: + const uint8_t* data = nullptr; + size_t size = 0; + size_t pos = 0; +}; + +struct TTF_TableRecord +{ + ttf_Tag tableTag = {}; + ttf_uint32 checksum = {}; + ttf_Offset32 offset = {}; + ttf_uint32 length = {}; + + void Load(TrueTypeFileReader& reader); + + TrueTypeFileReader GetReader(const void* filedata, size_t filesize) const + { + if ((size_t)offset + length > filesize) + throw std::runtime_error("Invalid TTF table directory record"); + + return TrueTypeFileReader((uint8_t*)filedata + offset, length); + } +}; + +struct TTF_TableDirectory +{ + ttf_uint32 sfntVersion = {}; + ttf_uint16 numTables = {}; + std::vector tableRecords; + + // To do: Apple TTF fonts allow 'true' and 'typ1' for sfntVersion as well. + bool ContainsTTFOutlines() const { return sfntVersion == 0x00010000; } + bool ContainsCFFData() const { return sfntVersion == 0x4F54544F; } + + void Load(TrueTypeFileReader& reader); + + const TTF_TableRecord& GetRecord(const char* tag) const + { + for (const auto& record : tableRecords) + { + if (memcmp(record.tableTag.data(), tag, 4) == 0) + { + return record; + } + } + throw std::runtime_error(std::string("Could not find required '") + tag + "' table entry"); + } + + TrueTypeFileReader GetReader(const void* filedata, size_t filesize, const char* tag) const + { + return GetRecord(tag).GetReader(filedata, filesize); + } +}; + +struct TTC_Header +{ + ttf_Tag ttcTag = {}; + ttf_uint16 majorVersion = {}; + ttf_uint16 minorVersion = {}; + ttf_uint32 numFonts = {}; + std::vector tableDirectoryOffsets; + + // majorVersion = 2, minorVersion = 0: + ttf_uint32 dsigTag = {}; + ttf_uint32 dsigLength = {}; + ttf_uint32 dsigOffset = {}; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_EncodingRecord +{ + ttf_uint16 platformID = {}; + ttf_uint16 encodingID = {}; + ttf_Offset32 subtableOffset = {}; +}; + +struct TTF_CMap // 'cmap' Character to glyph mapping +{ + ttf_uint16 version = {}; + ttf_uint16 numTables = {}; + std::vector encodingRecords; // [numTables] + + void Load(TrueTypeFileReader& reader); + + TTF_EncodingRecord GetEncoding(ttf_uint16 platformID, ttf_uint16 encodingID) const + { + for (const TTF_EncodingRecord& record : encodingRecords) + { + if (record.platformID == platformID && record.encodingID == encodingID) + { + return record; + } + } + return {}; + } +}; + +struct TTF_GlyphRange +{ + ttf_uint32 startCharCode = 0; + ttf_uint32 endCharCode = 0; + ttf_uint32 startGlyphID = 0; +}; + +struct TTF_CMapSubtable0 // Byte encoding table +{ + ttf_uint16 length = {}; + ttf_uint16 language = {}; + std::vector glyphIdArray; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_CMapSubtable4 // Segment mapping to delta values (U+0000 to U+FFFF) +{ + ttf_uint16 length = {}; + ttf_uint16 language = {}; + ttf_uint16 segCount = {}; + std::vector endCode; + ttf_uint16 reservedPad = {}; + std::vector startCode; + std::vector idDelta; + std::vector idRangeOffsets; + std::vector glyphIdArray; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_CMapSubtable12 // Segmented coverage (U+0000 to U+10FFFF) +{ + ttf_uint16 reserved; + ttf_uint32 length; + ttf_uint32 language; + ttf_uint32 numGroups; + std::vector groups; + + void Load(TrueTypeFileReader& reader); +}; + +typedef TTF_CMapSubtable12 TTF_CMapSubtable13; // Many-to-one range mappings + +struct TTF_FontHeader // 'head' Font header +{ + ttf_uint16 majorVersion = {}; + ttf_uint16 minorVersion = {}; + ttf_Fixed fontRevision = {}; + ttf_uint32 checksumAdjustment = {}; + ttf_uint32 magicNumber = {}; + ttf_uint16 flags = {}; + ttf_uint16 unitsPerEm = {}; + ttf_LONGDATETIME created = {}; + ttf_LONGDATETIME modified = {}; + ttf_int16 xMin = {}; + ttf_int16 yMin = {}; + ttf_int16 xMax = {}; + ttf_int16 yMax = {}; + ttf_uint16 macStyle = {}; + ttf_uint16 lowestRecPPEM = {}; + ttf_int16 fontDirectionHint = {}; + ttf_int16 indexToLocFormat = {}; + ttf_int16 glyphDataFormat = {}; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_HorizontalHeader // 'hhea' Horizontal header +{ + ttf_uint16 majorVersion = {}; + ttf_uint16 minorVersion = {}; + ttf_FWORD ascender = {}; + ttf_FWORD descender = {}; + ttf_FWORD lineGap = {}; + ttf_UFWORD advanceWidthMax = {}; + ttf_FWORD minLeftSideBearing = {}; + ttf_FWORD minRightSideBearing = {}; + ttf_FWORD xMaxExtent = {}; + ttf_int16 caretSlopeRise = {}; + ttf_int16 caretSlopeRun = {}; + ttf_int16 caretOffset = {}; + ttf_int16 reserved0 = {}; + ttf_int16 reserved1 = {}; + ttf_int16 reserved2 = {}; + ttf_int16 reserved3 = {}; + ttf_int16 metricDataFormat = {}; + ttf_uint16 numberOfHMetrics = {}; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_MaximumProfile; + +struct TTF_HorizontalMetrics // 'hmtx' Horizontal metrics +{ + struct longHorMetric + { + ttf_uint16 advanceWidth = {}; + ttf_int16 lsb = {}; + }; + std::vector hMetrics; // [hhea.numberOfHMetrics] + std::vector leftSideBearings; // [maxp.numGlyphs - hhea.numberOfHMetrics] + + void Load(const TTF_HorizontalHeader& hhea, const TTF_MaximumProfile& maxp, TrueTypeFileReader& reader); +}; + +struct TTF_MaximumProfile // 'maxp' Maximum profile +{ + // v0.5 and v1: + ttf_Version16Dot16 version = {}; + ttf_uint16 numGlyphs = {}; + + // v1 only: + ttf_uint16 maxPoints = {}; + ttf_uint16 maxContours = {}; + ttf_uint16 maxCompositePoints = {}; + ttf_uint16 maxCompositeContours = {}; + ttf_uint16 maxZones = {}; + ttf_uint16 maxTwilightPoints = {}; + ttf_uint16 maxStorage = {}; + ttf_uint16 maxFunctionDefs = {}; + ttf_uint16 maxInstructionDefs = {}; + ttf_uint16 maxStackElements = {}; + ttf_uint16 maxSizeOfInstructions = {}; + ttf_uint16 maxComponentElements = {}; + ttf_uint16 maxComponentDepth = {}; + + void Load(TrueTypeFileReader& reader); +}; + +class TTCFontName +{ +public: + std::string FamilyName; // Arial + std::string SubfamilyName; // Regular + std::string FullName; // Arial Regular + std::string UniqueID; + std::string VersionString; + std::string PostscriptName; +}; + +struct TTF_NamingTable // 'name' Naming table +{ + struct NameRecord + { + ttf_uint16 platformID = {}; + ttf_uint16 encodingID = {}; + ttf_uint16 languageID = {}; + ttf_uint16 nameID = {}; + ttf_uint16 length = {}; + ttf_Offset16 stringOffset = {}; + std::string text; + }; + + struct LangTagRecord + { + ttf_uint16 length = {}; + ttf_Offset16 langTagOffset = {}; + }; + + // v0 and v1: + ttf_uint16 version = {}; + ttf_uint16 count = {}; + ttf_Offset16 storageOffset = {}; + std::vector nameRecord; // [count] + + // v1 only: + ttf_uint16 langTagCount = {}; + std::vector langTagRecord; // [langTagCount] + + void Load(TrueTypeFileReader& reader); + + TTCFontName GetFontName() const; +}; + +struct TTF_OS2Windows // 'OS/2' Windows specific metrics +{ + ttf_uint16 version = {}; + ttf_int16 xAvgCharWidth = {}; + ttf_uint16 usWeightClass = {}; + ttf_uint16 usWidthClass = {}; + ttf_uint16 fsType = {}; + ttf_int16 ySubscriptXSize = {}; + ttf_int16 ySubscriptYSize = {}; + ttf_int16 ySubscriptXOffset = {}; + ttf_int16 ySubscriptYOffset = {}; + ttf_int16 ySuperscriptXSize = {}; + ttf_int16 ySuperscriptYSize = {}; + ttf_int16 ySuperscriptXOffset = {}; + ttf_int16 ySuperscriptYOffset = {}; + ttf_int16 yStrikeoutSize = {}; + ttf_int16 yStrikeoutPosition = {}; + ttf_int16 sFamilyClass = {}; + ttf_uint8 panose[10] = {}; + ttf_uint32 ulUnicodeRange1 = {}; + ttf_uint32 ulUnicodeRange2 = {}; + ttf_uint32 ulUnicodeRange3 = {}; + ttf_uint32 ulUnicodeRange4 = {}; + ttf_Tag achVendID = {}; + ttf_uint16 fsSelection = {}; + ttf_uint16 usFirstCharIndex = {}; + ttf_uint16 usLastCharIndex = {}; + ttf_int16 sTypoAscender = {}; + ttf_int16 sTypoDescender = {}; + ttf_int16 sTypoLineGap = {}; + ttf_uint16 usWinAscent = {}; // may be missing in v0 due to bugs in Apple docs + ttf_uint16 usWinDescent = {}; // may be missing in v0 due to bugs in Apple docs + ttf_uint32 ulCodePageRange1 = {}; // v1 + ttf_uint32 ulCodePageRange2 = {}; + ttf_int16 sxHeight = {}; // v2, v3 and v4 + ttf_int16 sCapHeight = {}; + ttf_uint16 usDefaultChar = {}; + ttf_uint16 usBreakChar = {}; + ttf_uint16 usMaxContext = {}; + ttf_uint16 usLowerOpticalPointSize = {}; // v5 + ttf_uint16 usUpperOpticalPointSize = {}; + + void Load(TrueTypeFileReader& reader); +}; + +// Simple glyph flags: +#define TTF_ON_CURVE_POINT 0x01 +#define TTF_X_SHORT_VECTOR 0x02 +#define TTF_Y_SHORT_VECTOR 0x04 +#define TTF_REPEAT_FLAG 0x08 +#define TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR 0x10 +#define TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR 0x20 +#define TTF_OVERLAP_SIMPLE = 0x40 + +// Composite glyph flags: +#define TTF_ARG_1_AND_2_ARE_WORDS 0x0001 +#define TTF_ARGS_ARE_XY_VALUES 0x0002 +#define TTF_ROUND_XY_TO_GRID 0x0004 +#define TTF_WE_HAVE_A_SCALE 0x0008 +#define TTF_MORE_COMPONENTS 0x0020 +#define TTF_WE_HAVE_AN_X_AND_Y_SCALE 0x0040 +#define TTF_WE_HAVE_A_TWO_BY_TWO 0x0080 +#define TTF_WE_HAVE_INSTRUCTIONS 0x0100 +#define TTF_USE_MY_METRICS 0x0200 +#define TTF_OVERLAP_COMPOUND 0x0400 +#define TTF_SCALED_COMPONENT_OFFSET 0x0800 +#define TTF_UNSCALED_COMPONENT_OFFSET 0x1000 + +struct TTF_IndexToLocation // 'loca' Index to location +{ + std::vector offsets; + + void Load(const TTF_FontHeader& head, const TTF_MaximumProfile& maxp, TrueTypeFileReader& reader); +}; + +struct TTF_Point +{ + float x; + float y; +}; + +struct TTF_SimpleGlyph +{ + std::vector endPtsOfContours; + std::vector flags; + std::vector points; +}; + +class TrueTypeGlyph +{ +public: + int advanceWidth = 0; + int leftSideBearing = 0; + int yOffset = 0; + + int width = 0; + int height = 0; + std::unique_ptr grayscale; +}; + +class TrueTypeTextMetrics +{ +public: + double ascender = 0.0; + double descender = 0.0; + double lineGap = 0.0; +}; + +class TrueTypeFontFileData +{ +public: + TrueTypeFontFileData(std::vector data) : dataVector(std::move(data)) + { + dataPtr = dataVector.data(); + dataSize = dataVector.size(); + } + + TrueTypeFontFileData(const void* data, size_t size, bool copyData = true) + { + dataSize = size; + if (copyData) + { + dataPtr = new uint8_t[size]; + deleteDataPtr = true; + memcpy(const_cast(dataPtr), data, size); + } + else + { + dataPtr = data; + } + } + + ~TrueTypeFontFileData() + { + if (deleteDataPtr) + { + delete[](uint8_t*)dataPtr; + } + dataPtr = nullptr; + } + + const void* data() const { return dataPtr; } + size_t size() const { return dataSize; } + +private: + std::vector dataVector; + const void* dataPtr = nullptr; + size_t dataSize = 0; + bool deleteDataPtr = false; + + TrueTypeFontFileData(const TrueTypeFontFileData&) = delete; + TrueTypeFontFileData& operator=(const TrueTypeFontFileData&) = delete; +}; + +class TrueTypeFont +{ +public: + TrueTypeFont(std::shared_ptr data, int ttcFontIndex = 0); + + static std::vector GetFontNames(const std::shared_ptr& data); + + TrueTypeTextMetrics GetTextMetrics(double height) const; + uint32_t GetGlyphIndex(uint32_t codepoint) const; + TrueTypeGlyph LoadGlyph(uint32_t glyphIndex, double height) const; + +private: + void LoadCharacterMapEncoding(TrueTypeFileReader& reader); + void LoadGlyph(TTF_SimpleGlyph& glyph, uint32_t glyphIndex, int compositeDepth = 0) const; + static float F2DOT14_ToFloat(ttf_F2DOT14 v); + + std::shared_ptr data; + + TTC_Header ttcHeader; + TTF_TableDirectory directory; + + // Required for all OpenType fonts: + TTF_CMap cmap; + TTF_FontHeader head; + TTF_HorizontalHeader hhea; + TTF_HorizontalMetrics hmtx; + TTF_MaximumProfile maxp; + TTF_NamingTable name; + TTF_OS2Windows os2; + + // TrueType outlines: + TTF_TableRecord glyf; // Parsed on a per glyph basis using offsets from other tables + TTF_IndexToLocation loca; + + std::vector Ranges; + std::vector ManyToOneRanges; +}; diff --git a/libraries/ZWidget/src/core/utf8reader.cpp b/libraries/ZWidget/src/core/utf8reader.cpp new file mode 100644 index 000000000..200da9fdc --- /dev/null +++ b/libraries/ZWidget/src/core/utf8reader.cpp @@ -0,0 +1,153 @@ +/* +** Copyright (c) 1997-2015 Mark Page +** +** This software is provided 'as-is', without any express or implied +** warranty. In no event will the authors be held liable for any damages +** arising from the use of this software. +** +** Permission is granted to anyone to use this software for any purpose, +** including commercial applications, and to alter it and redistribute it +** freely, subject to the following restrictions: +** +** 1. The origin of this software must not be misrepresented; you must not +** claim that you wrote the original software. If you use this software +** in a product, an acknowledgment in the product documentation would be +** appreciated but is not required. +** 2. Altered source versions must be plainly marked as such, and must not be +** misrepresented as being the original software. +** 3. This notice may not be removed or altered from any source distribution. +** +*/ + +#include "core/utf8reader.h" + +namespace +{ + static const char trailing_bytes_for_utf8[256] = + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5 + }; + + static const unsigned char bitmask_leadbyte_for_utf8[6] = + { + 0x7f, + 0x1f, + 0x0f, + 0x07, + 0x03, + 0x01 + }; +} + +UTF8Reader::UTF8Reader(const std::string::value_type *text, std::string::size_type length) : length(length), data((unsigned char *)text) +{ +} + +bool UTF8Reader::is_end() +{ + return current_position >= length; +} + +unsigned int UTF8Reader::character() +{ + if (current_position >= length) + return 0; + + int trailing_bytes = trailing_bytes_for_utf8[data[current_position]]; + if (trailing_bytes == 0 && (data[current_position] & 0x80) == 0x80) + return '?'; + + if (current_position + 1 + trailing_bytes > length) + { + return '?'; + } + else + { + unsigned int ucs4 = (data[current_position] & bitmask_leadbyte_for_utf8[trailing_bytes]); + for (std::string::size_type i = 0; i < trailing_bytes; i++) + { + if ((data[current_position + 1 + i] & 0xC0) == 0x80) + ucs4 = (ucs4 << 6) + (data[current_position + 1 + i] & 0x3f); + else + return '?'; + } + + // To do: verify that the ucs4 value is in the range for the trailing_bytes specified in the lead byte. + + return ucs4; + } + +} + +std::string::size_type UTF8Reader::char_length() +{ + if (current_position < length) + { + int trailing_bytes = trailing_bytes_for_utf8[data[current_position]]; + if (current_position + 1 + trailing_bytes > length) + return 1; + + for (std::string::size_type i = 0; i < trailing_bytes; i++) + { + if ((data[current_position + 1 + i] & 0xC0) != 0x80) + return 1; + } + + return 1 + trailing_bytes; + } + else + { + return 0; + } +} + +void UTF8Reader::prev() +{ + if (current_position > length) + current_position = length; + + if (current_position > 0) + { + current_position--; + move_to_leadbyte(); + } +} + +void UTF8Reader::next() +{ + current_position += char_length(); + +} + +void UTF8Reader::move_to_leadbyte() +{ + if (current_position < length) + { + int lead_position = (int)current_position; + + while (lead_position > 0 && (data[lead_position] & 0xC0) == 0x80) + lead_position--; + + int trailing_bytes = trailing_bytes_for_utf8[data[lead_position]]; + if (lead_position + trailing_bytes >= current_position) + current_position = lead_position; + } + +} + +std::string::size_type UTF8Reader::position() +{ + return current_position; +} + +void UTF8Reader::set_position(std::string::size_type position) +{ + current_position = position; +} diff --git a/libraries/ZWidget/src/core/widget.cpp b/libraries/ZWidget/src/core/widget.cpp new file mode 100644 index 000000000..f431a6917 --- /dev/null +++ b/libraries/ZWidget/src/core/widget.cpp @@ -0,0 +1,957 @@ + +#include "core/widget.h" +#include "core/timer.h" +#include "core/colorf.h" +#include "core/theme.h" +#include +#include +#include + +Widget::Widget(Widget* parent, WidgetType type, RenderAPI renderAPI) : Type(type) +{ + if (type != WidgetType::Child) + { + Widget* owner = parent ? parent->Window() : nullptr; + DispWindow = DisplayWindow::Create(this, type == WidgetType::Popup, owner ? owner->DispWindow.get() : nullptr, renderAPI); + if (renderAPI == RenderAPI::Unspecified || renderAPI == RenderAPI::Bitmap) + { + DispCanvas = Canvas::create(); + DispCanvas->attach(DispWindow.get()); + } + SetStyleState("root"); + + SetWindowBackground(GetStyleColor("window-background")); + if (GetStyleColor("window-border").a > 0.0f) + SetWindowBorderColor(GetStyleColor("window-border")); + if (GetStyleColor("window-caption-color").a > 0.0f) + SetWindowCaptionColor(GetStyleColor("window-caption-color")); + if (GetStyleColor("window-caption-text-color").a > 0.0f) + SetWindowCaptionTextColor(GetStyleColor("window-caption-text-color")); + } + + SetParent(parent); +} + +Widget::~Widget() +{ + if (DispCanvas) + DispCanvas->detach(); + + while (LastChildObj) + delete LastChildObj; + + while (FirstTimerObj) + delete FirstTimerObj; + + DetachFromParent(); +} + +void Widget::SetCanvas(std::unique_ptr canvas) +{ + if (DispWindow) + { + if (DispCanvas) + DispCanvas->detach(); + DispCanvas = std::move(canvas); + DispCanvas->attach(DispWindow.get()); + } +} + +void Widget::SetParent(Widget* newParent) +{ + if (ParentObj != newParent) + { + if (ParentObj) + DetachFromParent(); + + if (newParent) + { + PrevSiblingObj = newParent->LastChildObj; + if (PrevSiblingObj) PrevSiblingObj->NextSiblingObj = this; + newParent->LastChildObj = this; + if (!newParent->FirstChildObj) newParent->FirstChildObj = this; + ParentObj = newParent; + } + } +} + +void Widget::MoveBefore(Widget* sibling) +{ + if (sibling && sibling->ParentObj != ParentObj) throw std::runtime_error("Invalid sibling passed to Widget.MoveBefore"); + if (!ParentObj) throw std::runtime_error("Widget must have a parent before it can be moved"); + + if (NextSiblingObj != sibling) + { + Widget* p = ParentObj; + DetachFromParent(); + + ParentObj = p; + if (sibling) + { + NextSiblingObj = sibling; + PrevSiblingObj = sibling->PrevSiblingObj; + sibling->PrevSiblingObj = this; + if (PrevSiblingObj) PrevSiblingObj->NextSiblingObj = this; + if (ParentObj->FirstChildObj == sibling) ParentObj->FirstChildObj = this; + } + else + { + PrevSiblingObj = ParentObj->LastChildObj; + if (PrevSiblingObj) PrevSiblingObj->NextSiblingObj = this; + ParentObj->LastChildObj = this; + if (!ParentObj->FirstChildObj) ParentObj->FirstChildObj = this; + } + } +} + +void Widget::DetachFromParent() +{ + for (Widget* cur = ParentObj; cur; cur = cur->ParentObj) + { + if (cur->FocusWidget == this) + cur->FocusWidget = nullptr; + if (cur->CaptureWidget == this) + cur->CaptureWidget = nullptr; + if (cur->HoverWidget == this) + cur->HoverWidget = nullptr; + + if (cur->DispWindow) + break; + } + + if (PrevSiblingObj) + PrevSiblingObj->NextSiblingObj = NextSiblingObj; + if (NextSiblingObj) + NextSiblingObj->PrevSiblingObj = PrevSiblingObj; + if (ParentObj) + { + if (ParentObj->FirstChildObj == this) + ParentObj->FirstChildObj = NextSiblingObj; + if (ParentObj->LastChildObj == this) + ParentObj->LastChildObj = PrevSiblingObj; + } + PrevSiblingObj = nullptr; + NextSiblingObj = nullptr; + ParentObj = nullptr; +} + +std::string Widget::GetWindowTitle() const +{ + return WindowTitle; +} + +void Widget::SetWindowTitle(const std::string& text) +{ + if (WindowTitle != text) + { + WindowTitle = text; + if (DispWindow) + DispWindow->SetWindowTitle(WindowTitle); + } +} + +Size Widget::GetSize() const +{ + return ContentGeometry.size(); +} + +Rect Widget::GetFrameGeometry() const +{ + if (Type == WidgetType::Child) + { + return FrameGeometry; + } + else + { + return DispWindow->GetWindowFrame(); + } +} + +void Widget::SetNoncontentSizes(double left, double top, double right, double bottom) +{ + SetStyleDouble("noncontent-left", left); + SetStyleDouble("noncontent-top", top); + SetStyleDouble("noncontent-right", right); + SetStyleDouble("noncontent-bottom", bottom); +} + +void Widget::SetFrameGeometry(const Rect& geometry) +{ + if (Type == WidgetType::Child) + { + FrameGeometry = geometry; + double left = FrameGeometry.left() + GetNoncontentLeft(); + double top = FrameGeometry.top() + GetNoncontentTop(); + double right = FrameGeometry.right() - GetNoncontentRight(); + double bottom = FrameGeometry.bottom() - GetNoncontentBottom(); + left = std::min(left, FrameGeometry.right()); + top = std::min(top, FrameGeometry.bottom()); + right = std::max(right, FrameGeometry.left()); + bottom = std::max(bottom, FrameGeometry.top()); + left = GridFitPoint(left); + top = GridFitPoint(top); + right = GridFitPoint(right); + bottom = GridFitPoint(bottom); + ContentGeometry = Rect::ltrb(left, top, right, bottom); + OnGeometryChanged(); + } + else + { + DispWindow->SetWindowFrame(geometry); + } +} + +void Widget::Show() +{ + if (Type != WidgetType::Child) + { + DispWindow->Show(); + } + else if (HiddenFlag) + { + HiddenFlag = false; + Update(); + } +} + +void Widget::ShowFullscreen() +{ + if (Type != WidgetType::Child) + { + DispWindow->ShowFullscreen(); + } +} + +bool Widget::IsFullscreen() +{ + if (Type != WidgetType::Child) + { + return DispWindow->IsWindowFullscreen(); + } + return false; +} + +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(); + } + else if (!HiddenFlag) + { + HiddenFlag = true; + Update(); + } +} + +void Widget::ActivateWindow() +{ + if (Type != WidgetType::Child) + { + DispWindow->Activate(); + } +} + +void Widget::Close() +{ + OnClose(); +} + +void Widget::SetWindowBackground(const Colorf& color) +{ + Widget* w = Window(); + if (w && w->WindowBackground != color) + { + w->WindowBackground = color; + Update(); + } +} + +void Widget::SetWindowBorderColor(const Colorf& color) +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->SetBorderColor(color.toBgra8()); + w->DispWindow->Update(); + } +} + +void Widget::SetWindowCaptionColor(const Colorf& color) +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->SetCaptionColor(color.toBgra8()); + w->DispWindow->Update(); + } +} + +void Widget::SetWindowCaptionTextColor(const Colorf& color) +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->SetCaptionTextColor(color.toBgra8()); + w->DispWindow->Update(); + } +} + +void Widget::Update() +{ + Widget* w = Window(); + if (w) + { + w->DispWindow->Update(); + } +} + +void Widget::Repaint() +{ + Widget* w = Window(); + if (w->DispCanvas) + { + w->DispCanvas->begin(WindowBackground); + w->Paint(w->DispCanvas.get()); + w->DispCanvas->end(); + } +} + +void Widget::Paint(Canvas* canvas) +{ + Point oldOrigin = canvas->getOrigin(); + canvas->pushClip(FrameGeometry); + canvas->setOrigin(oldOrigin + FrameGeometry.topLeft()); + OnPaintFrame(canvas); + canvas->setOrigin(oldOrigin); + canvas->popClip(); + + canvas->pushClip(ContentGeometry); + canvas->setOrigin(oldOrigin + ContentGeometry.topLeft()); + OnPaint(canvas); + for (Widget* w = FirstChild(); w != nullptr; w = w->NextSibling()) + { + if (w->Type == WidgetType::Child && !w->HiddenFlag) + w->Paint(canvas); + } + canvas->setOrigin(oldOrigin); + canvas->popClip(); +} + +void Widget::OnPaintFrame(Canvas* canvas) +{ + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + if (style) + { + style->Paint(this, canvas, GetFrameGeometry().size()); + } +} + +bool Widget::GetKeyState(InputKey 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::IsHidden() +{ + return !IsVisible(); +} + +bool Widget::IsVisible() +{ + if (Type != WidgetType::Child) + { + return true; // DispWindow->IsVisible(); + } + else + { + return !HiddenFlag; + } +} + +void Widget::SetFocus() +{ + Widget* window = Window(); + if (window && window->FocusWidget != this) + { + if (window->FocusWidget) + window->FocusWidget->OnLostFocus(); + window->FocusWidget = this; + window->FocusWidget->OnSetFocus(); + window->ActivateWindow(); + } +} + +void Widget::SetEnabled(bool value) +{ +} + +void Widget::LockCursor() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != this) + { + w->CaptureWidget = this; + w->DispWindow->LockCursor(); + } +} + +void Widget::UnlockCursor() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != nullptr) + { + w->CaptureWidget = nullptr; + w->DispWindow->UnlockCursor(); + } +} + +void Widget::SetCursor(StandardCursor cursor) +{ + if (CurrentCursor != cursor) + { + CurrentCursor = cursor; + if (HoverWidget == this || CaptureWidget == this) + { + Widget* w = Window(); + if (w) + { + w->DispWindow->SetCursor(CurrentCursor); + } + } + } +} + +void Widget::SetPointerCapture() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != this) + { + w->CaptureWidget = this; + w->DispWindow->CaptureMouse(); + } +} + +void Widget::ReleasePointerCapture() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != nullptr) + { + w->CaptureWidget = nullptr; + w->DispWindow->ReleaseMouseCapture(); + } +} + +void Widget::SetModalCapture() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != this) + { + w->CaptureWidget = this; + } +} + +void Widget::ReleaseModalCapture() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != nullptr) + { + w->CaptureWidget = nullptr; + } +} + +std::string Widget::GetClipboardText() +{ + Widget* w = Window(); + if (w) + return w->DispWindow->GetClipboardText(); + else + return {}; +} + +void Widget::SetClipboardText(const std::string& text) +{ + Widget* w = Window(); + if (w) + w->DispWindow->SetClipboardText(text); +} + +Widget* Widget::Window() const +{ + for (const Widget* w = this; w != nullptr; w = w->Parent()) + { + if (w->DispWindow) + return const_cast(w); + } + return nullptr; +} + +Canvas* Widget::GetCanvas() const +{ + for (const Widget* w = this; w != nullptr; w = w->Parent()) + { + if (w->DispCanvas) + return w->DispCanvas.get(); + } + return nullptr; +} + +bool Widget::IsParent(const Widget* w) const +{ + while (w) + { + w = w->Parent(); + if (w == this) + return true; + } + return false; +} + +bool Widget::IsChild(const Widget* w) const +{ + if (!w) + return false; + return w->IsParent(this); +} + +Widget* Widget::ChildAt(const Point& pos) +{ + for (Widget* cur = LastChild(); cur != nullptr; cur = cur->PrevSibling()) + { + if (cur->Type == WidgetType::Child && !cur->HiddenFlag && cur->FrameGeometry.contains(pos)) + { + Widget* cur2 = cur->ChildAt(pos - cur->ContentGeometry.topLeft()); + return cur2 ? cur2 : cur; + } + } + return nullptr; +} + +Point Widget::MapFrom(const Widget* parent, const Point& pos) const +{ + Point p = pos; + for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) + { + if (cur == parent) + return p; + p -= cur->ContentGeometry.topLeft(); + } + throw std::runtime_error("MapFrom: not a parent of widget"); +} + +Point Widget::MapFromGlobal(const Point& pos) const +{ + Point p = pos; + for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) + { + if (cur->DispWindow) + { + return cur->DispWindow->MapFromGlobal(p); + } + p -= cur->ContentGeometry.topLeft(); + } + throw std::runtime_error("MapFromGlobal: no window widget found"); +} + +Point Widget::MapTo(const Widget* parent, const Point& pos) const +{ + Point p = pos; + for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) + { + if (cur == parent) + return p; + p += cur->ContentGeometry.topLeft(); + } + throw std::runtime_error("MapTo: not a parent of widget"); +} + +Point Widget::MapToGlobal(const Point& pos) const +{ + Point p = pos; + for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) + { + if (cur->DispWindow) + { + return cur->DispWindow->MapToGlobal(p); + } + p += cur->ContentGeometry.topLeft(); + } + throw std::runtime_error("MapFromGlobal: no window widget found"); +} + +void Widget::OnWindowPaint() +{ + Repaint(); +} + +void Widget::OnWindowMouseMove(const Point& pos) +{ + if (CaptureWidget) + { + DispWindow->SetCursor(CaptureWidget->CurrentCursor); + CaptureWidget->OnMouseMove(CaptureWidget->MapFrom(this, pos)); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + + if (HoverWidget != widget) + { + if (HoverWidget) + { + for (Widget* w = HoverWidget; w != widget && w != this; w = w->Parent()) + { + Widget* p = w->Parent(); + if (!w->FrameGeometry.contains(p->MapFrom(this, pos))) + { + w->OnMouseLeave(); + } + } + } + HoverWidget = widget; + } + + DispWindow->SetCursor(widget->CurrentCursor); + + do + { + widget->OnMouseMove(widget->MapFrom(this, pos)); + if (widget == this) + break; + widget = widget->Parent(); + } while (widget); + } +} + +void Widget::OnWindowMouseLeave() +{ + if (HoverWidget) + { + for (Widget* w = HoverWidget; w; w = w->Parent()) + { + w->OnMouseLeave(); + } + HoverWidget = nullptr; + } +} + +void Widget::OnWindowMouseDown(const Point& pos, InputKey key) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseDown(CaptureWidget->MapFrom(this, pos), key); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + while (widget) + { + bool stopPropagation = widget->OnMouseDown(widget->MapFrom(this, pos), key); + if (stopPropagation || widget == this) + break; + widget = widget->Parent(); + } + } +} + +void Widget::OnWindowMouseDoubleclick(const Point& pos, InputKey key) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseDoubleclick(CaptureWidget->MapFrom(this, pos), key); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + while (widget) + { + bool stopPropagation = widget->OnMouseDoubleclick(widget->MapFrom(this, pos), key); + if (stopPropagation || widget == this) + break; + widget = widget->Parent(); + } + } +} + +void Widget::OnWindowMouseUp(const Point& pos, InputKey key) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseUp(CaptureWidget->MapFrom(this, pos), key); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + while (widget) + { + bool stopPropagation = widget->OnMouseUp(widget->MapFrom(this, pos), key); + if (stopPropagation || widget == this) + break; + widget = widget->Parent(); + } + } +} + +void Widget::OnWindowMouseWheel(const Point& pos, InputKey key) +{ + if (CaptureWidget) + { + CaptureWidget->OnMouseWheel(CaptureWidget->MapFrom(this, pos), key); + } + else + { + Widget* widget = ChildAt(pos); + if (!widget) + widget = this; + while (widget) + { + bool stopPropagation = widget->OnMouseWheel(widget->MapFrom(this, pos), key); + if (stopPropagation || widget == this) + break; + widget = widget->Parent(); + } + } +} + +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(InputKey key) +{ + if (FocusWidget) + FocusWidget->OnKeyDown(key); +} + +void Widget::OnWindowKeyUp(InputKey key) +{ + if (FocusWidget) + FocusWidget->OnKeyUp(key); +} + +void Widget::OnWindowGeometryChanged() +{ + if (!DispWindow) + return; + Size size = DispWindow->GetClientSize(); + FrameGeometry = Rect::xywh(0.0, 0.0, size.width, size.height); + + double left = FrameGeometry.left() + GetNoncontentLeft(); + double top = FrameGeometry.top() + GetNoncontentTop(); + double right = FrameGeometry.right() - GetNoncontentRight(); + double bottom = FrameGeometry.bottom() - GetNoncontentBottom(); + left = std::min(left, FrameGeometry.right()); + top = std::min(top, FrameGeometry.bottom()); + right = std::max(right, FrameGeometry.left()); + bottom = std::max(bottom, FrameGeometry.top()); + ContentGeometry = Rect::ltrb(left, top, right, bottom); + + OnGeometryChanged(); +} + +void Widget::OnWindowClose() +{ + Close(); +} + +void Widget::OnWindowActivated() +{ +} + +void Widget::OnWindowDeactivated() +{ +} + +void Widget::OnWindowDpiScaleChanged() +{ +} + +double Widget::GetDpiScale() const +{ + Widget* w = Window(); + return w ? w->DispWindow->GetDpiScale() : 1.0; +} + +double Widget::GridFitPoint(double p) const +{ + double dpiscale = GetDpiScale(); + return std::round(p * dpiscale) / dpiscale; +} + +double Widget::GridFitSize(double s) const +{ + if (s <= 0.0) + return 0.0; + double dpiscale = GetDpiScale(); + return std::max(std::floor(s * dpiscale + 0.25), 1.0) / dpiscale; +} + +Size Widget::GetScreenSize() +{ + return DisplayWindow::GetScreenSize(); +} + +void* Widget::GetNativeHandle() +{ + Widget* w = Window(); + return w ? w->DispWindow->GetNativeHandle() : nullptr; +} + +int Widget::GetNativePixelWidth() +{ + Widget* w = Window(); + return w ? w->DispWindow->GetPixelWidth() : 0; +} + +int Widget::GetNativePixelHeight() +{ + Widget* w = Window(); + return w ? w->DispWindow->GetPixelHeight() : 0; +} + +void Widget::SetStyleClass(const std::string& themeClass) +{ + if (StyleClass != themeClass) + { + StyleClass = themeClass; + Update(); + } +} + +void Widget::SetStyleState(const std::string& state) +{ + if (StyleState != state) + { + StyleState = state; + Update(); + } +} + +void Widget::SetStyleBool(const std::string& propertyName, bool value) +{ + StyleProperties[propertyName] = value; +} + +void Widget::SetStyleInt(const std::string& propertyName, int value) +{ + StyleProperties[propertyName] = value; +} + +void Widget::SetStyleDouble(const std::string& propertyName, double value) +{ + StyleProperties[propertyName] = value; +} + +void Widget::SetStyleString(const std::string& propertyName, const std::string& value) +{ + StyleProperties[propertyName] = value; +} + +void Widget::SetStyleColor(const std::string& propertyName, const Colorf& value) +{ + StyleProperties[propertyName] = value; +} + +bool Widget::GetStyleBool(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetBool(StyleState, propertyName) : false; +} + +int Widget::GetStyleInt(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetInt(StyleState, propertyName) : 0; +} + +double Widget::GetStyleDouble(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetDouble(StyleState, propertyName) : 0.0; +} + +std::string Widget::GetStyleString(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetString(StyleState, propertyName) : std::string(); +} + +Colorf Widget::GetStyleColor(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetColor(StyleState, propertyName) : Colorf::transparent(); +} diff --git a/libraries/ZWidget/src/systemdialogs/open_file_dialog.cpp b/libraries/ZWidget/src/systemdialogs/open_file_dialog.cpp new file mode 100644 index 000000000..6d0ce3931 --- /dev/null +++ b/libraries/ZWidget/src/systemdialogs/open_file_dialog.cpp @@ -0,0 +1,12 @@ + +#include "systemdialogs/open_file_dialog.h" +#include "window/window.h" +#include "core/widget.h" + +std::unique_ptr OpenFileDialog::Create(Widget* owner) +{ + DisplayWindow* windowOwner = nullptr; + if (owner && owner->Window()) + windowOwner = owner->Window()->DispWindow.get(); + return DisplayBackend::Get()->CreateOpenFileDialog(windowOwner); +} diff --git a/libraries/ZWidget/src/systemdialogs/open_folder_dialog.cpp b/libraries/ZWidget/src/systemdialogs/open_folder_dialog.cpp new file mode 100644 index 000000000..8ccf66237 --- /dev/null +++ b/libraries/ZWidget/src/systemdialogs/open_folder_dialog.cpp @@ -0,0 +1,12 @@ + +#include "systemdialogs/open_folder_dialog.h" +#include "window/window.h" +#include "core/widget.h" + +std::unique_ptr OpenFolderDialog::Create(Widget* owner) +{ + DisplayWindow* windowOwner = nullptr; + if (owner && owner->Window()) + windowOwner = owner->Window()->DispWindow.get(); + return DisplayBackend::Get()->CreateOpenFolderDialog(windowOwner); +} diff --git a/libraries/ZWidget/src/systemdialogs/save_file_dialog.cpp b/libraries/ZWidget/src/systemdialogs/save_file_dialog.cpp new file mode 100644 index 000000000..0163fe7cb --- /dev/null +++ b/libraries/ZWidget/src/systemdialogs/save_file_dialog.cpp @@ -0,0 +1,12 @@ + +#include "systemdialogs/save_file_dialog.h" +#include "window/window.h" +#include "core/widget.h" + +std::unique_ptr SaveFileDialog::Create(Widget* owner) +{ + DisplayWindow* windowOwner = nullptr; + if (owner && owner->Window()) + windowOwner = owner->Window()->DispWindow.get(); + return DisplayBackend::Get()->CreateSaveFileDialog(windowOwner); +} diff --git a/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp b/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp new file mode 100644 index 000000000..77c1bd645 --- /dev/null +++ b/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp @@ -0,0 +1,101 @@ + +#include "widgets/checkboxlabel/checkboxlabel.h" + +CheckboxLabel::CheckboxLabel(Widget* parent) : Widget(parent) +{ + SetStyleClass("checkbox-label"); +} + +void CheckboxLabel::SetText(const std::string& value) +{ + if (text != value) + { + text = value; + Update(); + } +} + +const std::string& CheckboxLabel::GetText() const +{ + return text; +} + +void CheckboxLabel::SetChecked(bool value) +{ + if (value != checked) + { + checked = value; + Update(); + } +} + +bool CheckboxLabel::GetChecked() const +{ + return checked; +} + +double CheckboxLabel::GetPreferredHeight() const +{ + return 20.0; +} + +void CheckboxLabel::OnPaint(Canvas* canvas) +{ + // To do: add and use GetStyleImage for the checkbox + + double center = GridFitPoint(GetHeight() * 0.5); + double borderwidth = GridFitSize(1.0); + double outerboxsize = GridFitSize(10.0); + double innerboxsize = outerboxsize - 2.0 * borderwidth; + double checkedsize = innerboxsize - 2.0 * borderwidth; + + if (checked) + { + canvas->fillRect(Rect::xywh(0.0, center - 6.0 * borderwidth, outerboxsize, outerboxsize), GetStyleColor("checked-outer-border-color")); + canvas->fillRect(Rect::xywh(1.0 * borderwidth, center - 5.0 * borderwidth, innerboxsize, innerboxsize), GetStyleColor("checked-inner-border-color")); + canvas->fillRect(Rect::xywh(2.0 * borderwidth, center - 4.0 * borderwidth, checkedsize, checkedsize), GetStyleColor("checked-color")); + } + else + { + canvas->fillRect(Rect::xywh(0.0, center - 6.0 * borderwidth, outerboxsize, outerboxsize), GetStyleColor("unchecked-outer-border-color")); + canvas->fillRect(Rect::xywh(1.0 * borderwidth, center - 5.0 * borderwidth, innerboxsize, innerboxsize), GetStyleColor("unchecked-inner-border-color")); + } + + canvas->drawText(Point(14.0, GetHeight() - 5.0), GetStyleColor("color"), text); +} + +bool CheckboxLabel::OnMouseDown(const Point& pos, InputKey key) +{ + mouseDownActive = true; + SetFocus(); + return true; +} + +bool CheckboxLabel::OnMouseUp(const Point& pos, InputKey key) +{ + if (mouseDownActive) + { + Toggle(); + } + mouseDownActive = false; + return true; +} + +void CheckboxLabel::OnMouseLeave() +{ + mouseDownActive = false; +} + +void CheckboxLabel::OnKeyUp(InputKey key) +{ + if (key == InputKey::Space) + Toggle(); +} + +void CheckboxLabel::Toggle() +{ + bool oldchecked = checked; + checked = radiostyle? true : !checked; + Update(); + if (checked != oldchecked && FuncChanged) FuncChanged(checked); +} diff --git a/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp b/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp new file mode 100644 index 000000000..70323cc89 --- /dev/null +++ b/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp @@ -0,0 +1,73 @@ + +#include "widgets/imagebox/imagebox.h" + +ImageBox::ImageBox(Widget* parent) : Widget(parent) +{ +} + +double ImageBox::GetPreferredWidth() const +{ + if (image) + return (double)image->GetWidth(); + else + return 0.0; +} + +double ImageBox::GetPreferredHeight() const +{ + if (image) + return (double)image->GetHeight(); + else + return 0.0; +} + +void ImageBox::SetImage(std::shared_ptr newImage) +{ + if (image != newImage) + { + image = newImage; + Update(); + } +} + +void ImageBox::SetImageMode(ImageBoxMode newMode) +{ + if (mode != newMode) + { + mode = newMode; + Update(); + } +} + +void ImageBox::OnPaint(Canvas* canvas) +{ + if (image) + { + if (mode == ImageBoxMode::Center) + { + canvas->drawImage(image, Point((GetWidth() - (double)image->GetWidth()) * 0.5, (GetHeight() - (double)image->GetHeight()) * 0.5)); + } + else if (mode == ImageBoxMode::Contain) + { + double bw = GetWidth(); + double bh = GetHeight(); + double iw = image->GetWidth(); + double ih = image->GetHeight(); + double xscale = bw / iw; + double yscale = bh / ih; + double scale = std::min(xscale, yscale); + canvas->drawImage(image, Rect::xywh((bw - iw * scale) * 0.5, (bh - ih * scale) * 0.5, iw * scale, ih * scale)); + } + else if (mode == ImageBoxMode::Cover) + { + double bw = GetWidth(); + double bh = GetHeight(); + double iw = image->GetWidth(); + double ih = image->GetHeight(); + double xscale = bw / iw; + double yscale = bh / ih; + double scale = std::max(xscale, yscale); + canvas->drawImage(image, Rect::xywh((bw - iw * scale) * 0.5, (bh - ih * scale) * 0.5, iw * scale, ih * scale)); + } + } +} diff --git a/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp b/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp new file mode 100644 index 000000000..336ac9882 --- /dev/null +++ b/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp @@ -0,0 +1,1171 @@ + +#include "widgets/lineedit/lineedit.h" +#include "core/utf8reader.h" +#include "core/colorf.h" +#include + +LineEdit::LineEdit(Widget* parent) : Widget(parent) +{ + SetStyleClass("lineedit"); + + 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 newalignment) +{ + if (alignment != newalignment) + { + alignment = newalignment; + 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) + { + 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(); + } + } +} + +bool LineEdit::OnMouseDown(const Point& pos, InputKey key) +{ + if (key == InputKey::LeftMouse) + { + if (HasFocus()) + { + SetPointerCapture(); + mouse_selecting = true; + cursor_pos = GetCharacterIndex(pos.x); + SetTextSelection(cursor_pos, 0); + } + else + { + SetFocus(); + } + Update(); + } + return true; +} + +bool LineEdit::OnMouseDoubleclick(const Point& pos, InputKey key) +{ + return true; +} + +bool LineEdit::OnMouseUp(const Point& pos, InputKey key) +{ + if (mouse_selecting && key == InputKey::LeftMouse) + { + scroll_timer->Stop(); + ReleasePointerCapture(); + mouse_selecting = false; + int sel_end = GetCharacterIndex(pos.x); + SetSelectionLength(sel_end - selection_start); + cursor_pos = sel_end; + Update(); + } + return true; +} + +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(InputKey key) +{ + if (FuncIgnoreKeyDown && FuncIgnoreKeyDown(key)) + return; + + if (key == InputKey::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 == InputKey::Enter || key == InputKey::Escape || key == InputKey::Tab) + { + // Do not consume these. + return; + } + else if (key == InputKey::A && GetKeyState(InputKey::Ctrl)) + { + // select all + SetTextSelection(0, (int)text.size()); + cursor_pos = selection_length; + UpdateTextClipping(); + Update(); + } + else if (key == InputKey::C && GetKeyState(InputKey::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 == InputKey::Left) + { + Move(-1, GetKeyState(InputKey::Ctrl), GetKeyState(InputKey::Shift)); + } + else if (key == InputKey::Right) + { + Move(1, GetKeyState(InputKey::Ctrl), GetKeyState(InputKey::Shift)); + } + else if (key == InputKey::Backspace) + { + Backspace(); + UpdateTextClipping(); + } + else if (key == InputKey::Delete) + { + Del(); + UpdateTextClipping(); + } + else if (key == InputKey::Home) + { + SetSelectionStart(cursor_pos); + cursor_pos = 0; + if (GetKeyState(InputKey::Shift)) + SetSelectionLength(-selection_start); + else + SetTextSelection(0, 0); + UpdateTextClipping(); + Update(); + } + else if (key == InputKey::End) + { + SetSelectionStart(cursor_pos); + cursor_pos = (int)text.size(); + if (GetKeyState(InputKey::Shift)) + SetSelectionLength((int)text.size() - selection_start); + else + SetTextSelection(0, 0); + UpdateTextClipping(); + Update(); + } + else if (key == InputKey::X && GetKeyState(InputKey::Ctrl)) + { + std::string str = GetSelection(); + DeleteSelectedText(); + SetClipboardText(str); + UpdateTextClipping(); + } + else if (key == InputKey::V && GetKeyState(InputKey::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(InputKey::Ctrl) && key == InputKey::Z) + { + if (!readonly) + { + std::string tmp = undo_info.undo_text; + undo_info.undo_text = GetText(); + SetText(tmp); + } + } + else if (key == InputKey::Shift) + { + if (selection_start == -1) + SetTextSelection(cursor_pos, 0); + } + + if (FuncAfterEditChanged) + FuncAfterEditChanged(); +} + +void LineEdit::OnKeyUp(InputKey key) +{ +} + +void LineEdit::OnSetFocus() +{ + if (!readonly) + timer->Start(500); + if (select_all_on_focus_gain) + SelectAll(); + 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(); + if (!canvas) + return; + + 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) +{ + 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() ? GetStyleColor("selection-color") : GetStyleColor("no-focus-selection-color")); + } + + // Draw text before selection + if (!txt_before.empty()) + { + canvas->drawText(Point(0.0, canvas->verticalTextAlign().baseline), GetStyleColor("color"), txt_before); + } + if (!txt_selected.empty()) + { + canvas->drawText(Point(size_before.width, canvas->verticalTextAlign().baseline), GetStyleColor("color"), txt_selected); + } + if (!txt_after.empty()) + { + canvas->drawText(Point(size_before.width + size_selected.width, canvas->verticalTextAlign().baseline), GetStyleColor("color"), txt_after); + } + + // draw cursor + if (HasFocus()) + { + if (cursor_blink_visible) + { + Rect cursor_rect = GetCursorRect(); + canvas->fillRect(cursor_rect, GetStyleColor("color")); + } + } +} + +void LineEdit::OnScrollTimerExpired() +{ + if (mouse_moves_left) + Move(-1, false, false); + else + Move(1, false, false); +} + +void LineEdit::OnEnableChanged() +{ + bool enabled = IsEnabled(); + + if (!enabled) + { + cursor_blink_visible = false; + timer->Stop(); + } + Update(); +} + +bool LineEdit::InputMaskAcceptsInput(int cursor_pos, const std::string& str) +{ + return str.find_first_not_of(input_mask) == std::string::npos; +} + +std::string LineEdit::CreatePassword(std::string::size_type num_letters) const +{ + return std::string(num_letters, '*'); +} + +Size LineEdit::GetVisualTextSize(Canvas* canvas, int pos, int npos) const +{ + return canvas->measureText(password_mode ? CreatePassword(UTF8Reader::utf8_length(text.substr(pos, npos))) : text.substr(pos, npos)).size(); +} + +Size LineEdit::GetVisualTextSize(Canvas* canvas) const +{ + return canvas->measureText(password_mode ? CreatePassword(UTF8Reader::utf8_length(text)) : text).size(); +} + +std::string LineEdit::ToFixed(float number, int num_decimal_places) +{ + for (int i = 0; i < num_decimal_places; i++) + number *= 10.0f; + std::string val = std::to_string((int)std::round(number)); + if ((int)val.size() < num_decimal_places) + val.resize(num_decimal_places + 1, 0); + return val.substr(0, val.size() - num_decimal_places) + "." + val.substr(val.size() - num_decimal_places); +} + +std::string LineEdit::ToLower(const std::string& text) +{ + return text; +} + +std::string LineEdit::ToUpper(const std::string& text) +{ + return text; +} + +const std::string LineEdit::break_characters = " ::;,.-"; +const std::string LineEdit::numeric_mode_characters = "0123456789"; diff --git a/libraries/ZWidget/src/widgets/listview/listview.cpp b/libraries/ZWidget/src/widgets/listview/listview.cpp new file mode 100644 index 000000000..1dd9fc9c1 --- /dev/null +++ b/libraries/ZWidget/src/widgets/listview/listview.cpp @@ -0,0 +1,286 @@ + +#include "widgets/listview/listview.h" +#include "widgets/scrollbar/scrollbar.h" + +ListView::ListView(Widget* parent) : Widget(parent) +{ + SetStyleClass("listview"); + + scrollbar = new Scrollbar(this); + scrollbar->FuncScroll = [=]() { OnScrollbarScroll(); }; + + SetColumnWidths({ 0.0 }); +} + +void ListView::SetColumnWidths(const std::vector& widths) +{ + columnwidths = widths; + + bool updated = false; + const size_t newWidth = columnwidths.size(); + for (std::vector& column : items) + { + while (column.size() < newWidth) + { + updated = true; + column.push_back(""); + } + while (column.size() > newWidth) + { + updated = true; + column.pop_back(); + } + } + + if (updated) + Update(); +} + +void ListView::AddItem(const std::string& text, int index, int column) +{ + if (column < 0 || column >= columnwidths.size()) + return; + + std::vector newEntry; + for (size_t i = 0u; i < columnwidths.size(); ++i) + newEntry.push_back(""); + + newEntry[column] = text; + if (index >= 0) + { + if (index >= items.size()) + { + newEntry[column] = ""; + while (items.size() < index) + items.push_back(newEntry); + + newEntry[column] = text; + items.push_back(newEntry); + } + else + { + items.insert(items.begin() + index, newEntry); + } + } + else + { + items.push_back(newEntry); + } + scrollbar->SetRanges(GetHeight(), items.size() * getItemHeight()); + Update(); +} + +void ListView::UpdateItem(const std::string& text, int index, int column) +{ + if (index < 0 || index >= items.size() || column < 0 || column >= columnwidths.size()) + return; + + items[index][column] = text; + Update(); +} + +void ListView::RemoveItem(int index) +{ + if (!items.size() || index >= items.size()) + return; + + if (index < 0) + index = static_cast(items.size()) - 1; + + if (selectedItem == index) + SetSelectedItem(0); + + items.erase(items.begin() + index); + scrollbar->SetRanges(GetHeight(), items.size() * getItemHeight()); + Update(); +} + +void ListView::Activate() +{ + if (OnActivated) + OnActivated(); +} + +void ListView::SetSelectedItem(int index) +{ + if (selectedItem != index && index >= 0 && index < items.size()) + { + selectedItem = index; + if (OnChanged) OnChanged(selectedItem); + Update(); + } +} + +double ListView::getItemHeight() +{ + return 20.0; +} + +void ListView::ScrollToItem(int index) +{ + double itemHeight = getItemHeight(); + double y = itemHeight * index; + if (y < scrollbar->GetPosition()) + { + scrollbar->SetPosition(y); + } + else if (y + itemHeight > scrollbar->GetPosition() + GetHeight()) + { + scrollbar->SetPosition(std::max(y + itemHeight - GetHeight(), 0.0)); + } +} + +void ListView::OnScrollbarScroll() +{ + Update(); +} + +void ListView::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + double sw = scrollbar->GetPreferredWidth(); + scrollbar->SetFrameGeometry(Rect::xywh(w - sw, 0.0, sw, h)); + scrollbar->SetRanges(h, items.size() * getItemHeight()); +} + +void ListView::OnPaint(Canvas* canvas) +{ + double y = -scrollbar->GetPosition(); + double x = 2.0; + double w = GetWidth() - scrollbar->GetPreferredWidth() - 2.0; + double h = getItemHeight(); + + Colorf textColor = GetStyleColor("color"); + Colorf selectionColor = GetStyleColor("selection-color"); + + // Make sure the text doesn't enter the scrollbar's area. + canvas->pushClip({ 0.0, 0.0, w, GetHeight() }); + + int index = 0; + for (const std::vector& item : items) + { + double itemY = y; + if (itemY + h >= 0.0 && itemY < GetHeight()) + { + if (index == selectedItem) + { + canvas->fillRect(Rect::xywh(x - 2.0, itemY, w, h), selectionColor); + } + double cx = x; + for (size_t entry = 0u; entry < item.size(); ++entry) + { + canvas->drawText(Point(cx, y + 15.0), textColor, item[entry]); + cx += columnwidths[entry]; + } + } + y += h; + index++; + } + + canvas->popClip(); +} + +bool ListView::OnMouseDown(const Point& pos, InputKey key) +{ + SetFocus(); + + if (key == InputKey::LeftMouse) + { + int index = (int)((pos.y - 5.0 + scrollbar->GetPosition()) / getItemHeight()); + if (index >= 0 && (size_t)index < items.size()) + { + SetSelectedItem(index); + ScrollToItem(selectedItem); + } + } + return true; +} + +bool ListView::OnMouseDoubleclick(const Point& pos, InputKey key) +{ + if (key == InputKey::LeftMouse) + { + Activate(); + } + return true; +} + +bool ListView::OnMouseWheel(const Point& pos, InputKey key) +{ + if (key == InputKey::MouseWheelUp) + { + scrollbar->SetPosition(std::max(scrollbar->GetPosition() - getItemHeight(), 0.0)); + } + else if (key == InputKey::MouseWheelDown) + { + scrollbar->SetPosition(std::min(scrollbar->GetPosition() + getItemHeight(), scrollbar->GetMax())); + } + return true; +} + +void ListView::OnKeyDown(InputKey key) +{ + if (key == InputKey::Down) + { + if (selectedItem + 1 < (int)items.size()) + { + SetSelectedItem(selectedItem + 1); + } + ScrollToItem(selectedItem); + } + else if (key == InputKey::Up) + { + if (selectedItem > 0) + { + SetSelectedItem(selectedItem - 1); + } + ScrollToItem(selectedItem); + } + else if (key == InputKey::Enter) + { + Activate(); + } + else if (key == InputKey::Home) + { + if (selectedItem > 0) + { + SetSelectedItem(0); + } + ScrollToItem(selectedItem); + } + else if (key == InputKey::End) + { + if (selectedItem + 1 < (int)items.size()) + { + SetSelectedItem((int)items.size() - 1); + } + ScrollToItem(selectedItem); + } + else if (key == InputKey::PageUp) + { + double h = GetHeight(); + if (h <= 0.0 || items.empty()) + return; + int itemsPerPage = (int)std::max(std::round(h / getItemHeight()), 1.0); + int nextItem = std::max(selectedItem - itemsPerPage, 0); + if (nextItem != selectedItem) + { + SetSelectedItem(nextItem); + ScrollToItem(selectedItem); + } + } + else if (key == InputKey::PageDown) + { + double h = GetHeight(); + if (h <= 0.0 || items.empty()) + return; + int itemsPerPage = (int)std::max(std::round(h / getItemHeight()), 1.0); + int prevItem = std::min(selectedItem + itemsPerPage, (int)items.size() - 1); + if (prevItem != selectedItem) + { + SetSelectedItem(prevItem); + ScrollToItem(selectedItem); + } + } +} diff --git a/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp b/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp new file mode 100644 index 000000000..a59680a4e --- /dev/null +++ b/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp @@ -0,0 +1,44 @@ + +#include "widgets/mainwindow/mainwindow.h" +#include "widgets/menubar/menubar.h" +#include "widgets/toolbar/toolbar.h" +#include "widgets/statusbar/statusbar.h" + +MainWindow::MainWindow(RenderAPI api) : Widget(nullptr, WidgetType::Window, api) +{ + MenubarWidget = new Menubar(this); + TopToolbarWidget = new Toolbar(this); + TopToolbarWidget->SetDirection(ToolbarDirection::Horizontal); + LeftToolbarWidget = new Toolbar(this); + LeftToolbarWidget->SetDirection(ToolbarDirection::Vertical); + 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); + TopToolbarWidget->SetFrameGeometry(0.0, 32.0, s.width, 32.0); + LeftToolbarWidget->SetFrameGeometry(0.0, 64.0, 32.0, s.height - 64.0); + StatusbarWidget->SetFrameGeometry(0.0, s.height - 32.0, s.width, 32.0); + + if (CentralWidget) + CentralWidget->SetFrameGeometry(32.0, 64.0, s.width - 32.0, s.height - 64.0 - 32.0); +} diff --git a/libraries/ZWidget/src/widgets/menubar/menubar.cpp b/libraries/ZWidget/src/widgets/menubar/menubar.cpp new file mode 100644 index 000000000..37353756f --- /dev/null +++ b/libraries/ZWidget/src/widgets/menubar/menubar.cpp @@ -0,0 +1,390 @@ + +#include "widgets/menubar/menubar.h" +#include "core/colorf.h" + +Menubar::Menubar(Widget* parent) : Widget(parent) +{ + SetStyleClass("menubar"); +} + +Menubar::~Menubar() +{ +} + +MenubarItem* Menubar::AddItem(std::string text, std::function onOpen, bool alignRight) +{ + auto item = new MenubarItem(this, text, alignRight); + item->SetOpenCallback(std::move(onOpen)); + menuItems.push_back(item); + OnGeometryChanged(); + return item; +} + +void Menubar::ShowMenu(MenubarItem* item) +{ + int index = GetItemIndex(item); + if (index == currentMenubarItem) + return; + + CloseMenu(); + SetFocus(); + SetModalCapture(); + currentMenubarItem = index; + if (currentMenubarItem != -1) + menuItems[currentMenubarItem]->SetStyleState("hover"); + modalMode = true; + if (item->GetOpenCallback()) + { + openMenu = new Menu(this); + openMenu->onCloseMenu = [=]() { CloseMenu(); }; + item->GetOpenCallback()(openMenu); + if (item->AlignRight) + openMenu->SetRightPosition(item->MapToGlobal(Point(item->GetWidth(), item->GetHeight()))); + else + openMenu->SetLeftPosition(item->MapToGlobal(Point(0.0, item->GetHeight()))); + openMenu->Show(); + } +} + +void Menubar::CloseMenu() +{ + if (currentMenubarItem != -1) + menuItems[currentMenubarItem]->SetStyleState(""); + currentMenubarItem = -1; + delete openMenu; + openMenu = nullptr; + ReleaseModalCapture(); + modalMode = false; +} + +void Menubar::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + double left = 0.0; + double right = w; + for (MenubarItem* item : menuItems) + { + double itemwidth = item->GetPreferredWidth(); + itemwidth += 16.0; + if (!item->AlignRight) + { + item->SetFrameGeometry(left, 0.0, itemwidth, h); + left += itemwidth; + } + else + { + right -= itemwidth; + item->SetFrameGeometry(right, 0.0, itemwidth, h); + } + } +} + +MenubarItem* Menubar::GetMenubarItemAt(const Point& pos) +{ + Widget* widget = ChildAt(pos); + for (MenubarItem* item : menuItems) + { + if (widget == item) + return item; + } + return nullptr; +} + +bool Menubar::OnMouseDown(const Point& pos, InputKey key) +{ + if (!modalMode) + return Widget::OnMouseDown(pos, key); + + MenubarItem* item = GetMenubarItemAt(pos); + if (item) + ShowMenu(item); + else + CloseMenu(); + return true; +} + +bool Menubar::OnMouseUp(const Point& pos, InputKey key) +{ + if (!modalMode) + return Widget::OnMouseUp(pos, key); + + MenubarItem* item = GetMenubarItemAt(pos); + if (!item) + CloseMenu(); + return true; +} + +void Menubar::OnMouseMove(const Point& pos) +{ + if (!modalMode) + return Widget::OnMouseMove(pos); + + MenubarItem* item = GetMenubarItemAt(pos); + if (item) + ShowMenu(item); +} + +int Menubar::GetItemIndex(MenubarItem* item) +{ + int i = 0; + for (MenubarItem* cur : menuItems) + { + if (cur == item) + return i; + i++; + } + return -1; +} + +void Menubar::OnKeyDown(InputKey key) +{ + if (!modalMode) + return Widget::OnKeyDown(key); + + if (key == InputKey::Left) + { + if (!menuItems.empty()) + { + int index = currentMenubarItem - 1; + if (index < 0) + index = (int)menuItems.size() - 1; + ShowMenu(menuItems[index]); + } + } + else if (key == InputKey::Right) + { + if (!menuItems.empty()) + { + int index = currentMenubarItem + 1; + if (index == (int)menuItems.size()) + index = 0; + ShowMenu(menuItems[index]); + } + } + else if (key == InputKey::Up) + { + if (openMenu) + { + // Keep trying until we don't find a separator + for (int i = 0; i < 10; i++) + { + if (!openMenu->selectedItem || !openMenu->selectedItem->PrevSibling()) + { + if (openMenu->LastChild()) + openMenu->SetSelected(static_cast(openMenu->LastChild())); + } + else + { + openMenu->SetSelected(static_cast(openMenu->selectedItem->PrevSibling())); + } + + if (openMenu->selectedItem && openMenu->selectedItem->GetStyleClass() != "menuitemseparator") + break; + } + } + } + else if (key == InputKey::Down) + { + if (openMenu) + { + // Keep trying until we don't find a separator + for (int i = 0; i < 10; i++) + { + if (!openMenu->selectedItem || !openMenu->selectedItem->NextSibling()) + { + if (openMenu->FirstChild()) + openMenu->SetSelected(static_cast(openMenu->FirstChild())); + } + else + { + openMenu->SetSelected(static_cast(openMenu->selectedItem->NextSibling())); + } + + if (openMenu->selectedItem && openMenu->selectedItem->GetStyleClass() != "menuitemseparator") + break; + } + } + } + else if (key == InputKey::Enter) + { + if (openMenu && openMenu->selectedItem) + openMenu->selectedItem->Click(); + } +} + +///////////////////////////////////////////////////////////////////////////// + +MenubarItem::MenubarItem(Menubar* menubar, std::string text, bool alignRight) : Widget(menubar), menubar(menubar), text(text), AlignRight(alignRight) +{ + SetStyleClass("menubaritem"); +} + +bool MenubarItem::OnMouseDown(const Point& pos, InputKey key) +{ + menubar->ShowMenu(this); + return true; +} + +bool MenubarItem::OnMouseUp(const Point& pos, InputKey key) +{ + return true; +} + +void MenubarItem::OnMouseMove(const Point& pos) +{ + if (GetStyleState().empty()) + { + SetStyleState("hover"); + } +} + +void MenubarItem::OnMouseLeave() +{ + SetStyleState(""); +} + +double MenubarItem::GetPreferredWidth() const +{ + Canvas* canvas = GetCanvas(); + return canvas->measureText(text).width; +} + +void MenubarItem::OnPaint(Canvas* canvas) +{ + double x = (GetWidth() - canvas->measureText(text).width) * 0.5; + canvas->drawText(Point(x, 21.0), GetStyleColor("color"), text); +} + +///////////////////////////////////////////////////////////////////////////// + +Menu::Menu(Widget* parent) : Widget(parent, WidgetType::Popup) +{ + SetStyleClass("menu"); +} + +void Menu::SetLeftPosition(const Point& pos) +{ + SetFrameGeometry(Rect::xywh(pos.x, pos.y, GetPreferredWidth() + GetNoncontentLeft() + GetNoncontentRight(), GetPreferredHeight() + GetNoncontentTop() + GetNoncontentBottom())); +} + +void Menu::SetRightPosition(const Point& pos) +{ + SetFrameGeometry(Rect::xywh(pos.x - GetWidth() - GetNoncontentLeft() - GetNoncontentRight(), pos.y, GetWidth() + GetNoncontentLeft() + GetNoncontentRight(), GetHeight() + GetNoncontentTop() + GetNoncontentBottom())); +} + +MenuItem* Menu::AddItem(std::shared_ptr icon, std::string text, std::function onClick) +{ + auto item = new MenuItem(this, [this, onClick]() { if (onCloseMenu) onCloseMenu(); if (onClick) onClick(); }); + if (icon) + item->icon->SetImage(icon); + item->text->SetText(text); + return item; +} + +MenuItemSeparator* Menu::AddSeparator() +{ + auto sep = new MenuItemSeparator(this); + return sep; +} + +double Menu::GetPreferredWidth() const +{ + return GridFitSize(200.0); +} + +double Menu::GetPreferredHeight() const +{ + double h = 0.0; + for (Widget* item = FirstChild(); item != nullptr; item = item->NextSibling()) + { + double itemheight = GridFitSize((item->GetStyleClass() == "menuitemseparator") ? 7.0 : 30.0); + h += itemheight; + } + return h; +} + +void Menu::OnGeometryChanged() +{ + double w = GetWidth(); + double y = 0.0; + for (Widget* item = FirstChild(); item != nullptr; item = item->NextSibling()) + { + double itemheight = GridFitSize((item->GetStyleClass() == "menuitemseparator") ? 7.0 : 30.0); + item->SetFrameGeometry(Rect::xywh(0.0, y, w, itemheight)); + y += itemheight; + } +} + +void Menu::SetSelected(MenuItem* item) +{ + if (selectedItem) + { + selectedItem->SetStyleState(""); + } + selectedItem = item; + if (selectedItem) + { + selectedItem->SetStyleState("hover"); + } +} + +///////////////////////////////////////////////////////////////////////////// + +MenuItem::MenuItem(Menu* menu, std::function onClick) : Widget(menu), menu(menu), onClick(onClick) +{ + SetStyleClass("menuitem"); + icon = new ImageBox(this); + text = new TextLabel(this); +} + +void MenuItem::OnMouseMove(const Point& pos) +{ + menu->SetSelected(this); +} + +bool MenuItem::OnMouseUp(const Point& pos, InputKey key) +{ + Click(); + return true; +} + +void MenuItem::Click() +{ + if (onClick) + { + // We have to make a copy of the handler as it may delete 'this' + auto handler = onClick; + handler(); + } +} + +void MenuItem::OnMouseLeave() +{ + menu->SetSelected(nullptr); +} + +void MenuItem::OnGeometryChanged() +{ + double iconwidth = GridFitSize(icon->GetPreferredWidth()); + double iconheight = GridFitSize(icon->GetPreferredHeight()); + double w = GetWidth(); + double h = GetHeight(); + double textheight = 19.0; + double x0 = GridFitPoint(5.0); + double x1 = GridFitPoint(5.0 + iconwidth); + icon->SetFrameGeometry(Rect::xywh(x0, GridFitPoint((h - iconheight) * 0.5), iconwidth, iconheight)); + text->SetFrameGeometry(Rect::xywh(x1, GridFitPoint((h - textheight) * 0.5), w - x1, textheight)); +} + +///////////////////////////////////////////////////////////////////////////// + +MenuItemSeparator::MenuItemSeparator(Widget* parent) : Widget(parent) +{ + SetStyleClass("menuitemseparator"); +} + +void MenuItemSeparator::OnPaint(Canvas* canvas) +{ + canvas->fillRect(Rect::xywh(0.0, GridFitPoint(GetHeight() * 0.5), GetWidth(), GridFitSize(1.0)), Colorf::fromRgba8(75, 75, 75)); +} diff --git a/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp b/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp new file mode 100644 index 000000000..e85b2c557 --- /dev/null +++ b/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp @@ -0,0 +1,88 @@ + +#include "widgets/pushbutton/pushbutton.h" + +PushButton::PushButton(Widget* parent) : Widget(parent) +{ + SetStyleClass("pushbutton"); +} + +void PushButton::SetText(const std::string& value) +{ + if (text != value) + { + text = value; + Update(); + } +} + +const std::string& PushButton::GetText() const +{ + return text; +} + +double PushButton::GetPreferredHeight() const +{ + return 30.0; +} + +void PushButton::OnPaint(Canvas* canvas) +{ + Rect box = canvas->measureText(text); + canvas->drawText(Point((GetWidth() - box.width) * 0.5, GetHeight() - 5.0), GetStyleColor("color"), text); +} + +void PushButton::OnMouseMove(const Point& pos) +{ + if (GetStyleState().empty()) + { + SetStyleState("hover"); + } +} + +bool PushButton::OnMouseDown(const Point& pos, InputKey key) +{ + SetFocus(); + SetStyleState("down"); + return true; +} + +bool PushButton::OnMouseUp(const Point& pos, InputKey key) +{ + if (GetStyleState() == "down") + { + SetStyleState(""); + Repaint(); + Click(); + } + return true; +} + +void PushButton::OnMouseLeave() +{ + SetStyleState(""); +} + +void PushButton::OnKeyDown(InputKey key) +{ + if (key == InputKey::Space || key == InputKey::Enter) + { + SetStyleState("down"); + Update(); + } +} + +void PushButton::OnKeyUp(InputKey key) +{ + if (key == InputKey::Space || key == InputKey::Enter) + { + SetStyleState(""); + Repaint(); + Click(); + } +} + +void PushButton::Click() +{ + if (OnClick) + OnClick(); +} diff --git a/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp b/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp new file mode 100644 index 000000000..c4e29329a --- /dev/null +++ b/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp @@ -0,0 +1,409 @@ + +#include "widgets/scrollbar/scrollbar.h" +#include "core/colorf.h" +#include + +Scrollbar::Scrollbar(Widget* parent) : Widget(parent) +{ + SetStyleClass("scrollbar"); + UpdatePartPositions(); + + mouse_down_timer = new Timer(this); + mouse_down_timer->FuncExpired = [=]() { OnTimerExpired(); }; +} + +Scrollbar::~Scrollbar() +{ +} + +bool Scrollbar::IsVertical() const +{ + return vertical; +} + +bool Scrollbar::IsHorizontal() const +{ + return !vertical; +} + +double Scrollbar::GetMin() const +{ + return scroll_min; +} + +double Scrollbar::GetMax() const +{ + return scroll_max; +} + +double Scrollbar::GetLineStep() const +{ + return line_step; +} + +double Scrollbar::GetPageStep() const +{ + return page_step; +} + +double Scrollbar::GetPosition() const +{ + return position; +} + +void Scrollbar::SetVertical() +{ + vertical = true; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetHorizontal() +{ + vertical = false; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetMin(double new_scroll_min) +{ + SetRanges(new_scroll_min, scroll_max, line_step, page_step); +} + +void Scrollbar::SetMax(double new_scroll_max) +{ + SetRanges(scroll_min, new_scroll_max, line_step, page_step); +} + +void Scrollbar::SetLineStep(double step) +{ + SetRanges(scroll_min, scroll_max, step, page_step); +} + +void Scrollbar::SetPageStep(double step) +{ + SetRanges(scroll_min, scroll_max, line_step, step); +} + +void Scrollbar::SetRanges(double new_scroll_min, double new_scroll_max, double new_line_step, double new_page_step) +{ + if (new_scroll_min >= new_scroll_max || new_line_step <= 0.0 || new_page_step <= 0.0) + throw std::runtime_error("Scrollbar ranges out of bounds!"); + scroll_min = new_scroll_min; + scroll_max = new_scroll_max; + line_step = new_line_step; + page_step = new_page_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::SetRanges(double view_size, double total_size) +{ + if (view_size <= 0.0 || total_size <= 0.0) + { + SetRanges(0.0, 1.0, 1.0, 1.0); + } + else + { + double scroll_max = std::max(1.0, total_size - view_size + 1.0); + double page_step = std::max(1.0, view_size); + SetRanges(0.0, scroll_max, 10, page_step); + } +} + +void Scrollbar::SetPosition(double pos) +{ + position = pos; + if (pos >= scroll_max) + position = scroll_max - 1.0; + if (pos < scroll_min) + position = scroll_min; + + if (UpdatePartPositions()) + Update(); +} + +void Scrollbar::OnMouseMove(const Point& pos) +{ + if (mouse_down_mode == mouse_down_thumb_drag) + { + double last_position = position; + + if (pos.x < -100.0 || pos.x > GetWidth() + 100.0 || pos.y < -100.0 || pos.y > GetHeight() + 100.0) + { + position = thumb_start_position; + } + else + { + double delta = vertical ? (pos.y - mouse_drag_start_pos.y) : (pos.x - mouse_drag_start_pos.x); + double position_pixels = thumb_start_pixel_position + delta; + + double track_height = 0; + if (vertical) + track_height = rect_track_decrement.height + rect_track_increment.height; + else + track_height = rect_track_decrement.width + rect_track_increment.width; + + if (track_height != 0.0) + position = scroll_min + position_pixels * (scroll_max - scroll_min) / track_height; + else + position = 0; + + if (position >= scroll_max) + position = scroll_max - 1; + if (position < scroll_min) + position = scroll_min; + + } + + if (position != last_position) + { + InvokeScrollEvent(&FuncScrollThumbTrack); + UpdatePartPositions(); + } + } + + Update(); +} + +bool Scrollbar::OnMouseDown(const Point& pos, InputKey key) +{ + mouse_drag_start_pos = pos; + + if (rect_button_decrement.contains(pos)) + { + mouse_down_mode = mouse_down_button_decr; + FuncScrollOnMouseDown = &FuncScrollLineDecrement; + + double last_position = position; + + position -= line_step; + last_step_size = -line_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + + if (last_position != position) + InvokeScrollEvent(&FuncScrollLineDecrement); + } + else if (rect_button_increment.contains(pos)) + { + mouse_down_mode = mouse_down_button_incr; + FuncScrollOnMouseDown = &FuncScrollLineIncrement; + + double last_position = position; + + position += line_step; + last_step_size = line_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + + if (last_position != position) + InvokeScrollEvent(&FuncScrollLineIncrement); + } + else if (rect_thumb.contains(pos)) + { + mouse_down_mode = mouse_down_thumb_drag; + thumb_start_position = position; + thumb_start_pixel_position = vertical ? (rect_thumb.y - rect_track_decrement.y) : (rect_thumb.x - rect_track_decrement.x); + } + else if (rect_track_decrement.contains(pos)) + { + mouse_down_mode = mouse_down_track_decr; + FuncScrollOnMouseDown = &FuncScrollPageDecrement; + + double last_position = position; + + position -= page_step; + last_step_size = -page_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + + if (last_position != position) + InvokeScrollEvent(&FuncScrollPageDecrement); + } + else if (rect_track_increment.contains(pos)) + { + mouse_down_mode = mouse_down_track_incr; + FuncScrollOnMouseDown = &FuncScrollPageIncrement; + + double last_position = position; + + position += page_step; + last_step_size = page_step; + if (position >= scroll_max) + position = scroll_max - 1.0; + if (position < scroll_min) + position = scroll_min; + + if (last_position != position) + InvokeScrollEvent(&FuncScrollPageIncrement); + } + + mouse_down_timer->Start(100, false); + + UpdatePartPositions(); + + Update(); + SetPointerCapture(); + return true; +} + +bool Scrollbar::OnMouseUp(const Point& pos, InputKey key) +{ + if (mouse_down_mode == mouse_down_thumb_drag) + { + if (FuncScrollThumbRelease) + FuncScrollThumbRelease(); + } + + mouse_down_mode = mouse_down_none; + mouse_down_timer->Stop(); + + Update(); + ReleasePointerCapture(); + return true; +} + +void Scrollbar::OnMouseLeave() +{ + Update(); +} + +void Scrollbar::OnGeometryChanged() +{ + UpdatePartPositions(); +} + +void Scrollbar::OnPaint(Canvas* canvas) +{ + /* + part_button_decrement.render_box(canvas, rect_button_decrement); + part_track_decrement.render_box(canvas, rect_track_decrement); + part_thumb.render_box(canvas, rect_thumb); + part_thumb_gripper.render_box(canvas, rect_thumb); + part_track_increment.render_box(canvas, rect_track_increment); + part_button_increment.render_box(canvas, rect_button_increment); + */ + + canvas->fillRect(Rect::shrink(Rect::xywh(0.0, 0.0, GetWidth(), GetHeight()), 4.0, 0.0, 4.0, 0.0), GetStyleColor("track-color")); + canvas->fillRect(Rect::shrink(rect_thumb, 4.0, 0.0, 4.0, 0.0), GetStyleColor("thumb-color")); +} + +// Calculates positions of all parts. Returns true if thumb position was changed compared to previously, false otherwise. +bool Scrollbar::UpdatePartPositions() +{ + double decr_height = showbuttons ? 16.0 : 0.0; + double incr_height = showbuttons ? 16.0 : 0.0; + + double total_height = vertical ? GetHeight() : GetWidth(); + double track_height = std::max(0.0, total_height - decr_height - incr_height); + double thumb_height = CalculateThumbSize(track_height); + + double thumb_offset = decr_height + CalculateThumbPosition(thumb_height, track_height); + + Rect previous_rect_thumb = rect_thumb; + + rect_button_decrement = CreateRect(0.0, decr_height); + rect_track_decrement = CreateRect(decr_height, thumb_offset); + rect_thumb = CreateRect(thumb_offset, thumb_offset + thumb_height); + rect_track_increment = CreateRect(thumb_offset + thumb_height, decr_height + track_height); + rect_button_increment = CreateRect(decr_height + track_height, decr_height + track_height + incr_height); + + return (previous_rect_thumb != rect_thumb); +} + +double Scrollbar::CalculateThumbSize(double track_size) +{ + double minimum_thumb_size = 20.0; + double range = scroll_max - scroll_min; + double length = range + page_step - 1; + double thumb_size = page_step * track_size / length; + if (thumb_size < minimum_thumb_size) + thumb_size = minimum_thumb_size; + if (thumb_size > track_size) + thumb_size = track_size; + return thumb_size; +} + +double Scrollbar::CalculateThumbPosition(double thumb_size, double track_size) +{ + double relative_pos = position - scroll_min; + double range = scroll_max - scroll_min - 1; + if (range != 0) + { + double available_area = std::max(0.0, track_size - thumb_size); + return relative_pos * available_area / range; + } + else + { + return 0; + } +} + +Rect Scrollbar::CreateRect(double start, double end) +{ + if (vertical) + return Rect(0.0, start, GetWidth(), end - start); + else + return Rect(start, 0.0, end - start, GetHeight()); +} + +void Scrollbar::OnTimerExpired() +{ + if (mouse_down_mode == mouse_down_thumb_drag) + return; + + mouse_down_timer->Start(100, false); + + double last_position = position; + position += last_step_size; + if (position >= scroll_max) + position = scroll_max - 1; + + if (position < scroll_min) + position = scroll_min; + + if (position != last_position) + { + InvokeScrollEvent(FuncScrollOnMouseDown); + + if (UpdatePartPositions()) + Update(); + } +} + +void Scrollbar::OnEnableChanged() +{ + Update(); +} + +void Scrollbar::InvokeScrollEvent(std::function* event_ptr) +{ + if (position == scroll_max - 1) + { + if (FuncScrollMax) + FuncScrollMax(); + } + + if (position == scroll_min) + { + if (FuncScrollMin) + FuncScrollMin(); + } + + if (FuncScroll) + FuncScroll(); + + if (event_ptr && *event_ptr) + (*event_ptr)(); +} diff --git a/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp b/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp new file mode 100644 index 000000000..33faa15c3 --- /dev/null +++ b/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp @@ -0,0 +1,21 @@ + +#include "widgets/statusbar/statusbar.h" +#include "widgets/lineedit/lineedit.h" +#include "core/colorf.h" + +Statusbar::Statusbar(Widget* parent) : Widget(parent) +{ + SetStyleClass("statusbar"); + + CommandEdit = new LineEdit(this); + CommandEdit->SetFrameGeometry(Rect::xywh(90.0, 4.0, 400.0, 23.0)); +} + +Statusbar::~Statusbar() +{ +} + +void Statusbar::OnPaint(Canvas* canvas) +{ + canvas->drawText(Point(16.0, 21.0), Colorf::fromRgba8(226, 223, 219), "Command:"); +} diff --git a/libraries/ZWidget/src/widgets/tabwidget/tabwidget.cpp b/libraries/ZWidget/src/widgets/tabwidget/tabwidget.cpp new file mode 100644 index 000000000..ef935a9e8 --- /dev/null +++ b/libraries/ZWidget/src/widgets/tabwidget/tabwidget.cpp @@ -0,0 +1,345 @@ + +#include "widgets/tabwidget/tabwidget.h" +#include "widgets/textlabel/textlabel.h" +#include "widgets/imagebox/imagebox.h" +#include + +TabWidget::TabWidget(Widget* parent) : Widget(parent) +{ + Bar = new TabBar(this); + PageStack = new TabWidgetStack(this); + + Bar->OnCurrentChanged = [=]() { OnBarCurrentChanged(); }; +} + +int TabWidget::AddTab(Widget* page, const std::string& label) +{ + return AddTab(page, nullptr, label); +} + +int TabWidget::AddTab(Widget* page, const std::shared_ptr& icon, const std::string& label) +{ + int pageIndex = Bar->AddTab(label); + page->SetParent(PageStack); + page->SetVisible(false); + Pages.push_back(page); + if (Pages.size() == 1) + { + PageStack->SetCurrentWidget(page); + } + return pageIndex; +} + +void TabWidget::SetTabText(int index, const std::string& text) +{ + Bar->SetTabText(index, text); +} + +void TabWidget::SetTabText(Widget* page, const std::string& text) +{ + int index = GetPageIndex(page); + if (index != -1) + SetTabText(index, text); +} + +void TabWidget::SetTabIcon(int index, const std::shared_ptr& icon) +{ + Bar->SetTabIcon(index, icon); +} + +void TabWidget::SetTabIcon(Widget* page, const std::shared_ptr& icon) +{ + int index = GetPageIndex(page); + if (index != -1) + SetTabIcon(index, icon); +} + +int TabWidget::GetCurrentIndex() const +{ + return Bar->GetCurrentIndex(); +} + +Widget* TabWidget::GetCurrentWidget() const +{ + return Pages[Bar->GetCurrentIndex()]; +} + +void TabWidget::SetCurrentIndex(int pageIndex) +{ + if (Bar->GetCurrentIndex() != pageIndex) + { + Bar->SetCurrentIndex(pageIndex); + PageStack->SetCurrentWidget(Pages[pageIndex]); + } +} + +void TabWidget::SetCurrentWidget(Widget* pageWidget) +{ + int pageIndex = GetPageIndex(pageWidget); + if (pageIndex != -1) + SetCurrentIndex(pageIndex); +} + +int TabWidget::GetPageIndex(Widget* pageWidget) const +{ + for (size_t i = 0; i < Pages.size(); i++) + { + if (Pages[i] == pageWidget) + return (int)i; + } + return -1; +} + +void TabWidget::OnBarCurrentChanged() +{ + int pageIndex = Bar->GetCurrentIndex(); + PageStack->SetCurrentWidget(Pages[pageIndex]); + if (OnCurrentChanged) + OnCurrentChanged(); +} + +void TabWidget::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + double barHeight = Bar->GetPreferredHeight(); + Bar->SetFrameGeometry(Rect::xywh(0.0, 0.0, w, barHeight)); + PageStack->SetFrameGeometry(Rect::xywh(0.0, barHeight, w, std::max(h - barHeight, 0.0))); +} + +///////////////////////////////////////////////////////////////////////////// + +TabBar::TabBar(Widget* parent) : Widget(parent) +{ + SetStyleClass("tabbar"); + + leftSpacer = new TabBarSpacer(this); + rightSpacer = new TabBarSpacer(this); +} + +int TabBar::AddTab(const std::string& label) +{ + return AddTab(nullptr, label); +} + +int TabBar::AddTab(const std::shared_ptr& icon, const std::string& label) +{ + TabBarTab* tab = new TabBarTab(this); + tab->SetIcon(icon); + tab->SetText(label); + tab->OnClick = [=]() { OnTabClicked(tab); }; + int pageIndex = (int)Tabs.size(); + Tabs.push_back(tab); + if (CurrentIndex == -1) + SetCurrentIndex(pageIndex); + OnGeometryChanged(); + return pageIndex; +} + +void TabBar::SetTabText(int index, const std::string& text) +{ + Tabs[index]->SetText(text); + OnGeometryChanged(); +} + +void TabBar::SetTabIcon(int index, const std::shared_ptr& icon) +{ + Tabs[index]->SetIcon(icon); + OnGeometryChanged(); +} + +int TabBar::GetCurrentIndex() const +{ + return CurrentIndex; +} + +void TabBar::SetCurrentIndex(int pageIndex) +{ + if (CurrentIndex != pageIndex) + { + if (CurrentIndex != -1) + Tabs[CurrentIndex]->SetCurrent(false); + CurrentIndex = pageIndex; + if (CurrentIndex != -1) + Tabs[CurrentIndex]->SetCurrent(true); + } +} + +void TabBar::OnTabClicked(TabBarTab* tab) +{ + int pageIndex = GetTabIndex(tab); + if (CurrentIndex != pageIndex) + { + SetCurrentIndex(pageIndex); + if (OnCurrentChanged) + OnCurrentChanged(); + } +} + +int TabBar::GetTabIndex(TabBarTab* tab) +{ + for (size_t i = 0; i < Tabs.size(); i++) + { + if (Tabs[i] == tab) + return (int)i; + } + return -1; +} + +void TabBar::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + double x = 0.0; + + leftSpacer->SetFrameGeometry(Rect::xywh(x, 0.0, GetStyleDouble("spacer-left"), h)); + x += GetStyleDouble("spacer-left"); + + for (TabBarTab* tab : Tabs) + { + double tabWidth = tab->GetNoncontentLeft() + tab->GetPreferredWidth() + tab->GetNoncontentRight(); + tab->SetFrameGeometry(Rect::xywh(x, 0.0, tabWidth, h)); + x += tabWidth; + } + + rightSpacer->SetFrameGeometry(Rect::xywh(x, 0.0, std::max(w - x, 0.0), h)); +} + +///////////////////////////////////////////////////////////////////////////// + +TabBarSpacer::TabBarSpacer(Widget* parent) : Widget(parent) +{ + SetStyleClass("tabbar-spacer"); +} + +///////////////////////////////////////////////////////////////////////////// + +TabBarTab::TabBarTab(Widget* parent) : Widget(parent) +{ + SetStyleClass("tabbar-tab"); +} + +void TabBarTab::SetText(const std::string& text) +{ + if (!text.empty()) + { + if (!Label) + { + Label = new TextLabel(this); + OnGeometryChanged(); + } + Label->SetText(text); + } + else + { + delete Label; + Label = nullptr; + OnGeometryChanged(); + } +} + +void TabBarTab::SetIcon(const std::shared_ptr& image) +{ + if (image) + { + if (!Icon) + { + Icon = new ImageBox(this); + OnGeometryChanged(); + } + Icon->SetImage(image); + } + else + { + delete Icon; + Icon = nullptr; + OnGeometryChanged(); + } +} + +void TabBarTab::SetCurrent(bool value) +{ + if (IsCurrent != value) + { + IsCurrent = value; + SetStyleState(value ? "active" : ""); + } +} + +double TabBarTab::GetPreferredWidth() const +{ + double x = Icon ? 32.0 + 5.0 : 0.0; + if (Label) x += Label->GetPreferredWidth(); + return x; +} + +void TabBarTab::OnGeometryChanged() +{ + double x = 0.0; + double w = GetWidth(); + double h = GetHeight(); + if (Icon) + { + Icon->SetFrameGeometry(Rect::xywh(x, (h - 32.0) * 0.5, 32.0, 32.0)); + x = 32.0 + 5.0; + } + if (Label) + { + Label->SetFrameGeometry(Rect::xywh(x, (h - Label->GetPreferredHeight()) * 0.5, std::max(w - x, 0.0), Label->GetPreferredHeight())); + } +} + +void TabBarTab::OnMouseMove(const Point& pos) +{ + if (GetStyleState().empty()) + { + SetStyleState("hover"); + } +} + +bool TabBarTab::OnMouseDown(const Point& pos, InputKey key) +{ + if (OnClick) + OnClick(); + return true; +} + +bool TabBarTab::OnMouseUp(const Point& pos, InputKey key) +{ + return true; +} + +void TabBarTab::OnMouseLeave() +{ + if (GetStyleState() == "hover") + SetStyleState(""); + Update(); +} + +///////////////////////////////////////////////////////////////////////////// + +TabWidgetStack::TabWidgetStack(Widget* parent) : Widget(parent) +{ + SetStyleClass("tabwidget-stack"); +} + +void TabWidgetStack::SetCurrentWidget(Widget* widget) +{ + if (widget != CurrentWidget) + { + if (CurrentWidget) + CurrentWidget->SetVisible(false); + CurrentWidget = widget; + if (CurrentWidget) + { + CurrentWidget->SetVisible(true); + OnGeometryChanged(); + } + } +} + +void TabWidgetStack::OnGeometryChanged() +{ + if (CurrentWidget) + CurrentWidget->SetFrameGeometry(Rect::xywh(0.0, 0.0, GetWidth(), GetHeight())); +} diff --git a/libraries/ZWidget/src/widgets/textedit/textedit.cpp b/libraries/ZWidget/src/widgets/textedit/textedit.cpp new file mode 100644 index 000000000..2381332a1 --- /dev/null +++ b/libraries/ZWidget/src/widgets/textedit/textedit.cpp @@ -0,0 +1,1045 @@ + +#include "widgets/textedit/textedit.h" +#include "widgets/scrollbar/scrollbar.h" +#include "core/utf8reader.h" +#include "core/colorf.h" +#include + +#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) +{ + SetStyleClass("textedit"); + + 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(); + } + } +} + +bool TextEdit::OnMouseDown(const Point& pos, InputKey key) +{ + if (key == InputKey::LeftMouse) + { + SetPointerCapture(); + mouse_selecting = true; + cursor_pos = GetCharacterIndex(pos); + selection_start = cursor_pos; + selection_length = 0; + + Update(); + } + return true; +} + +bool TextEdit::OnMouseDoubleclick(const Point& pos, InputKey key) +{ + return true; +} + +bool TextEdit::OnMouseUp(const Point& pos, InputKey key) +{ + if (mouse_selecting && key == InputKey::LeftMouse) + { + if (ignore_mouse_events) // This prevents text selection from changing from what was set when focus was gained. + { + ReleasePointerCapture(); + ignore_mouse_events = false; + mouse_selecting = false; + } + else + { + scroll_timer->Stop(); + ReleasePointerCapture(); + mouse_selecting = false; + ivec2 sel_end = GetCharacterIndex(pos); + selection_length = ToOffset(sel_end) - ToOffset(selection_start); + cursor_pos = sel_end; + SetFocus(); + Update(); + } + } + return true; +} + +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(InputKey key) +{ + if (!readonly && key == InputKey::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 == InputKey::Enter || key == InputKey::Escape || key == InputKey::Tab) + { + // Do not consume these. + return; + } + else if (key == InputKey::A && GetKeyState(InputKey::Ctrl)) + { + // select all + SelectAll(); + } + else if (key == InputKey::C && GetKeyState(InputKey::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 == InputKey::Up) + { + if (GetKeyState(InputKey::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(InputKey::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 == InputKey::Down) + { + if (GetKeyState(InputKey::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(InputKey::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 == InputKey::Left) + { + Move(-1, GetKeyState(InputKey::Shift), GetKeyState(InputKey::Ctrl)); + } + else if (key == InputKey::Right) + { + Move(1, GetKeyState(InputKey::Shift), GetKeyState(InputKey::Ctrl)); + } + else if (key == InputKey::Backspace) + { + Backspace(); + } + else if (key == InputKey::Delete) + { + Del(); + } + else if (key == InputKey::Home) + { + if (GetKeyState(InputKey::Ctrl)) + cursor_pos = ivec2(0, 0); + else + cursor_pos.x = 0; + if (GetKeyState(InputKey::Shift)) + selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); + else + ClearSelection(); + Update(); + MoveVerticalScroll(); + } + else if (key == InputKey::End) + { + if (GetKeyState(InputKey::Ctrl)) + cursor_pos = ivec2(lines.back().text.length(), lines.size() - 1); + else + cursor_pos.x = lines[cursor_pos.y].text.size(); + + if (GetKeyState(InputKey::Shift)) + selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); + else + ClearSelection(); + Update(); + } + else if (key == InputKey::X && GetKeyState(InputKey::Ctrl)) + { + std::string str = GetSelection(); + DeleteSelectedText(); + SetClipboardText(str); + } + else if (key == InputKey::V && GetKeyState(InputKey::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(InputKey::Ctrl) && key == InputKey::Z) + { + if (!readonly) + { + std::string tmp = undo_info.undo_text; + undo_info.undo_text = GetText(); + SetText(tmp); + } + } + else if (key == InputKey::Shift) + { + if (selection_length == 0) + selection_start = cursor_pos; + } + + if (FuncAfterEditChanged) + FuncAfterEditChanged(); +} + +void TextEdit::OnKeyUp(InputKey key) +{ +} + +void TextEdit::OnSetFocus() +{ + if (!readonly) + timer->Start(500); + if (select_all_on_focus_gain) + SelectAll(); + ignore_mouse_events = true; + cursor_pos.y = lines.size() - 1; + cursor_pos.x = lines[cursor_pos.y].text.length(); + + Update(); + + if (FuncFocusGained) + FuncFocusGained(); +} + +void TextEdit::OnLostFocus() +{ + timer->Stop(); + ClearSelection(); + + Update(); + + if (FuncFocusLost) + FuncFocusLost(); +} + +void TextEdit::CreateComponents() +{ + vert_scrollbar = new Scrollbar(this); + vert_scrollbar->FuncScroll = [=]() { OnVerticalScroll(); }; + vert_scrollbar->SetVisible(false); + vert_scrollbar->SetVertical(); +} + +void TextEdit::OnVerticalScroll() +{ +} + +void TextEdit::UpdateVerticalScroll() +{ + Rect rect( + GetWidth() - 16.0/*vert_scrollbar->GetWidth()*/, + 0.0, + 16.0/*vert_scrollbar->GetWidth()*/, + GetHeight()); + + vert_scrollbar->SetFrameGeometry(rect); + + double total_height = GetTotalLineHeight(); + double height_per_line = std::max(1.0, total_height / std::max(1.0, (double)lines.size())); + bool visible = total_height > GetHeight(); + vert_scrollbar->SetRanges((int)std::round(GetHeight() / height_per_line), (int)std::round(total_height / height_per_line)); + vert_scrollbar->SetLineStep(1); + vert_scrollbar->SetVisible(visible); + + if (visible == false) + vert_scrollbar->SetPosition(0); +} + +void TextEdit::MoveVerticalScroll() +{ + double total_height = GetTotalLineHeight(); + double height_per_line = std::max(1.0, total_height / std::max((size_t)1, lines.size())); + int lines_fit = (int)(GetHeight() / height_per_line); + if (cursor_pos.y >= vert_scrollbar->GetPosition() + lines_fit) + { + vert_scrollbar->SetPosition(cursor_pos.y - lines_fit + 1); + } + else if (cursor_pos.y < vert_scrollbar->GetPosition()) + { + vert_scrollbar->SetPosition(cursor_pos.y); + } +} + +double TextEdit::GetTotalLineHeight() +{ + double total = 0; + for (std::vector::const_iterator iter = lines.begin(); iter != lines.end(); iter++) + { + total += iter->layout.GetSize().height; + } + return total; +} + +void TextEdit::Move(int steps, bool shift, bool ctrl) +{ + if (shift && selection_length == 0) + selection_start = cursor_pos; + + // Jump over words if control is pressed. + if (ctrl) + { + if (steps < 0 && cursor_pos.x == 0 && cursor_pos.y > 0) + { + cursor_pos.x = (int)lines[cursor_pos.y - 1].text.size(); + cursor_pos.y--; + } + else if (steps > 0 && cursor_pos.x == (int)lines[cursor_pos.y].text.size() && cursor_pos.y + 1 < (int)lines.size()) + { + cursor_pos.x = 0; + cursor_pos.y++; + } + + ivec2 new_pos; + if (steps < 0) + new_pos = FindPreviousBreakCharacter(cursor_pos); + else + new_pos = FindNextBreakCharacter(cursor_pos); + + cursor_pos = new_pos; + } + else if (steps < 0 && cursor_pos.x == 0 && cursor_pos.y > 0) + { + cursor_pos.x = (int)lines[cursor_pos.y - 1].text.size(); + cursor_pos.y--; + } + else if (steps > 0 && cursor_pos.x == (int)lines[cursor_pos.y].text.size() && cursor_pos.y + 1 < (int)lines.size()) + { + cursor_pos.x = 0; + cursor_pos.y++; + } + else + { + UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length()); + utf8_reader.set_position(cursor_pos.x); + if (steps > 0) + { + for (int i = 0; i < steps; i++) + utf8_reader.next(); + } + else if (steps < 0) + { + for (int i = 0; i < -steps; i++) + utf8_reader.prev(); + } + + cursor_pos.x = (int)utf8_reader.position(); + } + + if (shift) + { + selection_length = (int)ToOffset(cursor_pos) - (int)ToOffset(selection_start); + } + else + { + // Clear the selection if a cursor key is pressed but shift isn't down. + selection_start = ivec2(0, 0); + selection_length = 0; + } + + + MoveVerticalScroll(); + Update(); + + undo_info.first_text_insert = true; +} + +std::string TextEdit::break_characters = " ::;,.-"; + +TextEdit::ivec2 TextEdit::FindNextBreakCharacter(ivec2 search_start) +{ + search_start.x++; + if (search_start.x >= int(lines[search_start.y].text.size()) - 1) + return ivec2(lines[search_start.y].text.size(), search_start.y); + + int pos = lines[search_start.y].text.find_first_of(break_characters, search_start.x); + if (pos == std::string::npos) + return ivec2(lines[search_start.y].text.size(), search_start.y); + return ivec2(pos, search_start.y); +} + +TextEdit::ivec2 TextEdit::FindPreviousBreakCharacter(ivec2 search_start) +{ + search_start.x--; + if (search_start.x <= 0) + return ivec2(0, search_start.y); + int pos = lines[search_start.y].text.find_last_of(break_characters, search_start.x); + if (pos == std::string::npos) + return ivec2(0, search_start.y); + return ivec2(pos, search_start.y); +} + +void TextEdit::InsertText(ivec2 pos, const std::string& str) +{ + undo_info.first_erase = false; + if (undo_info.first_text_insert) + { + undo_info.undo_text = GetText(); + undo_info.first_text_insert = false; + } + + // checking if insert exceeds max length + if (ToOffset(ivec2(lines[lines.size() - 1].text.size(), lines.size() - 1)) + str.length() > max_length) + { + return; + } + + std::string::size_type start = 0; + while (true) + { + std::string::size_type next_newline = str.find('\n', start); + + lines[pos.y].text.insert(pos.x, str.substr(start, next_newline - start)); + lines[pos.y].invalidated = true; + + if (next_newline == std::string::npos) + break; + + pos.x += next_newline - start; + + Line line; + line.text = lines[pos.y].text.substr(pos.x); + lines.insert(lines.begin() + pos.y + 1, line); + lines[pos.y].text = lines[pos.y].text.substr(0, pos.x); + lines[pos.y].invalidated = true; + pos = ivec2(0, pos.y + 1); + + start = next_newline + 1; + } + + MoveVerticalScroll(); + + Update(); +} + +void TextEdit::Backspace() +{ + if (undo_info.first_erase) + { + undo_info.first_erase = false; + undo_info.undo_text = GetText(); + } + + if (GetSelectionLength() != 0) + { + DeleteSelectedText(); + ClearSelection(); + Update(); + } + else + { + if (cursor_pos.x > 0) + { + UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length()); + utf8_reader.set_position(cursor_pos.x); + utf8_reader.prev(); + int length = utf8_reader.char_length(); + lines[cursor_pos.y].text.erase(cursor_pos.x - length, length); + lines[cursor_pos.y].invalidated = true; + cursor_pos.x -= length; + Update(); + } + else if (cursor_pos.y > 0) + { + selection_start = ivec2(lines[cursor_pos.y - 1].text.length(), cursor_pos.y - 1); + selection_length = 1; + DeleteSelectedText(); + } + } + MoveVerticalScroll(); +} + +void TextEdit::Del() +{ + if (undo_info.first_erase) + { + undo_info.first_erase = false; + undo_info.undo_text = GetText(); + } + + if (GetSelectionLength() != 0) + { + DeleteSelectedText(); + ClearSelection(); + Update(); + } + else + { + if (cursor_pos.x < (int)lines[cursor_pos.y].text.size()) + { + UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length()); + utf8_reader.set_position(cursor_pos.x); + int length = utf8_reader.char_length(); + lines[cursor_pos.y].text.erase(cursor_pos.x, length); + lines[cursor_pos.y].invalidated = true; + Update(); + } + else if (cursor_pos.y + 1 < lines.size()) + { + selection_start = ivec2(lines[cursor_pos.y].text.length(), cursor_pos.y); + selection_length = 1; + DeleteSelectedText(); + } + } + MoveVerticalScroll(); +} + +void TextEdit::OnTimerExpired() +{ + if (IsVisible() == false) + { + timer->Stop(); + return; + } + + if (cursor_blink_visible) + timer->Start(500); + else + timer->Start(500); + + cursor_blink_visible = !cursor_blink_visible; + Update(); +} + +void TextEdit::OnGeometryChanged() +{ + Canvas* canvas = GetCanvas(); + + for (auto& line : lines) + { + line.invalidated = true; + } + + 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; + } + + Colorf textColor = GetStyleColor("color"); + Point draw_pos; + for (size_t i = (size_t)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, textColor); + else + line.layout.AddText(" ", font, textColor); // Draw one space character to get the correct height + line.layout.Layout(canvas, GetWidth()); + line.box = Rect(draw_pos, line.layout.GetSize()); + line.invalidated = false; + } + + if (sel_start != sel_end && sel_start.y <= i && sel_end.y >= i) + { + line.layout.SetSelectionRange(sel_start.y < i ? 0 : sel_start.x, sel_end.y > i ? line.text.size() : sel_end.x); + } + else + { + line.layout.SetSelectionRange(0, 0); + } + + line.layout.HideCursor(); + if (HasFocus()) + { + if (cursor_blink_visible && cursor_pos.y == i) + { + line.layout.SetCursorPos(cursor_pos.x); + line.layout.SetCursorColor(textColor); + 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) +{ + LayoutLines(canvas); + for (size_t i = (size_t)vert_scrollbar->GetPosition(); i < lines.size(); i++) + lines[i].layout.DrawLayout(canvas); +} + +TextEdit::ivec2 TextEdit::GetCharacterIndex(Point mouse_wincoords) +{ + Canvas* canvas = GetCanvas(); + for (size_t i = 0; i < lines.size(); i++) + { + Line& line = lines[i]; + if (line.box.top() <= mouse_wincoords.y && line.box.bottom() > mouse_wincoords.y) + { + SpanLayout::HitTestResult result = line.layout.HitTest(canvas, mouse_wincoords); + switch (result.type) + { + case SpanLayout::HitTestResult::inside: + return ivec2(clamp(result.offset, (size_t)0, line.text.size()), i); + case SpanLayout::HitTestResult::outside_left: + return ivec2(0, i); + case SpanLayout::HitTestResult::outside_right: + return ivec2(line.text.size(), i); + } + } + } + + return ivec2(lines.back().text.size(), lines.size() - 1); +} diff --git a/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp b/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp new file mode 100644 index 000000000..aa6e7e739 --- /dev/null +++ b/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp @@ -0,0 +1,60 @@ + +#include "widgets/textlabel/textlabel.h" + +TextLabel::TextLabel(Widget* parent) : Widget(parent) +{ +} + +void TextLabel::SetText(const std::string& value) +{ + if (text != value) + { + text = value; + Update(); + } +} + +const std::string& TextLabel::GetText() const +{ + return text; +} + +void TextLabel::SetTextAlignment(TextLabelAlignment alignment) +{ + if (textAlignment != alignment) + { + textAlignment = alignment; + Update(); + } +} + +TextLabelAlignment TextLabel::GetTextAlignment() const +{ + return textAlignment; +} + +double TextLabel::GetPreferredWidth() const +{ + Canvas* canvas = GetCanvas(); + return canvas->measureText(text).width; +} + +double TextLabel::GetPreferredHeight() const +{ + return 20.0; +} + +void TextLabel::OnPaint(Canvas* canvas) +{ + double x = 0.0; + if (textAlignment == TextLabelAlignment::Center) + { + x = (GetWidth() - canvas->measureText(text).width) * 0.5; + } + else if (textAlignment == TextLabelAlignment::Right) + { + x = GetWidth() - canvas->measureText(text).width; + } + + canvas->drawText(Point(x, GetHeight() - 5.0), GetStyleColor("color"), text); +} diff --git a/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp b/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp new file mode 100644 index 000000000..4629f3a1e --- /dev/null +++ b/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp @@ -0,0 +1,63 @@ + +#include "widgets/toolbar/toolbar.h" +#include "widgets/toolbar/toolbarbutton.h" + +Toolbar::Toolbar(Widget* parent) : Widget(parent) +{ + SetStyleClass("toolbar"); +} + +Toolbar::~Toolbar() +{ +} + +void Toolbar::SetDirection(ToolbarDirection newDirection) +{ + if (direction != newDirection) + { + direction = newDirection; + Update(); + } +} + +ToolbarButton* Toolbar::AddButton(std::string icon, std::string text, std::function onClicked) +{ + ToolbarButton* button = new ToolbarButton(this); + if (!icon.empty()) + button->SetIcon(icon); + if (!text.empty()) + button->SetText(text); + button->OnClick = std::move(onClicked); + buttons.push_back(button); + return button; +} + +void Toolbar::OnGeometryChanged() +{ + if (direction == ToolbarDirection::Horizontal) + { + double x = 7.0; + double barHeight = GetHeight(); + double gap = 7.0; + for (ToolbarButton* button : buttons) + { + double width = button->GetPreferredWidth(); + double height = button->GetPreferredHeight(); + button->SetFrameGeometry(Rect::xywh(x, (barHeight - height) * 0.5, width, height)); + x += width + gap; + } + } + else + { + double y = 7.0; + double barWidth = GetWidth(); + double gap = 7.0; + for (ToolbarButton* button : buttons) + { + double width = button->GetPreferredWidth(); + double height = button->GetPreferredHeight(); + button->SetFrameGeometry(Rect::xywh((barWidth - width) * 0.5, y, width, height)); + y += height + gap; + } + } +} diff --git a/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp b/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp new file mode 100644 index 000000000..5eda9a13d --- /dev/null +++ b/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp @@ -0,0 +1,114 @@ + +#include "widgets/toolbar/toolbarbutton.h" + +ToolbarButton::ToolbarButton(Widget* parent) : Widget(parent) +{ + SetStyleClass("toolbarbutton"); +} + +ToolbarButton::~ToolbarButton() +{ +} + +void ToolbarButton::SetIcon(std::string icon) +{ + if (!icon.empty()) + { + if (!image) + image = new ImageBox(this); + image->SetImage(Image::LoadResource(icon, GetDpiScale())); + image->SetImageMode(ImageBoxMode::Contain); + } + else + { + delete image; + image = nullptr; + } +} + +void ToolbarButton::SetText(std::string text) +{ + if (!text.empty()) + { + if (!label) + label = new TextLabel(this); + label->SetText(text); + } + else + { + delete label; + label = nullptr; + } +} + +void ToolbarButton::Click() +{ + if (OnClick) + OnClick(); +} + +double ToolbarButton::GetPreferredWidth() +{ + double w = 0.0; + if (image) + w = 26.0; + if (label) + w += label->GetPreferredWidth(); + return w; +} + +double ToolbarButton::GetPreferredHeight() +{ + double h = 0.0; + if (image) + h = 24.0; + if (label) + h = std::max(h, label->GetPreferredHeight()); + return h; +} + +void ToolbarButton::OnMouseMove(const Point& pos) +{ + if (GetStyleState().empty()) + { + SetStyleState("hover"); + } +} + +void ToolbarButton::OnMouseLeave() +{ + SetStyleState(""); +} + +bool ToolbarButton::OnMouseDown(const Point& pos, InputKey key) +{ + SetStyleState("down"); + return true; +} + +bool ToolbarButton::OnMouseUp(const Point& pos, InputKey key) +{ + if (GetStyleState() == "down") + { + SetStyleState(""); + Repaint(); + Click(); + } + return true; +} + +void ToolbarButton::OnGeometryChanged() +{ + double totalHeight = GetPreferredHeight(); + double x = 0.0; + if (image) + { + image->SetFrameGeometry(Rect::xywh(0.0, (totalHeight - 24.0) * 0.5, 24.0, 24.0)); + x += 26.0; + } + if (label) + { + double labelHeight = label->GetPreferredHeight(); + label->SetFrameGeometry(Rect::xywh(x, (totalHeight - labelHeight) * 0.5, GetWidth() - x, labelHeight)); + } +} diff --git a/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.cpp b/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.cpp new file mode 100644 index 000000000..1ebc0668f --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.cpp @@ -0,0 +1,288 @@ + +#include "dbus_open_file_dialog.h" +#include + +DBusOpenFileDialog::DBusOpenFileDialog(std::string ownerHandle) : ownerHandle(ownerHandle) +{ +} + +std::string DBusOpenFileDialog::Filename() const +{ + return outputFilenames.empty() ? std::string() : outputFilenames.front(); +} + +std::vector DBusOpenFileDialog::Filenames() const +{ + return outputFilenames; +} + +void DBusOpenFileDialog::SetMultiSelect(bool multiselect) +{ + this->multiSelect = multiSelect; +} + +void DBusOpenFileDialog::SetFilename(const std::string &filename) +{ + inputFilename = filename; +} + +void DBusOpenFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultExt = extension; +} + +void DBusOpenFileDialog::AddFilter(const std::string &filter_description, const std::string &filter_extension) +{ + filters.push_back({ filter_description, filter_extension }); +} + +void DBusOpenFileDialog::ClearFilters() +{ + filters.clear(); +} + +void DBusOpenFileDialog::SetFilterIndex(int filter_index) +{ + this->filter_index = filter_index; +} + +void DBusOpenFileDialog::SetInitialDirectory(const std::string &path) +{ + initialDirectory = path; +} + +void DBusOpenFileDialog::SetTitle(const std::string &title) +{ + this->title = title; +} + +bool DBusOpenFileDialog::Show() +{ + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html + + dbus_bool_t bresult = {}; + DBusError error = {}; + dbus_error_init(&error); + + DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &error); + if (!connection) + { + dbus_error_free(&error); + return false; + } + + std::string busname = dbus_bus_get_unique_name(connection); + + DBusMessage* request = dbus_message_new_method_call("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.FileChooser", "OpenFile"); + if (!request) + { + dbus_connection_unref(connection); + dbus_error_free(&error); + return false; + } + + const char* parentWindow = ownerHandle.c_str(); + const char* title = this->title.c_str(); + + DBusMessageIter requestArgs = {}; + dbus_message_iter_init_append(request, &requestArgs); + + bresult = dbus_message_iter_append_basic(&requestArgs, DBUS_TYPE_STRING, &parentWindow); + bresult = dbus_message_iter_append_basic(&requestArgs, DBUS_TYPE_STRING, &title); + + DBusMessageIter requestOptions = {}; + bresult = dbus_message_iter_open_container(&requestArgs, DBUS_TYPE_ARRAY, "{sv}", &requestOptions); + + // handle_token - s race condition prevention for signal (/org/freedesktop/portal/desktop/request/SENDER/TOKEN) + // accept_label - s text label for the OK button + // modal - b makes dialog modal. Defaults to true. Not really sure what it means since nothing stayed modal on my computer + // directory - b open folder mode, added in version 3 + // filters - a(sa(us)) [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] + // current_filter - sa(us) filter from filters that should be current filter + // choices - a(ssa(ss)s) list of serialized combo boxes to add to the file chooser + // current_name - s suggested name + + // current_folder - ay + if (!initialDirectory.empty()) + { + // Note: the docs unfortunately says "The portal implementation is free to ignore this option" + + const char* key = "current_folder"; + const char* value = initialDirectory.c_str(); + int valueCount = (int)initialDirectory.size() + 1; + + DBusMessageIter entry = {}; + bresult = dbus_message_iter_open_container(&requestOptions, DBUS_TYPE_DICT_ENTRY, nullptr, &entry); + bresult = dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key); + + DBusMessageIter variant = {}; + DBusMessageIter array = {}; + bresult = dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "ay", &variant); + bresult = dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE_AS_STRING, &array); + bresult = dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, &value, valueCount); + bresult = dbus_message_iter_close_container(&variant, &array); + bresult = dbus_message_iter_close_container(&entry, &variant); + + bresult = dbus_message_iter_close_container(&requestOptions, &entry); + } + + // multiple - b + { + const char* key = "multiple"; + dbus_bool_t value = multiSelect; + + DBusMessageIter entry = {}; + bresult = dbus_message_iter_open_container(&requestOptions, DBUS_TYPE_DICT_ENTRY, nullptr, &entry); + bresult = dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key); + + DBusMessageIter variant = {}; + bresult = dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, DBUS_TYPE_BOOLEAN_AS_STRING, &variant); + bresult = dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &value); + bresult = dbus_message_iter_close_container(&entry, &variant); + + bresult = dbus_message_iter_close_container(&requestOptions, &entry); + } + + bresult = dbus_message_iter_close_container(&requestArgs, &requestOptions); + + DBusMessage* response = dbus_connection_send_with_reply_and_block(connection, request, DBUS_TIMEOUT_USE_DEFAULT, &error); + if (!response) + { + dbus_message_unref(request); + dbus_connection_unref(connection); + dbus_error_free(&error); + return false; + } + + const char* handle = nullptr; + bresult = dbus_message_get_args(response, &error, DBUS_TYPE_OBJECT_PATH, &handle, DBUS_TYPE_INVALID); + if (!bresult) + { + dbus_message_unref(response); + dbus_message_unref(request); + dbus_connection_unref(connection); + dbus_error_free(&error); + return false; + } + + std::string signalObjectPath = handle; + + dbus_message_unref(response); + dbus_message_unref(request); + + std::string rule = "type='signal',interface='org.freedesktop.portal.Request',member='Response',path='" + signalObjectPath + "'"; + dbus_bus_add_match(connection, rule.c_str(), &error); + + // Wait for the response signal + // + // To do: process the run loop while we wait + // + DBusMessage* signalmsg = nullptr; + while (!signalmsg && dbus_connection_read_write(connection, -1)) + { + while (true) + { + DBusMessage* message = dbus_connection_pop_message(connection); + if (!message) + break; + + if (dbus_message_is_signal(message, "org.freedesktop.portal.Request", "Response") && dbus_message_get_path(message) == signalObjectPath) + { + signalmsg = message; + break; + } + else + { + dbus_message_unref(message); + } + } + } + dbus_bus_remove_match(connection, rule.c_str(), &error); + + // Read the response + + dbus_uint32_t responseCode = 0; + std::vector uris; + + DBusMessageIter signalArgs; + bresult = dbus_message_iter_init(signalmsg, &signalArgs); + + // response code - u + if (dbus_message_iter_get_arg_type(&signalArgs) == DBUS_TYPE_UINT32) + { + dbus_message_iter_get_basic(&signalArgs, &responseCode); + } + dbus_message_iter_next(&signalArgs); + + // results - a{sv} + if (dbus_message_iter_get_arg_type(&signalArgs) == DBUS_TYPE_ARRAY) + { + DBusMessageIter resultsArray = {}; + dbus_message_iter_recurse(&signalArgs, &resultsArray); + while (true) + { + int type = dbus_message_iter_get_arg_type(&resultsArray); + if (type != DBUS_TYPE_DICT_ENTRY) + break; + + DBusMessageIter entry = {}; + dbus_message_iter_recurse(&resultsArray, &entry); + + const char* key = nullptr; + dbus_message_iter_get_basic(&entry, &key); + dbus_message_iter_next(&entry); + + DBusMessageIter value = {}; + dbus_message_iter_recurse(&entry, &value); + + std::string k = key; + if (k == "uris" && dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_ARRAY) // as + { + DBusMessageIter uriArray = {}; + dbus_message_iter_recurse(&value, &uriArray); + while (true) + { + int type = dbus_message_iter_get_arg_type(&uriArray); + if (type != DBUS_TYPE_STRING) + break; + + const char* uri = nullptr; + dbus_message_iter_get_basic(&uriArray, &uri); + uris.push_back(uri); + + dbus_message_iter_next(&uriArray); + } + } + else if (k == "choices" && dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_ARRAY) // a(ss) + { + // An array of pairs of strings, + // the first string being the ID of a combobox that was passed into this call, + // the second string being the selected option. + } + else if (k == "current_filter" && dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_STRUCT) // sa(us) + { + // The filter that was selected. + // This may match a filter in the filter list or another filter that was applied unconditionally. + } + + dbus_message_iter_next(&entry); + dbus_message_iter_next(&resultsArray); + } + } + dbus_message_iter_next(&signalArgs); + + dbus_message_unref(signalmsg); + dbus_connection_unref(connection); + dbus_error_free(&error); + + if (responseCode != 0) // User cancelled + return false; + + for (const std::string& uri : uris) + { + if (uri.size() > 7 && uri.substr(0, 7) == "file://") + outputFilenames.push_back(uri.substr(7)); + } + + return !uris.empty(); +} diff --git a/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.h b/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.h new file mode 100644 index 000000000..f4efd2663 --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.h @@ -0,0 +1,38 @@ +#pragma once + +#include "systemdialogs/open_file_dialog.h" + +class DBusOpenFileDialog : public OpenFileDialog +{ +public: + DBusOpenFileDialog(std::string ownerHandle); + + std::string Filename() const override; + std::vector Filenames() const override; + void SetMultiSelect(bool multiselect) override; + void SetFilename(const std::string &filename) override; + void SetDefaultExtension(const std::string& extension) override; + void AddFilter(const std::string &filter_description, const std::string &filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string &path) override; + void SetTitle(const std::string& newtitle) override; + bool Show() override; + +private: + std::string ownerHandle; + std::string title; + std::string initialDirectory; + std::string inputFilename; + std::string defaultExt; + std::vector outputFilenames; + bool multiSelect = false; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filter_index = 0; +}; diff --git a/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.cpp b/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.cpp new file mode 100644 index 000000000..7bcd94953 --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.cpp @@ -0,0 +1,26 @@ + +#include "dbus_open_folder_dialog.h" + +DBusOpenFolderDialog::DBusOpenFolderDialog(std::string ownerHandle) : ownerHandle(ownerHandle) +{ +} + +bool DBusOpenFolderDialog::Show() +{ + return false; +} + +std::string DBusOpenFolderDialog::SelectedPath() const +{ + return selected_path; +} + +void DBusOpenFolderDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void DBusOpenFolderDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} diff --git a/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.h b/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.h new file mode 100644 index 000000000..3e40cc7bc --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.h @@ -0,0 +1,21 @@ +#pragma once + +#include "systemdialogs/open_folder_dialog.h" + +class DBusOpenFolderDialog : public OpenFolderDialog +{ +public: + DBusOpenFolderDialog(std::string ownerHandle); + + bool Show() override; + std::string SelectedPath() const override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + +private: + std::string ownerHandle; + + std::string selected_path; + std::string initial_directory; + std::string title; +}; diff --git a/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.cpp b/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.cpp new file mode 100644 index 000000000..ba6b6d1f4 --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.cpp @@ -0,0 +1,54 @@ + +#include "dbus_save_file_dialog.h" + +DBusSaveFileDialog::DBusSaveFileDialog(std::string ownerHandle) : ownerHandle(ownerHandle) +{ +} + +bool DBusSaveFileDialog::Show() +{ + return false; +} + +std::string DBusSaveFileDialog::Filename() const +{ + return filename; +} + +void DBusSaveFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void DBusSaveFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void DBusSaveFileDialog::ClearFilters() +{ + filters.clear(); +} + +void DBusSaveFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void DBusSaveFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void DBusSaveFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void DBusSaveFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} diff --git a/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.h b/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.h new file mode 100644 index 000000000..835789b2c --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.h @@ -0,0 +1,37 @@ +#pragma once + +#include "systemdialogs/save_file_dialog.h" + +class DBusSaveFileDialog : public SaveFileDialog +{ +public: + DBusSaveFileDialog(std::string ownerHandle); + + bool Show() override; + std::string Filename() const override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + std::string ownerHandle; + + std::string filename; + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.cpp b/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.cpp new file mode 100644 index 000000000..d67699eda --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.cpp @@ -0,0 +1,98 @@ + +#include "sdl2_display_backend.h" +#include "sdl2_display_window.h" +#include +#include +#ifndef WIN32 +#include +#endif + +namespace X11DPI +{ + typedef struct _XDisplay Display; + typedef Display *(*XOpenDisplayPtr)(const char*); + typedef char *(*XGetDefaultPtr)(Display*, const char*, const char*); + typedef int (*XCloseDisplayPtr)(Display*); +} + +SDL2DisplayBackend::SDL2DisplayBackend() +{ + int result = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); + if (result != 0) + throw std::runtime_error(std::string("Unable to initialize SDL:") + SDL_GetError()); + + SDL2DisplayWindow::PaintEventNumber = SDL_RegisterEvents(1); + + // SDL2 doesn't have proper native hidpi support for Linux. Cheat a bit here by asking X11 ourselves. +#if !defined(WIN32) && !defined(__APPLE__) + const char* driver = SDL_GetCurrentVideoDriver(); + if (driver && strcmp(driver, "x11") == 0) + { + void* module = dlopen("libX11.so", RTLD_NOW | RTLD_LOCAL); + if (module) + { + using namespace X11DPI; + auto XOpenDisplay = (XOpenDisplayPtr)dlsym(module, "XOpenDisplay"); + auto XGetDefault = (XGetDefaultPtr)dlsym(module, "XGetDefault"); + auto XCloseDisplay = (XCloseDisplayPtr)dlsym(module, "XCloseDisplay"); + if (XOpenDisplay && XGetDefault && XCloseDisplay) + { + auto display = XOpenDisplay(nullptr); + if (display) + { + char* value = XGetDefault(display, "Xft", "dpi"); + if (value) + { + int dpi = std::atoi(value); + if (dpi != 0) + { + UIScale = dpi / 96.0; + } + } + XCloseDisplay(display); + } + } + } + } +#endif +} + +std::unique_ptr SDL2DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return std::make_unique(windowHost, popupWindow, static_cast(owner), renderAPI, UIScale); +} + +void SDL2DisplayBackend::ProcessEvents() +{ + SDL2DisplayWindow::ProcessEvents(); +} + +void SDL2DisplayBackend::RunLoop() +{ + SDL2DisplayWindow::RunLoop(); +} + +void SDL2DisplayBackend::ExitLoop() +{ + SDL2DisplayWindow::ExitLoop(); +} + +Size SDL2DisplayBackend::GetScreenSize() +{ + SDL_Rect rect = {}; + int result = SDL_GetDisplayBounds(0, &rect); + if (result != 0) + throw std::runtime_error(std::string("Unable to get screen size:") + SDL_GetError()); + + return Size(rect.w / UIScale, rect.h / UIScale); +} + +void* SDL2DisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return SDL2DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void SDL2DisplayBackend::StopTimer(void* timerID) +{ + SDL2DisplayWindow::StopTimer(timerID); +} diff --git a/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.h b/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.h new file mode 100644 index 000000000..53b17b0eb --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.h @@ -0,0 +1,24 @@ +#pragma once + +#include "window/window.h" + +class SDL2DisplayBackend : public DisplayBackend +{ +public: + SDL2DisplayBackend(); + + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) override; + void ProcessEvents() override; + void RunLoop() override; + void ExitLoop() override; + + void* StartTimer(int timeoutMilliseconds, std::function onTimer) override; + void StopTimer(void* timerID) override; + + Size GetScreenSize() override; + + bool IsSDL2() override { return true; } + +private: + double UIScale = 1.0; +}; diff --git a/libraries/ZWidget/src/window/sdl2/sdl2_display_window.cpp b/libraries/ZWidget/src/window/sdl2/sdl2_display_window.cpp new file mode 100644 index 000000000..a71a4a1ca --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2_display_window.cpp @@ -0,0 +1,792 @@ + +#include "sdl2_display_window.h" +#include +#include + +Uint32 SDL2DisplayWindow::PaintEventNumber = 0xffffffff; +bool SDL2DisplayWindow::ExitRunLoop; +std::unordered_map SDL2DisplayWindow::WindowList; + +SDL2DisplayWindow::SDL2DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, SDL2DisplayWindow* owner, RenderAPI renderAPI, double uiscale) : WindowHost(windowHost), UIScale(uiscale) +{ + unsigned int flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE /*| SDL_WINDOW_ALLOW_HIGHDPI*/; + if (renderAPI == RenderAPI::Vulkan) + flags |= SDL_WINDOW_VULKAN; + else if (renderAPI == RenderAPI::OpenGL) + flags |= SDL_WINDOW_OPENGL; +#if defined(__APPLE__) + else if (renderAPI == RenderAPI::Metal) + flags |= SDL_WINDOW_METAL; +#endif + if (popupWindow) + flags |= SDL_WINDOW_BORDERLESS; + + if (renderAPI == RenderAPI::Vulkan || renderAPI == RenderAPI::OpenGL || renderAPI == RenderAPI::Metal) + { + Handle.window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 320, 200, flags); + if (!Handle.window) + throw std::runtime_error(std::string("Unable to create SDL window:") + SDL_GetError()); + } + else + { + int result = SDL_CreateWindowAndRenderer(320, 200, flags, &Handle.window, &RendererHandle); + if (result != 0) + throw std::runtime_error(std::string("Unable to create SDL window:") + SDL_GetError()); + } + + WindowList[SDL_GetWindowID(Handle.window)] = this; +} + +SDL2DisplayWindow::~SDL2DisplayWindow() +{ + UnlockCursor(); + + WindowList.erase(WindowList.find(SDL_GetWindowID(Handle.window))); + + if (BackBufferTexture) + { + SDL_DestroyTexture(BackBufferTexture); + BackBufferTexture = nullptr; + } + + if (RendererHandle) + SDL_DestroyRenderer(RendererHandle); + SDL_DestroyWindow(Handle.window); + RendererHandle = nullptr; + Handle.window = nullptr; +} + +std::vector SDL2DisplayWindow::GetVulkanInstanceExtensions() +{ + unsigned int extCount = 0; + SDL_Vulkan_GetInstanceExtensions(Handle.window, &extCount, nullptr); + std::vector extNames(extCount); + SDL_Vulkan_GetInstanceExtensions(Handle.window, &extCount, extNames.data()); + + std::vector result; + result.reserve(extNames.size()); + for (const char* ext : extNames) + result.emplace_back(ext); + return result; +} + +VkSurfaceKHR SDL2DisplayWindow::CreateVulkanSurface(VkInstance instance) +{ + VkSurfaceKHR surfaceHandle = {}; + SDL_Vulkan_CreateSurface(Handle.window, instance, &surfaceHandle); + if (surfaceHandle) + throw std::runtime_error("Could not create vulkan surface"); + return surfaceHandle; +} + +void SDL2DisplayWindow::SetWindowTitle(const std::string& text) +{ + SDL_SetWindowTitle(Handle.window, text.c_str()); +} + +void SDL2DisplayWindow::SetWindowFrame(const Rect& box) +{ + // SDL2 doesn't really seem to have an API for this. + // The docs aren't clear what you're setting when calling SDL_SetWindowSize. + SetClientFrame(box); +} + +void SDL2DisplayWindow::SetClientFrame(const Rect& box) +{ + // Is there a way to set both in one call? + + double uiscale = GetDpiScale(); + int x = (int)std::round(box.x * uiscale); + int y = (int)std::round(box.y * uiscale); + int w = (int)std::round(box.width * uiscale); + int h = (int)std::round(box.height * uiscale); + + SDL_SetWindowSize(Handle.window, w, h); + SDL_SetWindowPosition(Handle.window, x, y); +} + +void SDL2DisplayWindow::Show() +{ + SDL_ShowWindow(Handle.window); +} + +void SDL2DisplayWindow::ShowFullscreen() +{ + SDL_ShowWindow(Handle.window); + SDL_SetWindowFullscreen(Handle.window, SDL_WINDOW_FULLSCREEN_DESKTOP); + isFullscreen = true; +} + +void SDL2DisplayWindow::ShowMaximized() +{ + SDL_ShowWindow(Handle.window); + SDL_MaximizeWindow(Handle.window); +} + +void SDL2DisplayWindow::ShowMinimized() +{ + SDL_ShowWindow(Handle.window); + SDL_MinimizeWindow(Handle.window); +} + +void SDL2DisplayWindow::ShowNormal() +{ + SDL_ShowWindow(Handle.window); + SDL_SetWindowFullscreen(Handle.window, 0); + isFullscreen = false; +} + +bool SDL2DisplayWindow::IsWindowFullscreen() +{ + return isFullscreen; +} + +void SDL2DisplayWindow::Hide() +{ + SDL_HideWindow(Handle.window); +} + +void SDL2DisplayWindow::Activate() +{ + SDL_RaiseWindow(Handle.window); +} + +void SDL2DisplayWindow::ShowCursor(bool enable) +{ + SDL_ShowCursor(enable); +} + +void SDL2DisplayWindow::LockCursor() +{ + if (!CursorLocked) + { + SDL_SetRelativeMouseMode(SDL_TRUE); + CursorLocked = true; + } +} + +void SDL2DisplayWindow::UnlockCursor() +{ + if (CursorLocked) + { + SDL_SetRelativeMouseMode(SDL_FALSE); + CursorLocked = false; + } +} + +void SDL2DisplayWindow::CaptureMouse() +{ +} + +void SDL2DisplayWindow::ReleaseMouseCapture() +{ +} + +void SDL2DisplayWindow::SetCursor(StandardCursor cursor) +{ +} + +void SDL2DisplayWindow::Update() +{ + SDL_Event event = {}; + event.type = PaintEventNumber; + event.user.windowID = SDL_GetWindowID(Handle.window); + SDL_PushEvent(&event); +} + +bool SDL2DisplayWindow::GetKeyState(InputKey key) +{ + int numkeys = 0; + const Uint8* state = SDL_GetKeyboardState(&numkeys); + if (!state) return false; + + SDL_Scancode index = InputKeyToScancode(key); + return (index < numkeys) ? state[index] != 0 : false; +} + +Rect SDL2DisplayWindow::GetWindowFrame() const +{ + int x = 0; + int y = 0; + int w = 0; + int h = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowPosition(Handle.window, &x, &y); + SDL_GetWindowSize(Handle.window, &w, &h); + return Rect::xywh(x / uiscale, y / uiscale, w / uiscale, h / uiscale); +} + +Point SDL2DisplayWindow::MapFromGlobal(const Point& pos) const +{ + int x = 0; + int y = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowPosition(Handle.window, &x, &y); + return Point(pos.x - x / uiscale, pos.y - y / uiscale); +} + +Point SDL2DisplayWindow::MapToGlobal(const Point& pos) const +{ + int x = 0; + int y = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowPosition(Handle.window, &x, &y); + return Point(pos.x + x / uiscale, pos.y + y / uiscale); +} + +Size SDL2DisplayWindow::GetClientSize() const +{ + int w = 0; + int h = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowSize(Handle.window, &w, &h); + return Size(w / uiscale, h / uiscale); +} + +int SDL2DisplayWindow::GetPixelWidth() const +{ + int w = 0; + int h = 0; + if (RendererHandle) + SDL_GetRendererOutputSize(RendererHandle, &w, &h); + else + SDL_GL_GetDrawableSize(Handle.window, &w, &h); + return w; +} + +int SDL2DisplayWindow::GetPixelHeight() const +{ + int w = 0; + int h = 0; + if (RendererHandle) + SDL_GetRendererOutputSize(RendererHandle, &w, &h); + else + SDL_GL_GetDrawableSize(Handle.window, &w, &h); + return h; +} + +double SDL2DisplayWindow::GetDpiScale() const +{ + // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. + // We currently cheat by asking X11 directly. See the SDL2DisplayBackend constructor. + return UIScale; +} + +void SDL2DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + if (!RendererHandle) + return; + + if (!BackBufferTexture || BackBufferWidth != width || BackBufferHeight != height) + { + if (BackBufferTexture) + { + SDL_DestroyTexture(BackBufferTexture); + BackBufferTexture = nullptr; + } + + BackBufferTexture = SDL_CreateTexture(RendererHandle, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height); + if (!BackBufferTexture) + return; + + BackBufferWidth = width; + BackBufferHeight = height; + } + + int destpitch = 0; + void* dest = nullptr; + int result = SDL_LockTexture(BackBufferTexture, nullptr, &dest, &destpitch); + if (result != 0) return; + for (int y = 0; y < height; y++) + { + const void* sline = pixels + y * width; + void* dline = (uint8_t*)dest + y * destpitch; + memcpy(dline, sline, width << 2); + } + SDL_UnlockTexture(BackBufferTexture); + + SDL_RenderCopy(RendererHandle, BackBufferTexture, nullptr, nullptr); + SDL_RenderPresent(RendererHandle); +} + +void SDL2DisplayWindow::SetBorderColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +void SDL2DisplayWindow::SetCaptionColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +void SDL2DisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +std::string SDL2DisplayWindow::GetClipboardText() +{ + char* buffer = SDL_GetClipboardText(); + if (!buffer) + return {}; + std::string text = buffer; + SDL_free(buffer); + return text; +} + +void SDL2DisplayWindow::SetClipboardText(const std::string& text) +{ + SDL_SetClipboardText(text.c_str()); +} + +void SDL2DisplayWindow::ProcessEvents() +{ + SDL_Event event; + while (SDL_PollEvent(&event) != 0) + { + DispatchEvent(event); + } +} + +void SDL2DisplayWindow::RunLoop() +{ + ExitRunLoop = false; + while (!ExitRunLoop) + { + SDL_Event event = {}; + int result = SDL_WaitEvent(&event); + if (result == 1) + DispatchEvent(event); // Silently ignore if it fails and pray it doesn't busy loop, because SDL and Linux utterly sucks! + } +} + +void SDL2DisplayWindow::ExitLoop() +{ + ExitRunLoop = true; +} + +std::unordered_map> SDL2DisplayWindow::Timers; +std::unordered_map SDL2DisplayWindow::TimerHandles; +unsigned long SDL2DisplayWindow::TimerIDs = 0; +Uint32 TimerEventID = SDL_RegisterEvents(1); + +Uint32 SDL2DisplayWindow::ExecTimer(Uint32 interval, void* execID) +{ + // cancel event and stop loop if function not found + if (Timers.find(execID) == Timers.end()) + return 0; + + SDL_Event timerEvent; + SDL_zero(timerEvent); + + timerEvent.user.type = TimerEventID; + timerEvent.user.data1 = execID; + + SDL_PushEvent(&timerEvent); + + return interval; +} + +void* SDL2DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + // is this guard needed? + // CheckInitSDL(); + + void* execID = (void*)(uintptr_t)++TimerIDs; + void* id = (void*)(uintptr_t)SDL_AddTimer(timeoutMilliseconds, SDL2DisplayWindow::ExecTimer, execID); + + if (!id) return id; + + Timers.insert({execID, onTimer}); + TimerHandles.insert({id, execID}); + + return id; +} + +void SDL2DisplayWindow::StopTimer(void* timerID) +{ + // is this guard needed? + // CheckInitSDL(); + + SDL_RemoveTimer((SDL_TimerID)(uintptr_t)timerID); + + auto execID = TimerHandles.find(timerID); + if (execID == TimerHandles.end()) + return; + + Timers.erase(execID->second); + TimerHandles.erase(timerID); +} + +SDL2DisplayWindow* SDL2DisplayWindow::FindEventWindow(const SDL_Event& event) +{ + int windowID; + switch (event.type) + { + case SDL_WINDOWEVENT: windowID = event.window.windowID; break; + case SDL_TEXTINPUT: windowID = event.text.windowID; break; + case SDL_KEYUP: windowID = event.key.windowID; break; + case SDL_KEYDOWN: windowID = event.key.windowID; break; + case SDL_MOUSEBUTTONUP: windowID = event.button.windowID; break; + case SDL_MOUSEBUTTONDOWN: windowID = event.button.windowID; break; + case SDL_MOUSEWHEEL: windowID = event.wheel.windowID; break; + case SDL_MOUSEMOTION: windowID = event.motion.windowID; break; + default: + if (event.type == PaintEventNumber) windowID = event.user.windowID; + else return nullptr; + } + + auto it = WindowList.find(windowID); + return it != WindowList.end() ? it->second : nullptr; +} + +void SDL2DisplayWindow::DispatchEvent(const SDL_Event& event) +{ + // timers are created in a non-window context + if (event.type == TimerEventID) + return OnTimerEvent(event.user); + + SDL2DisplayWindow* window = FindEventWindow(event); + if (!window) return; + + switch (event.type) + { + case SDL_WINDOWEVENT: window->OnWindowEvent(event.window); break; + case SDL_TEXTINPUT: window->OnTextInput(event.text); break; + case SDL_KEYUP: window->OnKeyUp(event.key); break; + case SDL_KEYDOWN: window->OnKeyDown(event.key); break; + case SDL_MOUSEBUTTONUP: window->OnMouseButtonUp(event.button); break; + case SDL_MOUSEBUTTONDOWN: window->OnMouseButtonDown(event.button); break; + case SDL_MOUSEWHEEL: window->OnMouseWheel(event.wheel); break; + case SDL_MOUSEMOTION: window->OnMouseMotion(event.motion); break; + default: + if (event.type == PaintEventNumber) window->OnPaintEvent(); + } +} + +void SDL2DisplayWindow::OnWindowEvent(const SDL_WindowEvent& event) +{ + switch (event.event) + { + case SDL_WINDOWEVENT_CLOSE: WindowHost->OnWindowClose(); break; + case SDL_WINDOWEVENT_MOVED: WindowHost->OnWindowGeometryChanged(); break; + case SDL_WINDOWEVENT_RESIZED: WindowHost->OnWindowGeometryChanged(); break; + case SDL_WINDOWEVENT_SHOWN: WindowHost->OnWindowPaint(); break; + case SDL_WINDOWEVENT_EXPOSED: WindowHost->OnWindowPaint(); break; + case SDL_WINDOWEVENT_FOCUS_GAINED: WindowHost->OnWindowActivated(); break; + case SDL_WINDOWEVENT_FOCUS_LOST: WindowHost->OnWindowDeactivated(); break; + } +} + +void SDL2DisplayWindow::OnTextInput(const SDL_TextInputEvent& event) +{ + WindowHost->OnWindowKeyChar(event.text); +} + +void SDL2DisplayWindow::OnKeyUp(const SDL_KeyboardEvent& event) +{ + WindowHost->OnWindowKeyUp(ScancodeToInputKey(event.keysym.scancode)); +} + +void SDL2DisplayWindow::OnKeyDown(const SDL_KeyboardEvent& event) +{ + WindowHost->OnWindowKeyDown(ScancodeToInputKey(event.keysym.scancode)); +} + +InputKey SDL2DisplayWindow::GetMouseButtonKey(const SDL_MouseButtonEvent& event) +{ + switch (event.button) + { + case SDL_BUTTON_LEFT: return InputKey::LeftMouse; + case SDL_BUTTON_MIDDLE: return InputKey::MiddleMouse; + case SDL_BUTTON_RIGHT: return InputKey::RightMouse; + // case SDL_BUTTON_X1: return InputKey::XButton1; + // case SDL_BUTTON_X2: return InputKey::XButton2; + default: return InputKey::None; + } +} + +void SDL2DisplayWindow::OnMouseButtonUp(const SDL_MouseButtonEvent& event) +{ + InputKey key = GetMouseButtonKey(event); + if (key != InputKey::None) + { + WindowHost->OnWindowMouseUp(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseButtonDown(const SDL_MouseButtonEvent& event) +{ + InputKey key = GetMouseButtonKey(event); + if (key != InputKey::None) + { + WindowHost->OnWindowMouseDown(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseWheel(const SDL_MouseWheelEvent& event) +{ + int x = 0, y = 0; + SDL_GetMouseState(&x, &y); + double uiscale = GetDpiScale(); + Point mousepos(x / uiscale, y / uiscale); + + if (event.y > 0) + WindowHost->OnWindowMouseWheel(mousepos, InputKey::MouseWheelUp); + else if (event.y < 0) + WindowHost->OnWindowMouseWheel(mousepos, InputKey::MouseWheelDown); +} + +void SDL2DisplayWindow::OnMouseMotion(const SDL_MouseMotionEvent& event) +{ + if (CursorLocked) + { + WindowHost->OnWindowRawMouseMove(event.xrel, event.yrel); + } + else + { + WindowHost->OnWindowMouseMove(GetMousePos(event)); + } +} + +void SDL2DisplayWindow::OnPaintEvent() +{ + WindowHost->OnWindowPaint(); +} + +void SDL2DisplayWindow::OnTimerEvent(const SDL_UserEvent& event) +{ + auto func = Timers.find(event.data1); + + // incase timer was cancelled before we get here + if (func == Timers.end()) + return; + + func->second(); +} + +InputKey SDL2DisplayWindow::ScancodeToInputKey(SDL_Scancode keycode) +{ + switch (keycode) + { + case SDL_SCANCODE_BACKSPACE: return InputKey::Backspace; + case SDL_SCANCODE_TAB: return InputKey::Tab; + case SDL_SCANCODE_CLEAR: return InputKey::OEMClear; + case SDL_SCANCODE_RETURN: return InputKey::Enter; + case SDL_SCANCODE_MENU: return InputKey::Alt; + case SDL_SCANCODE_PAUSE: return InputKey::Pause; + case SDL_SCANCODE_ESCAPE: return InputKey::Escape; + case SDL_SCANCODE_SPACE: return InputKey::Space; + case SDL_SCANCODE_END: return InputKey::End; + case SDL_SCANCODE_PAGEDOWN: return InputKey::PageDown; + case SDL_SCANCODE_HOME: return InputKey::Home; + case SDL_SCANCODE_PAGEUP: return InputKey::PageUp; + case SDL_SCANCODE_LEFT: return InputKey::Left; + case SDL_SCANCODE_UP: return InputKey::Up; + case SDL_SCANCODE_RIGHT: return InputKey::Right; + case SDL_SCANCODE_DOWN: return InputKey::Down; + case SDL_SCANCODE_SELECT: return InputKey::Select; + case SDL_SCANCODE_PRINTSCREEN: return InputKey::Print; + case SDL_SCANCODE_EXECUTE: return InputKey::Execute; + case SDL_SCANCODE_INSERT: return InputKey::Insert; + case SDL_SCANCODE_DELETE: return InputKey::Delete; + case SDL_SCANCODE_HELP: return InputKey::Help; + case SDL_SCANCODE_0: return InputKey::_0; + case SDL_SCANCODE_1: return InputKey::_1; + case SDL_SCANCODE_2: return InputKey::_2; + case SDL_SCANCODE_3: return InputKey::_3; + case SDL_SCANCODE_4: return InputKey::_4; + case SDL_SCANCODE_5: return InputKey::_5; + case SDL_SCANCODE_6: return InputKey::_6; + case SDL_SCANCODE_7: return InputKey::_7; + case SDL_SCANCODE_8: return InputKey::_8; + case SDL_SCANCODE_9: return InputKey::_9; + case SDL_SCANCODE_A: return InputKey::A; + case SDL_SCANCODE_B: return InputKey::B; + case SDL_SCANCODE_C: return InputKey::C; + case SDL_SCANCODE_D: return InputKey::D; + case SDL_SCANCODE_E: return InputKey::E; + case SDL_SCANCODE_F: return InputKey::F; + case SDL_SCANCODE_G: return InputKey::G; + case SDL_SCANCODE_H: return InputKey::H; + case SDL_SCANCODE_I: return InputKey::I; + case SDL_SCANCODE_J: return InputKey::J; + case SDL_SCANCODE_K: return InputKey::K; + case SDL_SCANCODE_L: return InputKey::L; + case SDL_SCANCODE_M: return InputKey::M; + case SDL_SCANCODE_N: return InputKey::N; + case SDL_SCANCODE_O: return InputKey::O; + case SDL_SCANCODE_P: return InputKey::P; + case SDL_SCANCODE_Q: return InputKey::Q; + case SDL_SCANCODE_R: return InputKey::R; + case SDL_SCANCODE_S: return InputKey::S; + case SDL_SCANCODE_T: return InputKey::T; + case SDL_SCANCODE_U: return InputKey::U; + case SDL_SCANCODE_V: return InputKey::V; + case SDL_SCANCODE_W: return InputKey::W; + case SDL_SCANCODE_X: return InputKey::X; + case SDL_SCANCODE_Y: return InputKey::Y; + case SDL_SCANCODE_Z: return InputKey::Z; + case SDL_SCANCODE_KP_0: return InputKey::NumPad0; + case SDL_SCANCODE_KP_1: return InputKey::NumPad1; + case SDL_SCANCODE_KP_2: return InputKey::NumPad2; + case SDL_SCANCODE_KP_3: return InputKey::NumPad3; + case SDL_SCANCODE_KP_4: return InputKey::NumPad4; + case SDL_SCANCODE_KP_5: return InputKey::NumPad5; + case SDL_SCANCODE_KP_6: return InputKey::NumPad6; + case SDL_SCANCODE_KP_7: return InputKey::NumPad7; + case SDL_SCANCODE_KP_8: return InputKey::NumPad8; + case SDL_SCANCODE_KP_9: return InputKey::NumPad9; + // case SDL_SCANCODE_KP_ENTER: return InputKey::NumPadEnter; + // case SDL_SCANCODE_KP_MULTIPLY: return InputKey::Multiply; + // case SDL_SCANCODE_KP_PLUS: return InputKey::Add; + case SDL_SCANCODE_SEPARATOR: return InputKey::Separator; + // case SDL_SCANCODE_KP_MINUS: return InputKey::Subtract; + case SDL_SCANCODE_KP_PERIOD: return InputKey::NumPadPeriod; + // case SDL_SCANCODE_KP_DIVIDE: return InputKey::Divide; + case SDL_SCANCODE_F1: return InputKey::F1; + case SDL_SCANCODE_F2: return InputKey::F2; + case SDL_SCANCODE_F3: return InputKey::F3; + case SDL_SCANCODE_F4: return InputKey::F4; + case SDL_SCANCODE_F5: return InputKey::F5; + case SDL_SCANCODE_F6: return InputKey::F6; + case SDL_SCANCODE_F7: return InputKey::F7; + case SDL_SCANCODE_F8: return InputKey::F8; + case SDL_SCANCODE_F9: return InputKey::F9; + case SDL_SCANCODE_F10: return InputKey::F10; + case SDL_SCANCODE_F11: return InputKey::F11; + case SDL_SCANCODE_F12: return InputKey::F12; + case SDL_SCANCODE_F13: return InputKey::F13; + case SDL_SCANCODE_F14: return InputKey::F14; + case SDL_SCANCODE_F15: return InputKey::F15; + case SDL_SCANCODE_F16: return InputKey::F16; + case SDL_SCANCODE_F17: return InputKey::F17; + case SDL_SCANCODE_F18: return InputKey::F18; + case SDL_SCANCODE_F19: return InputKey::F19; + case SDL_SCANCODE_F20: return InputKey::F20; + case SDL_SCANCODE_F21: return InputKey::F21; + case SDL_SCANCODE_F22: return InputKey::F22; + case SDL_SCANCODE_F23: return InputKey::F23; + case SDL_SCANCODE_F24: return InputKey::F24; + case SDL_SCANCODE_NUMLOCKCLEAR: return InputKey::NumLock; + case SDL_SCANCODE_SCROLLLOCK: return InputKey::ScrollLock; + case SDL_SCANCODE_LSHIFT: return InputKey::LShift; + case SDL_SCANCODE_RSHIFT: return InputKey::RShift; + case SDL_SCANCODE_LCTRL: return InputKey::LControl; + case SDL_SCANCODE_RCTRL: return InputKey::RControl; + case SDL_SCANCODE_GRAVE: return InputKey::Tilde; + default: return InputKey::None; + } +} + +SDL_Scancode SDL2DisplayWindow::InputKeyToScancode(InputKey inputkey) +{ + switch (inputkey) + { + case InputKey::Backspace: return SDL_SCANCODE_BACKSPACE; + case InputKey::Tab: return SDL_SCANCODE_TAB; + case InputKey::OEMClear: return SDL_SCANCODE_CLEAR; + case InputKey::Enter: return SDL_SCANCODE_RETURN; + case InputKey::Alt: return SDL_SCANCODE_MENU; + case InputKey::Pause: return SDL_SCANCODE_PAUSE; + case InputKey::Escape: return SDL_SCANCODE_ESCAPE; + case InputKey::Space: return SDL_SCANCODE_SPACE; + case InputKey::End: return SDL_SCANCODE_END; + case InputKey::Home: return SDL_SCANCODE_HOME; + case InputKey::Left: return SDL_SCANCODE_LEFT; + case InputKey::Up: return SDL_SCANCODE_UP; + case InputKey::Right: return SDL_SCANCODE_RIGHT; + case InputKey::Down: return SDL_SCANCODE_DOWN; + case InputKey::Select: return SDL_SCANCODE_SELECT; + case InputKey::Print: return SDL_SCANCODE_PRINTSCREEN; + case InputKey::Execute: return SDL_SCANCODE_EXECUTE; + case InputKey::Insert: return SDL_SCANCODE_INSERT; + case InputKey::Delete: return SDL_SCANCODE_DELETE; + case InputKey::Help: return SDL_SCANCODE_HELP; + case InputKey::_0: return SDL_SCANCODE_0; + case InputKey::_1: return SDL_SCANCODE_1; + case InputKey::_2: return SDL_SCANCODE_2; + case InputKey::_3: return SDL_SCANCODE_3; + case InputKey::_4: return SDL_SCANCODE_4; + case InputKey::_5: return SDL_SCANCODE_5; + case InputKey::_6: return SDL_SCANCODE_6; + case InputKey::_7: return SDL_SCANCODE_7; + case InputKey::_8: return SDL_SCANCODE_8; + case InputKey::_9: return SDL_SCANCODE_9; + case InputKey::A: return SDL_SCANCODE_A; + case InputKey::B: return SDL_SCANCODE_B; + case InputKey::C: return SDL_SCANCODE_C; + case InputKey::D: return SDL_SCANCODE_D; + case InputKey::E: return SDL_SCANCODE_E; + case InputKey::F: return SDL_SCANCODE_F; + case InputKey::G: return SDL_SCANCODE_G; + case InputKey::H: return SDL_SCANCODE_H; + case InputKey::I: return SDL_SCANCODE_I; + case InputKey::J: return SDL_SCANCODE_J; + case InputKey::K: return SDL_SCANCODE_K; + case InputKey::L: return SDL_SCANCODE_L; + case InputKey::M: return SDL_SCANCODE_M; + case InputKey::N: return SDL_SCANCODE_N; + case InputKey::O: return SDL_SCANCODE_O; + case InputKey::P: return SDL_SCANCODE_P; + case InputKey::Q: return SDL_SCANCODE_Q; + case InputKey::R: return SDL_SCANCODE_R; + case InputKey::S: return SDL_SCANCODE_S; + case InputKey::T: return SDL_SCANCODE_T; + case InputKey::U: return SDL_SCANCODE_U; + case InputKey::V: return SDL_SCANCODE_V; + case InputKey::W: return SDL_SCANCODE_W; + case InputKey::X: return SDL_SCANCODE_X; + case InputKey::Y: return SDL_SCANCODE_Y; + case InputKey::Z: return SDL_SCANCODE_Z; + case InputKey::NumPad0: return SDL_SCANCODE_KP_0; + case InputKey::NumPad1: return SDL_SCANCODE_KP_1; + case InputKey::NumPad2: return SDL_SCANCODE_KP_2; + case InputKey::NumPad3: return SDL_SCANCODE_KP_3; + case InputKey::NumPad4: return SDL_SCANCODE_KP_4; + case InputKey::NumPad5: return SDL_SCANCODE_KP_5; + case InputKey::NumPad6: return SDL_SCANCODE_KP_6; + case InputKey::NumPad7: return SDL_SCANCODE_KP_7; + case InputKey::NumPad8: return SDL_SCANCODE_KP_8; + case InputKey::NumPad9: return SDL_SCANCODE_KP_9; + // case InputKey::NumPadEnter: return SDL_SCANCODE_KP_ENTER; + // case InputKey::Multiply return SDL_SCANCODE_KP_MULTIPLY:; + // case InputKey::Add: return SDL_SCANCODE_KP_PLUS; + case InputKey::Separator: return SDL_SCANCODE_SEPARATOR; + // case InputKey::Subtract: return SDL_SCANCODE_KP_MINUS; + case InputKey::NumPadPeriod: return SDL_SCANCODE_KP_PERIOD; + // case InputKey::Divide: return SDL_SCANCODE_KP_DIVIDE; + case InputKey::F1: return SDL_SCANCODE_F1; + case InputKey::F2: return SDL_SCANCODE_F2; + case InputKey::F3: return SDL_SCANCODE_F3; + case InputKey::F4: return SDL_SCANCODE_F4; + case InputKey::F5: return SDL_SCANCODE_F5; + case InputKey::F6: return SDL_SCANCODE_F6; + case InputKey::F7: return SDL_SCANCODE_F7; + case InputKey::F8: return SDL_SCANCODE_F8; + case InputKey::F9: return SDL_SCANCODE_F9; + case InputKey::F10: return SDL_SCANCODE_F10; + case InputKey::F11: return SDL_SCANCODE_F11; + case InputKey::F12: return SDL_SCANCODE_F12; + case InputKey::F13: return SDL_SCANCODE_F13; + case InputKey::F14: return SDL_SCANCODE_F14; + case InputKey::F15: return SDL_SCANCODE_F15; + case InputKey::F16: return SDL_SCANCODE_F16; + case InputKey::F17: return SDL_SCANCODE_F17; + case InputKey::F18: return SDL_SCANCODE_F18; + case InputKey::F19: return SDL_SCANCODE_F19; + case InputKey::F20: return SDL_SCANCODE_F20; + case InputKey::F21: return SDL_SCANCODE_F21; + case InputKey::F22: return SDL_SCANCODE_F22; + case InputKey::F23: return SDL_SCANCODE_F23; + case InputKey::F24: return SDL_SCANCODE_F24; + case InputKey::NumLock: return SDL_SCANCODE_NUMLOCKCLEAR; + case InputKey::ScrollLock: return SDL_SCANCODE_SCROLLLOCK; + case InputKey::LShift: return SDL_SCANCODE_LSHIFT; + case InputKey::RShift: return SDL_SCANCODE_RSHIFT; + case InputKey::LControl: return SDL_SCANCODE_LCTRL; + case InputKey::RControl: return SDL_SCANCODE_RCTRL; + case InputKey::Tilde: return SDL_SCANCODE_GRAVE; + default: return (SDL_Scancode)0; + } +} diff --git a/libraries/ZWidget/src/window/sdl2/sdl2_display_window.h b/libraries/ZWidget/src/window/sdl2/sdl2_display_window.h new file mode 100644 index 000000000..5a510878e --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2_display_window.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include +#include + +class SDL2DisplayWindow : public DisplayWindow +{ +public: + SDL2DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, SDL2DisplayWindow* owner, RenderAPI renderAPI, double uiscale); + ~SDL2DisplayWindow(); + + void SetWindowTitle(const std::string& text) override; + void SetWindowFrame(const Rect& box) override; + void SetClientFrame(const Rect& box) override; + void Show() override; + void ShowFullscreen() override; + void ShowMaximized() override; + void ShowMinimized() override; + void ShowNormal() override; + bool IsWindowFullscreen() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(InputKey key) override; + void SetCursor(StandardCursor cursor) override; + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + Point MapFromGlobal(const Point& pos) const override; + Point MapToGlobal(const Point& pos) const override; + + void* GetNativeHandle() override { return &Handle; } + + std::vector GetVulkanInstanceExtensions() override; + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) override; + + static void DispatchEvent(const SDL_Event& event); + static SDL2DisplayWindow* FindEventWindow(const SDL_Event& event); + + void OnWindowEvent(const SDL_WindowEvent& event); + void OnTextInput(const SDL_TextInputEvent& event); + void OnKeyUp(const SDL_KeyboardEvent& event); + void OnKeyDown(const SDL_KeyboardEvent& event); + void OnMouseButtonUp(const SDL_MouseButtonEvent& event); + void OnMouseButtonDown(const SDL_MouseButtonEvent& event); + void OnMouseWheel(const SDL_MouseWheelEvent& event); + void OnMouseMotion(const SDL_MouseMotionEvent& event); + void OnPaintEvent(); + static void OnTimerEvent(const SDL_UserEvent& event); + + InputKey GetMouseButtonKey(const SDL_MouseButtonEvent& event); + + static InputKey ScancodeToInputKey(SDL_Scancode keycode); + static SDL_Scancode InputKeyToScancode(InputKey inputkey); + + template + Point GetMousePos(const T& event) + { + double uiscale = GetDpiScale(); + return Point(event.x / uiscale, event.y / uiscale); + } + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + static Uint32 ExecTimer(Uint32 interval, void* id); + + DisplayWindowHost* WindowHost = nullptr; + SDL2NativeHandle Handle; + SDL_Renderer* RendererHandle = nullptr; + SDL_Texture* BackBufferTexture = nullptr; + int BackBufferWidth = 0; + int BackBufferHeight = 0; + + double UIScale = 1.0; + + bool CursorLocked = false; + bool isFullscreen = false; + + static bool ExitRunLoop; + static Uint32 PaintEventNumber; + static std::unordered_map WindowList; + + static std::unordered_map TimerHandles; + static std::unordered_map> Timers; + static unsigned long TimerIDs; + static Uint32 TimerEventNumber; +}; diff --git a/libraries/ZWidget/src/window/stub/stub_open_file_dialog.cpp b/libraries/ZWidget/src/window/stub/stub_open_file_dialog.cpp new file mode 100644 index 000000000..b3c61fe0b --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_open_file_dialog.cpp @@ -0,0 +1,64 @@ + +#include "stub_open_file_dialog.h" + +StubOpenFileDialog::StubOpenFileDialog(DisplayWindow* owner) : owner(owner) +{ +} + +bool StubOpenFileDialog::Show() +{ + return false; +} + +std::string StubOpenFileDialog::Filename() const +{ + return !filenames.empty() ? filenames.front() : std::string(); +} + +std::vector StubOpenFileDialog::Filenames() const +{ + return filenames; +} + +void StubOpenFileDialog::SetMultiSelect(bool new_multi_select) +{ + multi_select = new_multi_select; +} + +void StubOpenFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void StubOpenFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void StubOpenFileDialog::ClearFilters() +{ + filters.clear(); +} + +void StubOpenFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void StubOpenFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void StubOpenFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void StubOpenFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} diff --git a/libraries/ZWidget/src/window/stub/stub_open_file_dialog.h b/libraries/ZWidget/src/window/stub/stub_open_file_dialog.h new file mode 100644 index 000000000..9ffcead2e --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_open_file_dialog.h @@ -0,0 +1,41 @@ +#pragma once + +#include "systemdialogs/open_file_dialog.h" + +class DisplayWindow; + +class StubOpenFileDialog : public OpenFileDialog +{ +public: + StubOpenFileDialog(DisplayWindow* owner); + + bool Show() override; + std::string Filename() const override; + std::vector Filenames() const override; + void SetMultiSelect(bool new_multi_select) override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + DisplayWindow* owner = nullptr; + + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + bool multi_select = false; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.cpp b/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.cpp new file mode 100644 index 000000000..83ca24469 --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.cpp @@ -0,0 +1,26 @@ + +#include "stub_open_folder_dialog.h" + +StubOpenFolderDialog::StubOpenFolderDialog(DisplayWindow* owner) : owner(owner) +{ +} + +bool StubOpenFolderDialog::Show() +{ + return false; +} + +std::string StubOpenFolderDialog::SelectedPath() const +{ + return selected_path; +} + +void StubOpenFolderDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void StubOpenFolderDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} diff --git a/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.h b/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.h new file mode 100644 index 000000000..4d18708be --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.h @@ -0,0 +1,23 @@ +#pragma once + +#include "systemdialogs/open_folder_dialog.h" + +class DisplayWindow; + +class StubOpenFolderDialog : public OpenFolderDialog +{ +public: + StubOpenFolderDialog(DisplayWindow* owner); + + bool Show() override; + std::string SelectedPath() const override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + +private: + DisplayWindow* owner = nullptr; + + std::string selected_path; + std::string initial_directory; + std::string title; +}; diff --git a/libraries/ZWidget/src/window/stub/stub_save_file_dialog.cpp b/libraries/ZWidget/src/window/stub/stub_save_file_dialog.cpp new file mode 100644 index 000000000..64e96a2ff --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_save_file_dialog.cpp @@ -0,0 +1,54 @@ + +#include "stub_save_file_dialog.h" + +StubSaveFileDialog::StubSaveFileDialog(DisplayWindow* owner) : owner(owner) +{ +} + +bool StubSaveFileDialog::Show() +{ + return false; +} + +std::string StubSaveFileDialog::Filename() const +{ + return filename; +} + +void StubSaveFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void StubSaveFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void StubSaveFileDialog::ClearFilters() +{ + filters.clear(); +} + +void StubSaveFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void StubSaveFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void StubSaveFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void StubSaveFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} diff --git a/libraries/ZWidget/src/window/stub/stub_save_file_dialog.h b/libraries/ZWidget/src/window/stub/stub_save_file_dialog.h new file mode 100644 index 000000000..c57ce2e5e --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_save_file_dialog.h @@ -0,0 +1,39 @@ +#pragma once + +#include "systemdialogs/save_file_dialog.h" + +class DisplayWindow; + +class StubSaveFileDialog : public SaveFileDialog +{ +public: + StubSaveFileDialog(DisplayWindow* owner); + + bool Show() override; + std::string Filename() const override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + DisplayWindow* owner = nullptr; + + std::string filename; + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/wayland/wayland_display_backend.cpp b/libraries/ZWidget/src/window/wayland/wayland_display_backend.cpp new file mode 100644 index 000000000..ffb394853 --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wayland_display_backend.cpp @@ -0,0 +1,1150 @@ +#include "wayland_display_backend.h" +#include "wayland_display_window.h" + +#ifdef USE_DBUS +#include "window/dbus/dbus_open_file_dialog.h" +#include "window/dbus/dbus_save_file_dialog.h" +#include "window/dbus/dbus_open_folder_dialog.h" +#endif + +WaylandDisplayBackend::WaylandDisplayBackend() +{ + if (!s_waylandDisplay) + throw std::runtime_error("Wayland Display initialization failed!"); + + s_waylandRegistry = s_waylandDisplay.get_registry(); + + s_waylandRegistry.on_global() = [&](uint32_t name, std::string interface, uint32_t version) { + if (interface == wayland::compositor_t::interface_name) + s_waylandRegistry.bind(name, m_waylandCompositor, 3); + if (interface == wayland::shm_t::interface_name) + s_waylandRegistry.bind(name, m_waylandSHM, version); + if (interface == wayland::output_t::interface_name) + s_waylandRegistry.bind(name, m_waylandOutput, version); + if (interface == wayland::seat_t::interface_name) + s_waylandRegistry.bind(name, m_waylandSeat, 8); + if (interface == wayland::data_device_manager_t::interface_name) + s_waylandRegistry.bind(name, m_DataDeviceManager, 3); + if (interface == wayland::xdg_wm_base_t::interface_name) + s_waylandRegistry.bind(name, m_XDGWMBase, 4); + if (interface == wayland::zxdg_output_manager_v1_t::interface_name) + s_waylandRegistry.bind(name, m_XDGOutputManager, version); + if (interface == wayland::zxdg_exporter_v2_t::interface_name) + s_waylandRegistry.bind(name, m_XDGExporter, 1); + if (interface == wayland::zwp_pointer_constraints_v1_t::interface_name) + s_waylandRegistry.bind(name, m_PointerConstraints, 1); + if (interface == wayland::xdg_activation_v1_t::interface_name) + s_waylandRegistry.bind(name, m_XDGActivation, 1); + if (interface == wayland::zxdg_decoration_manager_v1_t::interface_name) + s_waylandRegistry.bind(name, m_XDGDecorationManager, 1); + if (interface == wayland::fractional_scale_manager_v1_t::interface_name) + s_waylandRegistry.bind(name, m_FractionalScaleManager, 1); + if (interface == wayland::zwp_relative_pointer_manager_v1_t::interface_name) + s_waylandRegistry.bind(name, m_RelativePointerManager, 1); + }; + + s_waylandDisplay.roundtrip(); + + if (!m_XDGWMBase) + throw std::runtime_error("WaylandDisplayBackend: XDG-Shell is required!"); + + if (!m_XDGOutputManager) + throw std::runtime_error("WaylandDisplayBackend: xdg-output-manager-v1 is required!"); + + if (!m_XDGExporter) + throw std::runtime_error("WaylandDisplayBackend: xdg-foreign-unstable-v2 is required!"); + + if (!m_PointerConstraints) + throw std::runtime_error("WaylandDisplayBackend: pointer-constrains-unstable-v1 is required!"); + + if (!m_RelativePointerManager) + throw std::runtime_error("WaylandDisplayBackend: relative-pointer-unstable-v1 is required!"); + + m_waylandOutput.on_mode() = [this] (wayland::output_mode flags, int32_t width, int32_t height, int32_t refresh) { + s_ScreenSize = Size(width, height); + }; + + m_XDGWMBase.on_ping() = [this] (uint32_t serial) { + m_XDGWMBase.pong(serial); + }; + + m_waylandSeat.on_capabilities() = [this] (uint32_t capabilities) { + hasKeyboard = capabilities & wayland::seat_capability::keyboard; + hasPointer = capabilities & wayland::seat_capability::pointer; + }; + + m_XDGOutput = m_XDGOutputManager.get_xdg_output(m_waylandOutput); + + m_XDGOutput.on_logical_position() = [this] (int32_t x, int32_t y) { + //m_WindowGlobalPos = Point(x, y); + }; + + m_XDGOutput.on_logical_size() = [this] (int32_t width, int32_t height) { + s_ScreenSize = Size(width, height); + }; + + if (!m_FractionalScaleManager) + { + m_waylandOutput.on_scale() = [this] (int32_t scale) { + for (WaylandDisplayWindow* w : s_Windows) + { + w->m_ScaleFactor = scale; + w->m_NeedsUpdate = true; + w->windowHost->OnWindowDpiScaleChanged(); + } + }; + } + + s_waylandDisplay.roundtrip(); + + // To do: this shouldn't really be fatal. The user might have forgotten to plug in their keyboard or mouse. + if (!hasKeyboard) + throw std::runtime_error("No keyboard detected!"); + if (!hasPointer) + throw std::runtime_error("No pointer device detected!"); + + m_waylandKeyboard = m_waylandSeat.get_keyboard(); + m_waylandPointer = m_waylandSeat.get_pointer(); + m_RelativePointer = m_RelativePointerManager.get_relative_pointer(m_waylandPointer); + + ConnectDeviceEvents(); + + m_cursorSurface = m_waylandCompositor.create_surface(); + SetCursor(StandardCursor::arrow); + +/* + m_DataDevice = m_DataDeviceManager.get_data_device(m_waylandSeat); + + m_DataSource = m_DataDeviceManager.create_data_source(); + m_DataSource.offer("text/plain"); + + m_DataSource.on_send() = [&] (std::string mime_type, int fd) { + if (mime_type != "text/plain") + return; + + if (!m_ClipboardContents.empty()) + write(fd, m_ClipboardContents.data(), m_ClipboardContents.size()); + close(fd); + }; + + m_DataDevice.on_selection() = [&] (wayland::data_offer_t dataOffer) { + m_ClipboardContents.clear(); + + if (!dataOffer) + // Clipboard is empty + return; + + int fds[2]; + pipe(fds); + + dataOffer.receive("text/plain", fds[1]); + close(fds[1]); + + m_waylandDisplay.roundtrip(); + + while (true) + { + char buf[1024]; + + ssize_t n = read(fds[0], buf, sizeof(buf)); + + if (n <= 0) + break; + + m_ClipboardContents += buf; + } + + close(fds[0]); + + dataOffer.proxy_release(); + }; +*/ +} + +WaylandDisplayBackend::~WaylandDisplayBackend() +{ + if (m_KeymapContext) + xkb_context_unref(m_KeymapContext); + + if (m_KeyboardState) + xkb_state_unref(m_KeyboardState); +} + +void WaylandDisplayBackend::ConnectDeviceEvents() +{ + m_KeymapContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + + m_waylandKeyboard.on_keymap() = [this] (wayland::keyboard_keymap_format format, int fd, uint32_t size) { + if (format != wayland::keyboard_keymap_format::xkb_v1) + throw std::runtime_error("WaylandDisplayBackend: Unrecognized keymap format!"); + + char* mapSHM = (char*)mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (mapSHM == MAP_FAILED) + throw std::runtime_error("WaylandDisplayBackend: Keymap shared memory allocation failed!"); + + if (m_Keymap) + xkb_keymap_unref(m_Keymap); + + m_Keymap = xkb_keymap_new_from_string(m_KeymapContext, mapSHM, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap(mapSHM, size); + close(fd); + + if (m_KeyboardState) + xkb_state_unref(m_KeyboardState); + + m_KeyboardState = xkb_state_new(m_Keymap); + }; + + m_waylandKeyboard.on_modifiers() = [this] (uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { + xkb_state_update_mask(m_KeyboardState, mods_depressed, mods_latched, mods_locked, 0, 0, group); + }; + + m_waylandKeyboard.on_enter() = [this] (uint32_t serial, wayland::surface_t surfaceEntered, wayland::array_t keys) { + std::vector keysVec = keys; + + m_KeyboardSerial = serial; + + // Find the window to focus on by checking the surface window owns. + if (!m_FocusWindow || m_FocusWindow->GetWindowSurface() != surfaceEntered) + { + for (auto win: s_Windows) + { + if (win->GetWindowSurface() == surfaceEntered) + m_FocusWindow = win; + } + } + + + for (auto key: keysVec) + { + // keys parameter represents the keys pressed when entering the surface + // key variable is Linux evdev scancode, to translate it to XKB keycode, we must add 8 + xkb_keysym_t sym = xkb_state_key_get_one_sym(m_KeyboardState, key + 8); + OnKeyboardKeyEvent(sym, wayland::keyboard_key_state::pressed); + + // Also cause a Char event + char buf[128]; + xkb_state_key_get_utf8(m_KeyboardState, key + 8, buf, sizeof(buf)); + + OnKeyboardCharEvent(buf, wayland::keyboard_key_state::pressed); + } + }; + + m_waylandKeyboard.on_key() = [this] (uint32_t serial, uint32_t time, uint32_t key, wayland::keyboard_key_state state) { + // key is Linux evdev scancode, to translate it to XKB keycode, we must add 8 + xkb_keysym_t sym = xkb_state_key_get_one_sym(m_KeyboardState, key + 8); + OnKeyboardKeyEvent(sym, state); + + // Also cause a Char event + char buf[128]; + xkb_state_key_get_utf8(m_KeyboardState, key + 8, buf, sizeof(buf)); + + OnKeyboardCharEvent(buf, state); + + //m_DataDevice.set_selection(m_DataSource, m_KeyboardSerial); + }; + + m_waylandPointer.on_enter() = [this](uint32_t serial, wayland::surface_t surfaceEntered, double surfaceX, double surfaceY) { + // Find the window to focus on by checking the surface window owns. + if (!m_MouseFocusWindow || m_MouseFocusWindow->GetWindowSurface() != surfaceEntered) + { + for (auto win: s_Windows) + { + if (win->GetWindowSurface() == surfaceEntered) + { + m_MouseFocusWindow = win; + } + } + } + + currentPointerEvent.event_mask |= POINTER_EVENT_ENTER; + currentPointerEvent.serial = serial; + currentPointerEvent.surfaceX = surfaceX; + currentPointerEvent.surfaceY = surfaceY; + }; + + m_waylandPointer.on_leave() = [this](uint32_t serial, wayland::surface_t surfaceLeft) { + currentPointerEvent.event_mask |= POINTER_EVENT_LEAVE; + currentPointerEvent.serial = serial; + }; + + m_waylandPointer.on_motion() = [this] (uint32_t serial, double surfaceX, double surfaceY) { + currentPointerEvent.event_mask |= POINTER_EVENT_MOTION; + currentPointerEvent.serial = serial; + currentPointerEvent.surfaceX = surfaceX; + currentPointerEvent.surfaceY = surfaceY; + }; + + m_waylandPointer.on_button() = [this] (uint32_t serial, uint32_t time, uint32_t button, wayland::pointer_button_state state) { + currentPointerEvent.event_mask |= POINTER_EVENT_BUTTON; + currentPointerEvent.serial = serial; + currentPointerEvent.time = time; + currentPointerEvent.button = button; + currentPointerEvent.state = state; + }; + + m_waylandPointer.on_axis() = [this] (uint32_t serial, wayland::pointer_axis axis, double value) { + currentPointerEvent.event_mask |= POINTER_EVENT_AXIS; + currentPointerEvent.serial = serial; + currentPointerEvent.axes[uint32_t(axis)].value = value; + currentPointerEvent.axes[uint32_t(axis)].valid = true; + }; + + // High resolution scroll event + m_waylandPointer.on_axis_value120() = [this] (wayland::pointer_axis axis, int32_t value) { + currentPointerEvent.event_mask |= POINTER_EVENT_AXIS_120; + currentPointerEvent.axes[uint32_t(axis)].valid = true; + currentPointerEvent.axes[uint32_t(axis)].value120 = value; + }; + + m_waylandPointer.on_axis_source() = [this] (wayland::pointer_axis_source axis) { + currentPointerEvent.event_mask |= POINTER_EVENT_AXIS_SOURCE; + currentPointerEvent.axis_source = axis; + }; + + m_waylandPointer.on_axis_stop() = [this] (uint32_t time, wayland::pointer_axis axis) { + currentPointerEvent.event_mask |= POINTER_EVENT_AXIS_STOP; + currentPointerEvent.time = time; + currentPointerEvent.axes[uint32_t(axis)].valid = true; + }; + + m_RelativePointer.on_relative_motion() = [this] (uint32_t utime_hi, uint32_t utime_lo, + double dx, double dy, + double dx_unaccel, double dy_unaccel) { + currentPointerEvent.event_mask |= POINTER_EVENT_RELATIVE_MOTION; + currentPointerEvent.time = utime_lo; + currentPointerEvent.dx = dx; + currentPointerEvent.dy = dy; + }; + + m_waylandPointer.on_frame() = [this] () { + if (currentPointerEvent.event_mask & POINTER_EVENT_ENTER) + OnMouseEnterEvent(currentPointerEvent.serial); + + if (currentPointerEvent.event_mask & POINTER_EVENT_LEAVE) + OnMouseLeaveEvent(); + + if (currentPointerEvent.event_mask & POINTER_EVENT_MOTION) + { + OnMouseMoveEvent(Point(currentPointerEvent.surfaceX, currentPointerEvent.surfaceY)); + } + + if (currentPointerEvent.event_mask & POINTER_EVENT_RELATIVE_MOTION) + { + if (hasMouseLock) + OnMouseMoveRawEvent(int(currentPointerEvent.dx), int(currentPointerEvent.dy)); + } + + if (currentPointerEvent.event_mask & POINTER_EVENT_BUTTON) + { + InputKey ikey = LinuxInputEventCodeToInputKey(currentPointerEvent.button); + if (currentPointerEvent.state == wayland::pointer_button_state::pressed) + OnMousePressEvent(ikey); + else // released + OnMouseReleaseEvent(ikey); + } + + uint32_t axisevents = POINTER_EVENT_AXIS | POINTER_EVENT_AXIS_120 | POINTER_EVENT_AXIS_SOURCE | POINTER_EVENT_AXIS_STOP; + + if (currentPointerEvent.event_mask & axisevents) + { + for (size_t idx = 0 ; idx < 2 ; idx++) + { + if (!currentPointerEvent.axes[idx].valid) + continue; + + if (currentPointerEvent.event_mask & POINTER_EVENT_AXIS_120) + { + if (idx == uint32_t(wayland::pointer_axis::vertical_scroll) && currentPointerEvent.axes[idx].value > 0) + OnMouseWheelEvent(InputKey::MouseWheelDown); + if (idx == uint32_t(wayland::pointer_axis::vertical_scroll) && currentPointerEvent.axes[idx].value < 0) + OnMouseWheelEvent(InputKey::MouseWheelUp); + } + else if (currentPointerEvent.event_mask & POINTER_EVENT_AXIS) + { + if (idx == uint32_t(wayland::pointer_axis::vertical_scroll) && currentPointerEvent.axes[idx].value120 > 0) + OnMouseWheelEvent(InputKey::MouseWheelDown); + if (idx == uint32_t(wayland::pointer_axis::vertical_scroll) && currentPointerEvent.axes[idx].value120 < 0) + OnMouseWheelEvent(InputKey::MouseWheelUp); + } + } + } + + // Reset everything once all the events are processed + currentPointerEvent = {0}; + }; + + m_keyboardDelayTimer = ZTimer(); + m_keyboardRepeatTimer = ZTimer(); + + m_waylandKeyboard.on_repeat_info() = [this] (int32_t rate, int32_t delay) { + // rate is characters per second, delay is in milliseconds + m_keyboardDelayTimer.SetDuration(ZTimer::Duration(delay)); + m_keyboardRepeatTimer.SetDuration(ZTimer::Duration(1000.0 / rate)); + }; + + m_keyboardDelayTimer.SetCallback([this] () { OnKeyboardDelayEnd(); }); + m_keyboardRepeatTimer.SetCallback([this] () { OnKeyboardRepeat(); }); + + m_keyboardRepeatTimer.SetRepeating(true); + + m_previousTime = ZTimer::Clock::now(); + m_currentTime = ZTimer::Clock::now(); +} + +void WaylandDisplayBackend::OnKeyboardKeyEvent(xkb_keysym_t xkbKeySym, wayland::keyboard_key_state state) +{ + InputKey inputKey = XKBKeySymToInputKey(xkbKeySym); + + if (state == wayland::keyboard_key_state::pressed) + { + inputKeyStates[inputKey] = true; + m_FocusWindow->windowHost->OnWindowKeyDown(inputKey); + if (inputKey != previousKey) + { + previousKey = inputKey; + m_keyboardDelayTimer.Stop(); + m_keyboardRepeatTimer.Stop(); + } + m_keyboardDelayTimer.Start(); + } + if (state == wayland::keyboard_key_state::released) + { + inputKeyStates[inputKey] = false; + if (m_FocusWindow) + m_FocusWindow->windowHost->OnWindowKeyUp(inputKey); + m_keyboardDelayTimer.Stop(); + m_keyboardRepeatTimer.Stop(); + } + +} + +void WaylandDisplayBackend::OnKeyboardCharEvent(const char* ch, wayland::keyboard_key_state state) +{ + if (state == wayland::keyboard_key_state::pressed) + { + previousChars = std::string(ch); + m_FocusWindow->windowHost->OnWindowKeyChar(previousChars); + } +} + +void WaylandDisplayBackend::OnKeyboardDelayEnd() +{ + if (inputKeyStates[previousKey]) + m_keyboardRepeatTimer.Start(); +} + +void WaylandDisplayBackend::OnKeyboardRepeat() +{ + if (inputKeyStates[previousKey]) + { + m_FocusWindow->windowHost->OnWindowKeyDown(previousKey); + m_FocusWindow->windowHost->OnWindowKeyChar(previousChars); + } +} + +void WaylandDisplayBackend::OnMouseEnterEvent(uint32_t serial) +{ + m_cursorSurface.attach(!hasMouseLock ? m_cursorBuffer : nullptr, 0, 0); + if (!hasMouseLock) + m_cursorSurface.damage(0, 0, m_cursorImage.width(), m_cursorImage.height()); + m_cursorSurface.commit(); + m_waylandPointer.set_cursor(serial, m_cursorSurface, 0, 0); +} + +void WaylandDisplayBackend::OnMouseLeaveEvent() +{ + if (m_MouseFocusWindow) + { + m_MouseFocusWindow->windowHost->OnWindowMouseLeave(); + //m_MouseFocusWindow = nullptr; // Borks up the menus + } +} + +void WaylandDisplayBackend::OnMousePressEvent(InputKey button) +{ + if (m_MouseFocusWindow) + m_MouseFocusWindow->windowHost->OnWindowMouseDown(m_MouseFocusWindow->m_SurfaceMousePos, button); +} + +void WaylandDisplayBackend::OnMouseReleaseEvent(InputKey button) +{ + if (m_MouseFocusWindow) + m_MouseFocusWindow->windowHost->OnWindowMouseUp(m_MouseFocusWindow->m_SurfaceMousePos, button); +} + +void WaylandDisplayBackend::OnMouseMoveEvent(Point surfacePos) +{ + if (m_MouseFocusWindow) + { + m_MouseFocusWindow->m_SurfaceMousePos = surfacePos / m_MouseFocusWindow->m_ScaleFactor; + m_MouseFocusWindow->windowHost->OnWindowMouseMove(m_MouseFocusWindow->m_SurfaceMousePos); + } +} + +void WaylandDisplayBackend::OnMouseMoveRawEvent(int dx, int dy) +{ + if (m_MouseFocusWindow) + m_MouseFocusWindow->windowHost->OnWindowRawMouseMove(dx, dy); +} + +void WaylandDisplayBackend::OnMouseWheelEvent(InputKey button) +{ + if (m_MouseFocusWindow) + m_MouseFocusWindow->windowHost->OnWindowMouseWheel(m_MouseFocusWindow->m_SurfaceMousePos, button); +} + +void WaylandDisplayBackend::SetCursor(StandardCursor cursor) +{ + std::string cursorName = GetWaylandCursorName(cursor); + + // Perhaps the cursor size can be inferred from the user prefs as well? + wayland::cursor_theme_t cursorTheme = wayland::cursor_theme_t("default", 16, m_waylandSHM); + wayland::cursor_t obtainedCursor = cursorTheme.get_cursor(cursorName); + m_cursorImage = obtainedCursor.image(0); + m_cursorBuffer = m_cursorImage.get_buffer(); +} + +void WaylandDisplayBackend::ShowCursor(bool enable) +{ + m_cursorSurface.attach(enable ? m_cursorBuffer : nullptr, 0, 0); + m_cursorSurface.commit(); +} + +std::unique_ptr WaylandDisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return std::make_unique(this, windowHost, popupWindow, static_cast(owner), renderAPI); +} + +void WaylandDisplayBackend::OnWindowCreated(WaylandDisplayWindow* window) +{ + s_Windows.push_back(window); + if (!window->m_PopupWindow) + { + m_FocusWindow = window; + m_MouseFocusWindow = window; + } + +} + +void WaylandDisplayBackend::OnWindowDestroyed(WaylandDisplayWindow* window) +{ + auto it = std::find(s_Windows.begin(), s_Windows.end(), window); + if (it != s_Windows.end()) + s_Windows.erase(it); + + if (m_FocusWindow == window) + m_FocusWindow = nullptr; + + if (m_MouseFocusWindow == window) + m_MouseFocusWindow = nullptr; +} + +void WaylandDisplayBackend::ProcessEvents() +{ + while (wl_display_prepare_read(s_waylandDisplay)) + s_waylandDisplay.dispatch_pending(); + + while (wl_display_flush(s_waylandDisplay) < 0 && errno == EAGAIN) + poll_single(s_waylandDisplay.get_fd(), POLLOUT, -1); + + if (poll_single(s_waylandDisplay.get_fd(), POLLIN, 0) & POLLIN) + { + wl_display_read_events(s_waylandDisplay); + s_waylandDisplay.dispatch_pending(); + } + else + wl_display_cancel_read(s_waylandDisplay); + + if (s_waylandDisplay.get_error()) + throw std::runtime_error("Wayland Protocol Error"); +} + +void WaylandDisplayBackend::RunLoop() +{ + exitRunLoop = false; + + while (!exitRunLoop) + { + CheckNeedsUpdate(); + UpdateTimers(); + + ProcessEvents(); + } +} + +void WaylandDisplayBackend::UpdateTimers() +{ + m_currentTime = ZTimer::Clock::now(); + + m_keyboardDelayTimer.Update(m_currentTime - m_previousTime); + m_keyboardRepeatTimer.Update(m_currentTime - m_previousTime); + + m_previousTime = m_currentTime; +} + +void WaylandDisplayBackend::CheckNeedsUpdate() +{ + for (auto window: s_Windows) + { + if (window->m_NeedsUpdate) + { + window->m_NeedsUpdate = false; + window->windowHost->OnWindowPaint(); + } + } +} + +void WaylandDisplayBackend::ExitLoop() +{ + exitRunLoop = true; +} + +Size WaylandDisplayBackend::GetScreenSize() +{ + return s_ScreenSize; +} + +void* WaylandDisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return nullptr; +} + +void WaylandDisplayBackend::StopTimer(void* timerID) +{ +} + +#ifdef USE_DBUS +std::unique_ptr WaylandDisplayBackend::CreateOpenFileDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "wayland:" + static_cast(owner)->GetWaylandWindowID(); + return std::make_unique(ownerHandle); +} + +std::unique_ptr WaylandDisplayBackend::CreateSaveFileDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "wayland:" + static_cast(owner)->GetWaylandWindowID(); + return std::make_unique(ownerHandle); +} + +std::unique_ptr WaylandDisplayBackend::CreateOpenFolderDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "wayland:" + static_cast(owner)->GetWaylandWindowID(); + return std::make_unique(ownerHandle); +} +#endif + +bool WaylandDisplayBackend::GetKeyState(InputKey key) +{ + auto it = inputKeyStates.find(key); + + // if the key isn't "registered", then it is not pressed. + if (it == inputKeyStates.end()) + return false; + + return it->second; +} + +InputKey WaylandDisplayBackend::XKBKeySymToInputKey(xkb_keysym_t keySym) +{ + switch (keySym) + { + case XKB_KEY_Escape: + return InputKey::Escape; + case XKB_KEY_1: + return InputKey::_1; + case XKB_KEY_2: + return InputKey::_2; + case XKB_KEY_3: + return InputKey::_3; + case XKB_KEY_4: + return InputKey::_4; + case XKB_KEY_5: + return InputKey::_5; + case XKB_KEY_6: + return InputKey::_6; + case XKB_KEY_7: + return InputKey::_7; + case XKB_KEY_8: + return InputKey::_8; + case XKB_KEY_9: + return InputKey::_9; + case XKB_KEY_0: + return InputKey::_0; + case XKB_KEY_KP_1: + return InputKey::NumPad1; + case XKB_KEY_KP_2: + return InputKey::NumPad2; + case XKB_KEY_KP_3: + return InputKey::NumPad3; + case XKB_KEY_KP_4: + return InputKey::NumPad4; + case XKB_KEY_KP_5: + return InputKey::NumPad5; + case XKB_KEY_KP_6: + return InputKey::NumPad6; + case XKB_KEY_KP_7: + return InputKey::NumPad7; + case XKB_KEY_KP_8: + return InputKey::NumPad8; + case XKB_KEY_KP_9: + return InputKey::NumPad9; + case XKB_KEY_KP_0: + return InputKey::NumPad0; + case XKB_KEY_F1: + return InputKey::F1; + case XKB_KEY_F2: + return InputKey::F2; + case XKB_KEY_F3: + return InputKey::F3; + case XKB_KEY_F4: + return InputKey::F4; + case XKB_KEY_F5: + return InputKey::F5; + case XKB_KEY_F6: + return InputKey::F6; + case XKB_KEY_F7: + return InputKey::F7; + case XKB_KEY_F8: + return InputKey::F8; + case XKB_KEY_F9: + return InputKey::F9; + case XKB_KEY_F10: + return InputKey::F10; + case XKB_KEY_F11: + return InputKey::F11; + case XKB_KEY_F12: + return InputKey::F12; + case XKB_KEY_F13: + return InputKey::F13; + case XKB_KEY_F14: + return InputKey::F14; + case XKB_KEY_F15: + return InputKey::F15; + case XKB_KEY_F16: + return InputKey::F16; + case XKB_KEY_F17: + return InputKey::F17; + case XKB_KEY_F18: + return InputKey::F18; + case XKB_KEY_F19: + return InputKey::F19; + case XKB_KEY_F20: + return InputKey::F20; + case XKB_KEY_F21: + return InputKey::F21; + case XKB_KEY_F22: + return InputKey::F22; + case XKB_KEY_F23: + return InputKey::F23; + case XKB_KEY_F24: + return InputKey::F24; + case XKB_KEY_minus: + case XKB_KEY_KP_Subtract: + return InputKey::Minus; + case XKB_KEY_equal: + return InputKey::Equals; + case XKB_KEY_BackSpace: + return InputKey::Backspace; + case XKB_KEY_backslash: + return InputKey::Backslash; + case XKB_KEY_Tab: + return InputKey::Tab; + case XKB_KEY_braceleft: + return InputKey::LeftBracket; + case XKB_KEY_braceright: + return InputKey::RightBracket; + case XKB_KEY_Control_L: + case XKB_KEY_Control_R: + return InputKey::Ctrl; + case XKB_KEY_Alt_L: + case XKB_KEY_Alt_R: + return InputKey::Alt; + case XKB_KEY_Delete: + return InputKey::Delete; + case XKB_KEY_semicolon: + return InputKey::Semicolon; + case XKB_KEY_comma: + return InputKey::Comma; + case XKB_KEY_period: + return InputKey::Period; + case XKB_KEY_Num_Lock: + return InputKey::NumLock; + case XKB_KEY_Caps_Lock: + return InputKey::CapsLock; + case XKB_KEY_Scroll_Lock: + return InputKey::ScrollLock; + case XKB_KEY_Shift_L: + return InputKey::LShift; + case XKB_KEY_Shift_R: + return InputKey::RShift; + case XKB_KEY_grave: + return InputKey::Tilde; + case XKB_KEY_apostrophe: + return InputKey::SingleQuote; + case XKB_KEY_KP_Enter: + case XKB_KEY_Return: + return InputKey::Enter; + case XKB_KEY_space: + return InputKey::Space; + + case XKB_KEY_Up: + return InputKey::Up; + case XKB_KEY_Down: + return InputKey::Down; + case XKB_KEY_Left: + return InputKey::Left; + case XKB_KEY_Right: + return InputKey::Right; + + case XKB_KEY_A: + case XKB_KEY_a: + return InputKey::A; + case XKB_KEY_B: + case XKB_KEY_b: + return InputKey::B; + case XKB_KEY_C: + case XKB_KEY_c: + return InputKey::C; + case XKB_KEY_D: + case XKB_KEY_d: + return InputKey::D; + case XKB_KEY_E: + case XKB_KEY_e: + return InputKey::E; + case XKB_KEY_F: + case XKB_KEY_f: + return InputKey::F; + case XKB_KEY_G: + case XKB_KEY_g: + return InputKey::G; + case XKB_KEY_H: + case XKB_KEY_h: + return InputKey::H; + case XKB_KEY_I: + case XKB_KEY_i: + return InputKey::I; + case XKB_KEY_J: + case XKB_KEY_j: + return InputKey::J; + case XKB_KEY_K: + case XKB_KEY_k: + return InputKey::K; + case XKB_KEY_L: + case XKB_KEY_l: + return InputKey::L; + case XKB_KEY_M: + case XKB_KEY_m: + return InputKey::M; + case XKB_KEY_N: + case XKB_KEY_n: + return InputKey::N; + case XKB_KEY_O: + case XKB_KEY_o: + return InputKey::O; + case XKB_KEY_P: + case XKB_KEY_p: + return InputKey::P; + case XKB_KEY_Q: + case XKB_KEY_q: + return InputKey::Q; + case XKB_KEY_R: + case XKB_KEY_r: + return InputKey::R; + case XKB_KEY_S: + case XKB_KEY_s: + return InputKey::S; + case XKB_KEY_T: + case XKB_KEY_t: + return InputKey::T; + case XKB_KEY_U: + case XKB_KEY_u: + return InputKey::U; + case XKB_KEY_V: + case XKB_KEY_v: + return InputKey::V; + case XKB_KEY_W: + case XKB_KEY_w: + return InputKey::W; + case XKB_KEY_X: + case XKB_KEY_x: + return InputKey::X; + case XKB_KEY_Y: + case XKB_KEY_y: + return InputKey::Y; + case XKB_KEY_Z: + case XKB_KEY_z: + return InputKey::Z; + + case XKB_KEY_NoSymbol: + case XKB_KEY_VoidSymbol: + return InputKey::None; + default: + return InputKey::None; + } +} + +InputKey WaylandDisplayBackend::LinuxInputEventCodeToInputKey(uint32_t inputCode) +{ + switch (inputCode) + { + // Keyboard + // Probably not needed due to the existence of XKBKeySym + case KEY_ESC: + return InputKey::Escape; + case KEY_1: + return InputKey::_1; + case KEY_2: + return InputKey::_2; + case KEY_3: + return InputKey::_3; + case KEY_4: + return InputKey::_4; + case KEY_5: + return InputKey::_5; + case KEY_6: + return InputKey::_6; + case KEY_7: + return InputKey::_7; + case KEY_8: + return InputKey::_8; + case KEY_9: + return InputKey::_9; + case KEY_0: + return InputKey::_0; + case KEY_KP1: + return InputKey::NumPad1; + case KEY_KP2: + return InputKey::NumPad2; + case KEY_KP3: + return InputKey::NumPad3; + case KEY_KP4: + return InputKey::NumPad4; + case KEY_KP5: + return InputKey::NumPad5; + case KEY_KP6: + return InputKey::NumPad6; + case KEY_KP7: + return InputKey::NumPad7; + case KEY_KP8: + return InputKey::NumPad8; + case KEY_KP9: + return InputKey::NumPad9; + case KEY_KP0: + return InputKey::NumPad0; + case KEY_F1: + return InputKey::F1; + case KEY_F2: + return InputKey::F2; + case KEY_F3: + return InputKey::F3; + case KEY_F4: + return InputKey::F4; + case KEY_F5: + return InputKey::F5; + case KEY_F6: + return InputKey::F6; + case KEY_F7: + return InputKey::F7; + case KEY_F8: + return InputKey::F8; + case KEY_F9: + return InputKey::F9; + case KEY_F10: + return InputKey::F10; + case KEY_F11: + return InputKey::F11; + case KEY_F12: + return InputKey::F12; + case KEY_F13: + return InputKey::F13; + case KEY_F14: + return InputKey::F14; + case KEY_F15: + return InputKey::F15; + case KEY_F16: + return InputKey::F16; + case KEY_F17: + return InputKey::F17; + case KEY_F18: + return InputKey::F18; + case KEY_F19: + return InputKey::F19; + case KEY_F20: + return InputKey::F20; + case KEY_F21: + return InputKey::F21; + case KEY_F22: + return InputKey::F22; + case KEY_F23: + return InputKey::F23; + case KEY_F24: + return InputKey::F24; + case KEY_MINUS: + case KEY_KPMINUS: + return InputKey::Minus; + case KEY_EQUAL: + return InputKey::Equals; + case KEY_BACKSPACE: + return InputKey::Backspace; + case KEY_BACKSLASH: + return InputKey::Backslash; + case KEY_TAB: + return InputKey::Tab; + case KEY_LEFTBRACE: + return InputKey::LeftBracket; + case KEY_RIGHTBRACE: + return InputKey::RightBracket; + case KEY_LEFTCTRL: + return InputKey::LControl; + case KEY_RIGHTCTRL: + return InputKey::RControl; + case KEY_LEFTALT: + case KEY_RIGHTALT: + return InputKey::Alt; + case KEY_DELETE: + return InputKey::Delete; + case KEY_SEMICOLON: + return InputKey::Semicolon; + case KEY_COMMA: + return InputKey::Comma; + case KEY_DOT: + return InputKey::Period; + case KEY_NUMLOCK: + return InputKey::NumLock; + case KEY_CAPSLOCK: + return InputKey::CapsLock; + case KEY_SCROLLDOWN: + return InputKey::ScrollLock; + case KEY_LEFTSHIFT: + return InputKey::LShift; + case KEY_RIGHTSHIFT: + return InputKey::RShift; + case KEY_GRAVE: + return InputKey::Tilde; + case KEY_APOSTROPHE: + return InputKey::SingleQuote; + case KEY_SPACE: + return InputKey::Space; + case KEY_ENTER: + case KEY_KPENTER: + return InputKey::Enter; + + case KEY_UP: + return InputKey::Up; + case KEY_DOWN: + return InputKey::Down; + case KEY_LEFT: + return InputKey::Left; + case KEY_RIGHT: + return InputKey::Right; + + case KEY_A: + return InputKey::A; + case KEY_B: + return InputKey::B; + case KEY_C: + return InputKey::C; + case KEY_D: + return InputKey::D; + case KEY_E: + return InputKey::E; + case KEY_F: + return InputKey::F; + case KEY_G: + return InputKey::G; + case KEY_H: + return InputKey::H; + case KEY_I: + return InputKey::I; + case KEY_J: + return InputKey::J; + case KEY_K: + return InputKey::K; + case KEY_L: + return InputKey::L; + case KEY_M: + return InputKey::M; + case KEY_N: + return InputKey::N; + case KEY_O: + return InputKey::O; + case KEY_P: + return InputKey::P; + case KEY_Q: + return InputKey::Q; + case KEY_R: + return InputKey::R; + case KEY_S: + return InputKey::S; + case KEY_T: + return InputKey::T; + case KEY_U: + return InputKey::U; + case KEY_V: + return InputKey::V; + case KEY_W: + return InputKey::W; + case KEY_X: + return InputKey::X; + case KEY_Y: + return InputKey::Y; + case KEY_Z: + return InputKey::Z; + + // Mouse + case BTN_LEFT: + return InputKey::LeftMouse; + case BTN_RIGHT: + return InputKey::RightMouse; + case BTN_MIDDLE: + return InputKey::MiddleMouse; + default: + return InputKey::None; + } +} + +std::string WaylandDisplayBackend::GetWaylandCursorName(StandardCursor cursor) +{ + // Checked out Adwaita and Breeze cursors for the names. + // Other cursor themes should adhere to the names these two have. + switch (cursor) + { + case StandardCursor::arrow: + return "default"; + case StandardCursor::appstarting: + return "progress"; + case StandardCursor::cross: + return "crosshair"; + case StandardCursor::hand: + return "pointer"; + case StandardCursor::ibeam: + return "text"; + case StandardCursor::no: + return "not-allowed"; + case StandardCursor::size_all: + return "fleur"; + case StandardCursor::size_nesw: + return "nesw-resize"; + case StandardCursor::size_ns: + return "ns-resize"; + case StandardCursor::size_nwse: + return "nwse-resize"; + case StandardCursor::size_we: + return "ew-resize"; + case StandardCursor::uparrow: + // Breeze actually has an up-arrow cursor, but Adwaita doesn't, so the default cursor it is + return "default"; + case StandardCursor::wait: + return "wait"; + default: + return "default"; + } +} + diff --git a/libraries/ZWidget/src/window/wayland/wayland_display_backend.h b/libraries/ZWidget/src/window/wayland/wayland_display_backend.h new file mode 100644 index 000000000..26e3e7f64 --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wayland_display_backend.h @@ -0,0 +1,168 @@ +#pragma once + +#include "window/window.h" +#include "window/ztimer/ztimer.h" + +#include +#include +#include +#include +#include +#include "wl_fractional_scaling_protocol.hpp" +#include +#include +#include +#include + +static short poll_single(int fd, short events, int timeout) { + pollfd pfd { .fd = fd, .events = events, .revents = 0 }; + if (0 > poll(&pfd, 1, timeout)) { + throw std::runtime_error("poll() failed"); + } + + return pfd.revents; +} + +enum pointer_event_mask { + POINTER_EVENT_ENTER = 1 << 0, + POINTER_EVENT_LEAVE = 1 << 1, + POINTER_EVENT_MOTION = 1 << 2, + POINTER_EVENT_BUTTON = 1 << 3, + POINTER_EVENT_AXIS = 1 << 4, + POINTER_EVENT_AXIS_SOURCE = 1 << 5, + POINTER_EVENT_AXIS_STOP = 1 << 6, + POINTER_EVENT_AXIS_DISCRETE = 1 << 7, + POINTER_EVENT_AXIS_120 = 1 << 8, + POINTER_EVENT_RELATIVE_MOTION = 1 << 9, +}; + +struct WaylandPointerEvent +{ + uint32_t event_mask; + double surfaceX, surfaceY; + double dx, dy; + uint32_t button; + wayland::pointer_button_state state; + uint32_t time; + uint32_t serial; + struct { + bool valid; + double value; + int32_t discrete; + int32_t value120; + } axes[2]; + wayland::pointer_axis_source axis_source; +}; + +class WaylandDisplayWindow; + +class WaylandDisplayBackend : public DisplayBackend +{ +public: + WaylandDisplayBackend(); + ~WaylandDisplayBackend(); + + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) override; + void ProcessEvents() override; + void RunLoop() override; + void ExitLoop() override; + + void* StartTimer(int timeoutMilliseconds, std::function onTimer) override; + void StopTimer(void* timerID) override; + + Size GetScreenSize() override; + + bool IsWayland() override { return true; } + + void OnWindowCreated(WaylandDisplayWindow* window); + void OnWindowDestroyed(WaylandDisplayWindow* window); + + void SetCursor(StandardCursor cursor); + void ShowCursor(bool enable); + bool GetKeyState(InputKey key); + +#ifdef USE_DBUS + std::unique_ptr CreateOpenFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateSaveFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateOpenFolderDialog(DisplayWindow* owner) override; +#endif + + bool exitRunLoop = false; + Size s_ScreenSize = Size(0, 0); + wayland::display_t s_waylandDisplay = wayland::display_t(); + wayland::registry_t s_waylandRegistry; + std::vector s_Windows; + WaylandDisplayWindow* m_FocusWindow = nullptr; + WaylandDisplayWindow* m_MouseFocusWindow = nullptr; // Mouse focus should be tracked separately. + + wayland::compositor_t m_waylandCompositor; + wayland::shm_t m_waylandSHM; + wayland::seat_t m_waylandSeat; + wayland::output_t m_waylandOutput; + wayland::data_device_manager_t m_DataDeviceManager; + wayland::xdg_wm_base_t m_XDGWMBase; + wayland::zwp_pointer_constraints_v1_t m_PointerConstraints; + wayland::xdg_activation_v1_t m_XDGActivation; + wayland::zxdg_decoration_manager_v1_t m_XDGDecorationManager; + wayland::fractional_scale_manager_v1_t m_FractionalScaleManager; + wayland::zxdg_output_manager_v1_t m_XDGOutputManager; + wayland::zxdg_output_v1_t m_XDGOutput; + wayland::zxdg_exporter_v2_t m_XDGExporter; + + wayland::keyboard_t m_waylandKeyboard; + wayland::pointer_t m_waylandPointer; + + wayland::zwp_relative_pointer_manager_v1_t m_RelativePointerManager; + wayland::zwp_relative_pointer_v1_t m_RelativePointer; + + wayland::cursor_image_t m_cursorImage; + wayland::surface_t m_cursorSurface; + wayland::buffer_t m_cursorBuffer; + + std::map inputKeyStates; // True when the key is pressed, false when isn't + + bool IsMouseLocked() { return hasMouseLock; } + void SetMouseLocked(bool val) { hasMouseLock = val; } + +private: + void CheckNeedsUpdate(); + void UpdateTimers(); + void ConnectDeviceEvents(); + void OnKeyboardKeyEvent(xkb_keysym_t xkbKeySym, wayland::keyboard_key_state state); + void OnKeyboardCharEvent(const char* ch, wayland::keyboard_key_state state); + void OnKeyboardDelayEnd(); + void OnKeyboardRepeat(); + void OnMouseEnterEvent(uint32_t serial); + void OnMouseLeaveEvent(); + void OnMousePressEvent(InputKey button); + void OnMouseReleaseEvent(InputKey button); + void OnMouseMoveEvent(Point surfacePos); + void OnMouseMoveRawEvent(int surfaceX, int surfaceY); + void OnMouseWheelEvent(InputKey button); + + InputKey XKBKeySymToInputKey(xkb_keysym_t keySym); + InputKey LinuxInputEventCodeToInputKey(uint32_t inputCode); + + std::string GetWaylandCursorName(StandardCursor cursor); + + bool hasKeyboard = false; + bool hasPointer = false; + bool hasMouseLock = false; + + ZTimer::TimePoint m_previousTime; + ZTimer::TimePoint m_currentTime; + + ZTimer m_keyboardDelayTimer; + ZTimer m_keyboardRepeatTimer; + + InputKey previousKey = {}; + std::string previousChars; + + uint32_t m_KeyboardSerial = 0; + + xkb_context* m_KeymapContext = nullptr; + xkb_keymap* m_Keymap = nullptr; + xkb_state* m_KeyboardState = nullptr; + + WaylandPointerEvent currentPointerEvent = {0}; +}; diff --git a/libraries/ZWidget/src/window/wayland/wayland_display_window.cpp b/libraries/ZWidget/src/window/wayland/wayland_display_window.cpp new file mode 100644 index 000000000..f41b3317e --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wayland_display_window.cpp @@ -0,0 +1,472 @@ +#include "wayland_display_window.h" + +#include +#include + +WaylandDisplayWindow::WaylandDisplayWindow(WaylandDisplayBackend* backend, DisplayWindowHost* windowHost, bool popupWindow, WaylandDisplayWindow* owner, RenderAPI renderAPI) + : backend(backend), m_owner(owner), windowHost(windowHost), m_PopupWindow(popupWindow), m_renderAPI(renderAPI) +{ + m_AppSurface = backend->m_waylandCompositor.create_surface(); + + m_NativeHandle.display = backend->s_waylandDisplay; + m_NativeHandle.surface = m_AppSurface; + + if (backend->m_FractionalScaleManager) + { + m_FractionalScale = backend->m_FractionalScaleManager.get_fractional_scale(m_AppSurface); + + m_FractionalScale.on_preferred_scale() = [&](uint32_t scale_numerator) { + // parameter is the numerator of a fraction with the denominator of 120 + m_ScaleFactor = scale_numerator / 120.0; + + m_NeedsUpdate = true; + windowHost->OnWindowDpiScaleChanged(); + }; + } + + m_XDGSurface = backend->m_XDGWMBase.get_xdg_surface(m_AppSurface); + m_XDGSurface.on_configure() = [&] (uint32_t serial) { + m_XDGSurface.ack_configure(serial); + }; + + if (popupWindow) + { + InitializePopup(); + } + else + { + InitializeToplevel(); + } + + backend->OnWindowCreated(this); + + this->DrawSurface(); +} + +WaylandDisplayWindow::~WaylandDisplayWindow() +{ + backend->OnWindowDestroyed(this); +} + +void WaylandDisplayWindow::InitializeToplevel() +{ + m_XDGToplevel = m_XDGSurface.get_toplevel(); + m_XDGToplevel.set_title("ZWidget Window"); + + if (m_owner) + m_XDGToplevel.set_parent(m_owner->m_XDGToplevel); + + if (backend->m_XDGDecorationManager) + { + // Force server side decorations if possible + m_XDGToplevelDecoration = backend->m_XDGDecorationManager.get_toplevel_decoration(m_XDGToplevel); + m_XDGToplevelDecoration.set_mode(wayland::zxdg_toplevel_decoration_v1_mode::server_side); + } + + m_AppSurface.commit(); + + backend->s_waylandDisplay.roundtrip(); + + // These have to be added after the roundtrip + m_XDGToplevel.on_configure() = [&] (int32_t width, int32_t height, wayland::array_t states) { + OnXDGToplevelConfigureEvent(width, height); + }; + + m_XDGToplevel.on_close() = [&] () { + OnExitEvent(); + }; + + m_XDGToplevel.on_configure_bounds() = [this] (int32_t width, int32_t height) + { + + }; + + m_XDGExported = backend->m_XDGExporter.export_toplevel(m_AppSurface); + + m_XDGExported.on_handle() = [&] (std::string handleStr) { + OnExportHandleEvent(handleStr); + }; +} + +void WaylandDisplayWindow::InitializePopup() +{ + if (!m_owner) + throw std::runtime_error("Popup window must have an owner!"); + + wayland::xdg_positioner_t popupPositioner = backend->m_XDGWMBase.create_positioner(); + + popupPositioner.set_anchor(wayland::xdg_positioner_anchor::bottom); + popupPositioner.set_anchor_rect(0, 0, 1, 30); + popupPositioner.set_size(1, 1); + + m_XDGPopup = m_XDGSurface.get_popup(m_owner->m_XDGSurface, popupPositioner); + + m_XDGPopup.on_configure() = [&] (int32_t x, int32_t y, int32_t width, int32_t height) { + SetClientFrame(Rect::xywh(x, y, width, height)); + }; + + //m_XDGPopup.on_repositioned() + + m_XDGPopup.on_popup_done() = [&] () { + OnExitEvent(); + }; + + m_AppSurface.commit(); + + backend->s_waylandDisplay.roundtrip(); +} + + +void WaylandDisplayWindow::SetWindowTitle(const std::string& text) +{ + if (m_XDGToplevel) + m_XDGToplevel.set_title(text); +} + +void WaylandDisplayWindow::SetWindowFrame(const Rect& box) +{ + // Resizing will be shown on the next commit + CreateBuffers(box.width, box.height); + windowHost->OnWindowGeometryChanged(); + m_NeedsUpdate = true; + m_AppSurface.commit(); +} + +void WaylandDisplayWindow::SetClientFrame(const Rect& box) +{ + SetWindowFrame(box); +} + +void WaylandDisplayWindow::Show() +{ + m_AppSurface.attach(m_AppSurfaceBuffer, 0, 0); + m_AppSurface.damage(0, 0, m_WindowSize.width, m_WindowSize.height); + m_AppSurface.commit(); +} + +void WaylandDisplayWindow::ShowFullscreen() +{ + if (m_XDGToplevel) + { + m_XDGToplevel.set_fullscreen(backend->m_waylandOutput); + isFullscreen = true; + } +} + +void WaylandDisplayWindow::ShowMaximized() +{ + if (m_XDGToplevel) + m_XDGToplevel.set_maximized(); +} + +void WaylandDisplayWindow::ShowMinimized() +{ + if (m_XDGToplevel) + m_XDGToplevel.set_minimized(); +} + +void WaylandDisplayWindow::ShowNormal() +{ + if (m_XDGToplevel) + m_XDGToplevel.unset_fullscreen(); +} + +bool WaylandDisplayWindow::IsWindowFullscreen() +{ + return isFullscreen; +} + +void WaylandDisplayWindow::Hide() +{ + // Apparently this is how hiding a window works + // By attaching a null buffer to the surface + // See: https://lists.freedesktop.org/archives/wayland-devel/2017-November/035963.html + m_AppSurface.attach(nullptr, 0, 0); + m_AppSurface.commit(); +} + +void WaylandDisplayWindow::Activate() +{ + // To do: this needs to be in the backend instance if all windows share the activation token + wayland::xdg_activation_token_v1_t xdgActivationToken = backend->m_XDGActivation.get_activation_token(); + + std::string tokenString; + + xdgActivationToken.on_done() = [&tokenString] (std::string obtainedString) { + tokenString = obtainedString; + }; + + xdgActivationToken.set_surface(m_AppSurface); + xdgActivationToken.commit(); // This will set our token string + + backend->m_XDGActivation.activate(tokenString, m_AppSurface); + backend->m_FocusWindow = this; + backend->m_MouseFocusWindow = this; + windowHost->OnWindowActivated(); +} + +void WaylandDisplayWindow::ShowCursor(bool enable) +{ + backend->ShowCursor(enable); +} + +void WaylandDisplayWindow::LockCursor() +{ + m_LockedPointer = backend->m_PointerConstraints.lock_pointer(m_AppSurface, backend->m_waylandPointer, nullptr, wayland::zwp_pointer_constraints_v1_lifetime::persistent); + backend->SetMouseLocked(true); + ShowCursor(false); +} + +void WaylandDisplayWindow::UnlockCursor() +{ + if (m_LockedPointer) + m_LockedPointer.proxy_release(); + backend->SetMouseLocked(false); + ShowCursor(true); +} + +void WaylandDisplayWindow::CaptureMouse() +{ + m_ConfinedPointer = backend->m_PointerConstraints.confine_pointer(GetWindowSurface(), backend->m_waylandPointer, nullptr, wayland::zwp_pointer_constraints_v1_lifetime::persistent); + ShowCursor(false); +} + +void WaylandDisplayWindow::ReleaseMouseCapture() +{ + if (m_ConfinedPointer) + m_ConfinedPointer.proxy_release(); + ShowCursor(true); +} + +void WaylandDisplayWindow::Update() +{ + m_NeedsUpdate = true; +} + +bool WaylandDisplayWindow::GetKeyState(InputKey key) +{ + return backend->GetKeyState(key); +} + +void WaylandDisplayWindow::SetCursor(StandardCursor cursor) +{ + backend->SetCursor(cursor); +} + +Rect WaylandDisplayWindow::GetWindowFrame() const +{ + return Rect(m_WindowGlobalPos.x, m_WindowGlobalPos.y, m_WindowSize.width, m_WindowSize.height); +} + +Size WaylandDisplayWindow::GetClientSize() const +{ + return m_WindowSize / m_ScaleFactor; +} + +int WaylandDisplayWindow::GetPixelWidth() const +{ + return m_WindowSize.width; +} + +int WaylandDisplayWindow::GetPixelHeight() const +{ + return m_WindowSize.height; +} + +double WaylandDisplayWindow::GetDpiScale() const +{ + return m_ScaleFactor; +} + +void WaylandDisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + // Make new buffers if the sizes don't match + if (width != m_WindowSize.width || height != m_WindowSize.height) + CreateBuffers(width, height); + + std::memcpy(shared_mem->get_mem(), (void*)pixels, width * height * 4); +} + +void WaylandDisplayWindow::SetBorderColor(uint32_t bgra8) +{ + +} + +void WaylandDisplayWindow::SetCaptionColor(uint32_t bgra8) +{ + +} + +void WaylandDisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ + +} + +std::string WaylandDisplayWindow::GetClipboardText() +{ + return m_ClipboardContents; +} + +void WaylandDisplayWindow::SetClipboardText(const std::string& text) +{ + m_ClipboardContents = text; +} + +Point WaylandDisplayWindow::MapFromGlobal(const Point& pos) const +{ + return (pos - m_WindowGlobalPos) / m_ScaleFactor; +} + +Point WaylandDisplayWindow::MapToGlobal(const Point& pos) const +{ + return (m_WindowGlobalPos + pos) / m_ScaleFactor; +} + +void WaylandDisplayWindow::OnXDGToplevelConfigureEvent(int32_t width, int32_t height) +{ + Rect rect = GetWindowFrame(); + rect.width = width / m_ScaleFactor; + rect.height = height / m_ScaleFactor; + SetWindowFrame(rect); + windowHost->OnWindowGeometryChanged(); +} + +void WaylandDisplayWindow::OnExportHandleEvent(std::string exportedHandle) +{ + m_windowID = exportedHandle; +} + +void WaylandDisplayWindow::OnExitEvent() +{ + windowHost->OnWindowClose(); +} + +void WaylandDisplayWindow::DrawSurface(uint32_t serial) +{ + m_AppSurface.attach(m_AppSurfaceBuffer, 0, 0); + m_AppSurface.damage(0, 0, m_WindowSize.width, m_WindowSize.height); + + if (m_renderAPI == RenderAPI::Unspecified || m_renderAPI == RenderAPI::Bitmap) + { + m_FrameCallback = m_AppSurface.frame(); + + m_FrameCallback.on_done() = bind_mem_fn(&WaylandDisplayWindow::DrawSurface, this); + } + m_AppSurface.commit(); +} + +void WaylandDisplayWindow::CreateBuffers(int32_t width, int32_t height) +{ + if (width == 0 || height == 0) + return; + + if (shared_mem) + shared_mem.reset(); + + int scaled_width = width * m_ScaleFactor; + int scaled_height = height * m_ScaleFactor; + + shared_mem = std::make_shared(scaled_width * scaled_height * 4); + auto pool = backend->m_waylandSHM.create_pool(shared_mem->get_fd(), scaled_width * scaled_height * 4); + + m_AppSurfaceBuffer = pool.create_buffer(0, scaled_width, scaled_height, scaled_width * 4, wayland::shm_format::xrgb8888); + + m_WindowSize = Size(scaled_width, scaled_height); +} + +std::string WaylandDisplayWindow::GetWaylandWindowID() +{ + return m_windowID; +} + +// This is to avoid needing all the Vulkan headers and the volk binding library just for this: +#ifndef VK_VERSION_1_0 + +#define VKAPI_CALL +#define VKAPI_PTR VKAPI_CALL + +typedef uint32_t VkFlags; +typedef enum VkStructureType { VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR = 1000006000, VK_OBJECT_TYPE_MAX_ENUM = 0x7FFFFFFF } VkStructureType; +typedef enum VkResult { VK_SUCCESS = 0, VK_RESULT_MAX_ENUM = 0x7FFFFFFF } VkResult; +typedef struct VkAllocationCallbacks VkAllocationCallbacks; + +typedef void (VKAPI_PTR* PFN_vkVoidFunction)(void); +typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_vkGetInstanceProcAddr)(VkInstance instance, const char* pName); + +#ifndef VK_KHR_wayland_surface + +typedef VkFlags VkWaylandSurfaceCreateFlagsKHR; +typedef struct VkWaylandSurfaceCreateInfoKHR +{ + VkStructureType sType; + const void* pNext; + VkWaylandSurfaceCreateFlagsKHR flags; + struct wl_display* display; + struct wl_surface* surface; +} VkWaylandSurfaceCreateInfoKHR; + +typedef VkResult(VKAPI_PTR* PFN_vkCreateWaylandSurfaceKHR)(VkInstance instance, const VkWaylandSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface); + +#endif +#endif + +class ZWidgetWaylandVulkanLoader +{ +public: + ZWidgetWaylandVulkanLoader() + { +#if defined(__APPLE__) + module = dlopen("libvulkan.dylib", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libvulkan.1.dylib", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libMoltenVK.dylib", RTLD_NOW | RTLD_LOCAL); +#else + module = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); +#endif + + if (!module) + throw std::runtime_error("Could not load vulkan"); + + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym(module, "vkGetInstanceProcAddr"); + if (!vkGetInstanceProcAddr) + { + dlclose(module); + throw std::runtime_error("vkGetInstanceProcAddr not found"); + } + } + + ~ZWidgetWaylandVulkanLoader() + { + dlclose(module); + } + + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = nullptr; + void* module = nullptr; +}; + +VkSurfaceKHR WaylandDisplayWindow::CreateVulkanSurface(VkInstance instance) +{ + static ZWidgetWaylandVulkanLoader loader; + + auto vkCreateWaylandSurfaceKHR = (PFN_vkCreateWaylandSurfaceKHR)loader.vkGetInstanceProcAddr(instance, "vkCreateWaylandSurfaceKHR"); + if (!vkCreateWaylandSurfaceKHR) + throw std::runtime_error("Could not create vulkan surface"); + + VkWaylandSurfaceCreateInfoKHR createInfo = { VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR }; + createInfo.display = m_NativeHandle.display; + createInfo.surface = m_NativeHandle.surface; + + VkSurfaceKHR surface = {}; + VkResult result = vkCreateWaylandSurfaceKHR(instance, &createInfo, nullptr, &surface); + if (result != VK_SUCCESS) + throw std::runtime_error("Could not create vulkan surface"); + return surface; +} + +std::vector WaylandDisplayWindow::GetVulkanInstanceExtensions() +{ + return { "VK_KHR_surface", "VK_KHR_wayland_surface" }; +} diff --git a/libraries/ZWidget/src/window/wayland/wayland_display_window.h b/libraries/ZWidget/src/window/wayland/wayland_display_window.h new file mode 100644 index 000000000..5553ece84 --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wayland_display_window.h @@ -0,0 +1,198 @@ +#pragma once + +#include "wayland_display_backend.h" +#include "window/ztimer/ztimer.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "zwidget/window/window.h" +#include "zwidget/window/waylandnativehandle.h" + +#include +#include +#include +#include + +template +std::function bind_mem_fn(R(T::* func)(Args...), T *t) +{ + return [func, t] (Args... args) + { + return (t->*func)(args...); + }; +} + +class SharedMemHelper +{ +public: + SharedMemHelper(size_t size) + : len(size) + { + std::stringstream ss; + std::random_device device; + std::default_random_engine engine(device()); + std::uniform_int_distribution distribution(0, std::numeric_limits::max()); + ss << distribution(engine); + name = ss.str(); + + fd = memfd_create(name.c_str(), 0); + if(fd < 0) + throw std::runtime_error("shm_open failed."); + + if(ftruncate(fd, size) < 0) + throw std::runtime_error("ftruncate failed."); + + mem = mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if(mem == MAP_FAILED) // NOLINT + throw std::runtime_error("mmap failed with len " + std::to_string(len) + "."); + } + + ~SharedMemHelper() noexcept + { + if(fd) + { + munmap(mem, len); + close(fd); + shm_unlink(name.c_str()); + } + } + + int get_fd() const + { + return fd; + } + + void *get_mem() + { + return mem; + } + +private: + std::string name; + int fd = 0; + size_t len = 0; + void *mem = nullptr; +}; + +class WaylandDisplayWindow : public DisplayWindow +{ +public: + WaylandDisplayWindow(WaylandDisplayBackend* backend, DisplayWindowHost* windowHost, bool popupWindow, WaylandDisplayWindow* owner, RenderAPI renderAPI); + ~WaylandDisplayWindow(); + + void SetWindowTitle(const std::string& text) override; + void SetWindowFrame(const Rect& box) override; + void SetClientFrame(const Rect& box) override; + void Show() override; + void ShowFullscreen() override; + void ShowMaximized() override; + void ShowMinimized() override; + void ShowNormal() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(InputKey key) override; + void SetCursor(StandardCursor cursor) override; + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + Point MapFromGlobal(const Point& pos) const override; + Point MapToGlobal(const Point& pos) const override; + + void* GetNativeHandle() override { return (void*)&m_NativeHandle; } + + std::vector GetVulkanInstanceExtensions() override; + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) override; + + wayland::surface_t GetWindowSurface() { return m_AppSurface; } + + bool IsWindowFullscreen() override; + +private: + // Event handlers as otherwise linking DisplayWindowHost On...() functions with Wayland events directly crashes the app + // Alternatively to avoid crashes one can capture by value ([=]) instead of reference ([&]) + void OnXDGToplevelConfigureEvent(int32_t width, int32_t height); + void OnExportHandleEvent(std::string exportedHandle); + void OnExitEvent(); + + void DrawSurface(uint32_t serial = 0); + + void InitializeToplevel(); + void InitializePopup(); + + WaylandDisplayBackend* backend = nullptr; + WaylandDisplayWindow* m_owner = nullptr; + DisplayWindowHost* windowHost = nullptr; + bool m_PopupWindow = false; + + bool m_NeedsUpdate = true; + + Point m_WindowGlobalPos = Point(0, 0); + Size m_WindowSize = Size(0, 0); + double m_ScaleFactor = 1.0; + + Point m_SurfaceMousePos = Point(0, 0); + + WaylandNativeHandle m_NativeHandle; + RenderAPI m_renderAPI; + + wayland::data_device_t m_DataDevice; + wayland::data_source_t m_DataSource; + + wayland::zxdg_toplevel_decoration_v1_t m_XDGToplevelDecoration; + + wayland::fractional_scale_v1_t m_FractionalScale; + + wayland::surface_t m_AppSurface; + wayland::buffer_t m_AppSurfaceBuffer; + + wayland::xdg_surface_t m_XDGSurface; + wayland::xdg_toplevel_t m_XDGToplevel; + wayland::xdg_popup_t m_XDGPopup; + + wayland::zxdg_exported_v2_t m_XDGExported; + + wayland::zwp_locked_pointer_v1_t m_LockedPointer; + wayland::zwp_confined_pointer_v1_t m_ConfinedPointer; + + wayland::callback_t m_FrameCallback; + + std::string m_windowID; + std::string m_ClipboardContents; + + std::shared_ptr shared_mem; + + bool isFullscreen = false; + + // Helper functions + void CreateBuffers(int32_t width, int32_t height); + std::string GetWaylandWindowID(); + + friend WaylandDisplayBackend; +}; diff --git a/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.cpp b/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.cpp new file mode 100644 index 000000000..536ae765d --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.cpp @@ -0,0 +1,205 @@ +#include "wl_fractional_scaling_protocol.hpp" + +using namespace wayland; +using namespace wayland::detail; + +const wl_interface* fractional_scale_manager_v1_interface_destroy_request[0] = { +}; + +const wl_interface* fractional_scale_manager_v1_interface_get_fractional_scale_request[2] = { + &fractional_scale_v1_interface, + &surface_interface, +}; + +const wl_message fractional_scale_manager_v1_interface_requests[2] = { + { + "destroy", + "", + fractional_scale_manager_v1_interface_destroy_request, + }, + { + "get_fractional_scale", + "no", + fractional_scale_manager_v1_interface_get_fractional_scale_request, + }, +}; + +const wl_message fractional_scale_manager_v1_interface_events[0] = { +}; + +const wl_interface wayland::detail::fractional_scale_manager_v1_interface = + { + "wp_fractional_scale_manager_v1", + 1, + 2, + fractional_scale_manager_v1_interface_requests, + 0, + fractional_scale_manager_v1_interface_events, + }; + +const wl_interface* fractional_scale_v1_interface_destroy_request[0] = { +}; + +const wl_interface* fractional_scale_v1_interface_preferred_scale_event[1] = { + nullptr, +}; + +const wl_message fractional_scale_v1_interface_requests[1] = { + { + "destroy", + "", + fractional_scale_v1_interface_destroy_request, + }, +}; + +const wl_message fractional_scale_v1_interface_events[1] = { + { + "preferred_scale", + "u", + fractional_scale_v1_interface_preferred_scale_event, + }, +}; + +const wl_interface wayland::detail::fractional_scale_v1_interface = + { + "wp_fractional_scale_v1", + 1, + 1, + fractional_scale_v1_interface_requests, + 1, + fractional_scale_v1_interface_events, + }; + +fractional_scale_manager_v1_t::fractional_scale_manager_v1_t(const proxy_t &p) + : proxy_t(p) +{ + if(proxy_has_object() && get_wrapper_type() == wrapper_type::standard) + { + set_events(std::shared_ptr(new events_t), dispatcher); + set_destroy_opcode(0U); + } + set_interface(&fractional_scale_manager_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_manager_v1_t(p); }); +} + +fractional_scale_manager_v1_t::fractional_scale_manager_v1_t() +{ + set_interface(&fractional_scale_manager_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_manager_v1_t(p); }); +} + +fractional_scale_manager_v1_t::fractional_scale_manager_v1_t(wp_fractional_scale_manager_v1 *p, wrapper_type t) + : proxy_t(reinterpret_cast (p), t){ + if(proxy_has_object() && get_wrapper_type() == wrapper_type::standard) + { + set_events(std::shared_ptr(new events_t), dispatcher); + set_destroy_opcode(0U); + } + set_interface(&fractional_scale_manager_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_manager_v1_t(p); }); +} + +fractional_scale_manager_v1_t::fractional_scale_manager_v1_t(proxy_t const &wrapped_proxy, construct_proxy_wrapper_tag /*unused*/) + : proxy_t(wrapped_proxy, construct_proxy_wrapper_tag()){ + set_interface(&fractional_scale_manager_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_manager_v1_t(p); }); +} + +fractional_scale_manager_v1_t fractional_scale_manager_v1_t::proxy_create_wrapper() +{ + return {*this, construct_proxy_wrapper_tag()}; +} + +const std::string fractional_scale_manager_v1_t::interface_name = "wp_fractional_scale_manager_v1"; + +fractional_scale_manager_v1_t::operator wp_fractional_scale_manager_v1*() const +{ + return reinterpret_cast (c_ptr()); +} + +fractional_scale_v1_t fractional_scale_manager_v1_t::get_fractional_scale(surface_t const& surface) +{ + proxy_t p = marshal_constructor(1U, &fractional_scale_v1_interface, nullptr, surface.proxy_has_object() ? reinterpret_cast(surface.c_ptr()) : nullptr); + return fractional_scale_v1_t(p); +} + + +int fractional_scale_manager_v1_t::dispatcher(uint32_t opcode, const std::vector& args, const std::shared_ptr& e) +{ + return 0; +} + + +fractional_scale_v1_t::fractional_scale_v1_t(const proxy_t &p) + : proxy_t(p) +{ + if(proxy_has_object() && get_wrapper_type() == wrapper_type::standard) + { + set_events(std::shared_ptr(new events_t), dispatcher); + set_destroy_opcode(0U); + } + set_interface(&fractional_scale_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_v1_t(p); }); +} + +fractional_scale_v1_t::fractional_scale_v1_t() +{ + set_interface(&fractional_scale_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_v1_t(p); }); +} + +fractional_scale_v1_t::fractional_scale_v1_t(wp_fractional_scale_v1 *p, wrapper_type t) + : proxy_t(reinterpret_cast (p), t){ + if(proxy_has_object() && get_wrapper_type() == wrapper_type::standard) + { + set_events(std::shared_ptr(new events_t), dispatcher); + set_destroy_opcode(0U); + } + set_interface(&fractional_scale_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_v1_t(p); }); +} + +fractional_scale_v1_t::fractional_scale_v1_t(proxy_t const &wrapped_proxy, construct_proxy_wrapper_tag /*unused*/) + : proxy_t(wrapped_proxy, construct_proxy_wrapper_tag()){ + set_interface(&fractional_scale_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_v1_t(p); }); +} + +fractional_scale_v1_t fractional_scale_v1_t::proxy_create_wrapper() +{ + return {*this, construct_proxy_wrapper_tag()}; +} + +const std::string fractional_scale_v1_t::interface_name = "wp_fractional_scale_v1"; + +fractional_scale_v1_t::operator wp_fractional_scale_v1*() const +{ + return reinterpret_cast (c_ptr()); +} + +std::function &fractional_scale_v1_t::on_preferred_scale() +{ + return std::static_pointer_cast(get_events())->preferred_scale; +} + +int fractional_scale_v1_t::dispatcher(uint32_t opcode, const std::vector& args, const std::shared_ptr& e) +{ + std::shared_ptr events = std::static_pointer_cast(e); + switch(opcode) + { + case 0: + if(events->preferred_scale) events->preferred_scale(args[0].get()); + break; + } + return 0; +} + + diff --git a/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.hpp b/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.hpp new file mode 100644 index 000000000..859530671 --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +struct wp_fractional_scale_manager_v1; +struct wp_fractional_scale_v1; + +namespace wayland +{ +class fractional_scale_manager_v1_t; +enum class fractional_scale_manager_v1_error : uint32_t; +class fractional_scale_v1_t; + +namespace detail +{ + extern const wl_interface fractional_scale_manager_v1_interface; + extern const wl_interface fractional_scale_v1_interface; +} + +/** \brief fractional surface scale information + + A global interface for requesting surfaces to use fractional scales. + +*/ +class fractional_scale_manager_v1_t : public proxy_t +{ +private: + struct events_t : public detail::events_base_t + { + }; + + static int dispatcher(uint32_t opcode, const std::vector& args, const std::shared_ptr& e); + + fractional_scale_manager_v1_t(proxy_t const &wrapped_proxy, construct_proxy_wrapper_tag /*unused*/); + +public: + fractional_scale_manager_v1_t(); + explicit fractional_scale_manager_v1_t(const proxy_t &proxy); + fractional_scale_manager_v1_t(wp_fractional_scale_manager_v1 *p, wrapper_type t = wrapper_type::standard); + + fractional_scale_manager_v1_t proxy_create_wrapper(); + + static const std::string interface_name; + + operator wp_fractional_scale_manager_v1*() const; + + /** \brief extend surface interface for scale information + \return the new surface scale info interface id + \param surface the surface + + Create an add-on object for the the wl_surface to let the compositor + request fractional scales. If the given wl_surface already has a + wp_fractional_scale_v1 object associated, the fractional_scale_exists + protocol error is raised. + + */ + fractional_scale_v1_t get_fractional_scale(surface_t const& surface); + + /** \brief Minimum protocol version required for the \ref get_fractional_scale function + */ + static constexpr std::uint32_t get_fractional_scale_since_version = 1; + +}; + +/** \brief + + */ +enum class fractional_scale_manager_v1_error : uint32_t + { + /** \brief the surface already has a fractional_scale object associated */ + fractional_scale_exists = 0 +}; + + +/** \brief fractional scale interface to a wl_surface + + An additional interface to a wl_surface object which allows the compositor + to inform the client of the preferred scale. + +*/ +class fractional_scale_v1_t : public proxy_t +{ +private: + struct events_t : public detail::events_base_t + { + std::function preferred_scale; + }; + + static int dispatcher(uint32_t opcode, const std::vector& args, const std::shared_ptr& e); + + fractional_scale_v1_t(proxy_t const &wrapped_proxy, construct_proxy_wrapper_tag /*unused*/); + +public: + fractional_scale_v1_t(); + explicit fractional_scale_v1_t(const proxy_t &proxy); + fractional_scale_v1_t(wp_fractional_scale_v1 *p, wrapper_type t = wrapper_type::standard); + + fractional_scale_v1_t proxy_create_wrapper(); + + static const std::string interface_name; + + operator wp_fractional_scale_v1*() const; + + /** \brief notify of new preferred scale + \param scale the new preferred scale + + Notification of a new preferred scale for this surface that the + compositor suggests that the client should use. + + The sent scale is the numerator of a fraction with a denominator of 120. + + */ + std::function &on_preferred_scale(); + +}; + + + +} diff --git a/libraries/ZWidget/src/window/win32/win32_display_backend.cpp b/libraries/ZWidget/src/window/win32/win32_display_backend.cpp new file mode 100644 index 000000000..0934da042 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_display_backend.cpp @@ -0,0 +1,56 @@ + +#include "win32_display_backend.h" +#include "win32_display_window.h" +#include "win32_open_file_dialog.h" +#include "win32_save_file_dialog.h" +#include "win32_open_folder_dialog.h" + +std::unique_ptr Win32DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return std::make_unique(windowHost, popupWindow, static_cast(owner), renderAPI); +} + +void Win32DisplayBackend::ProcessEvents() +{ + Win32DisplayWindow::ProcessEvents(); +} + +void Win32DisplayBackend::RunLoop() +{ + Win32DisplayWindow::RunLoop(); +} + +void Win32DisplayBackend::ExitLoop() +{ + Win32DisplayWindow::ExitLoop(); +} + +Size Win32DisplayBackend::GetScreenSize() +{ + return Win32DisplayWindow::GetScreenSize(); +} + +void* Win32DisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return Win32DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void Win32DisplayBackend::StopTimer(void* timerID) +{ + Win32DisplayWindow::StopTimer(timerID); +} + +std::unique_ptr Win32DisplayBackend::CreateOpenFileDialog(DisplayWindow* owner) +{ + return std::make_unique(static_cast(owner)); +} + +std::unique_ptr Win32DisplayBackend::CreateSaveFileDialog(DisplayWindow* owner) +{ + return std::make_unique(static_cast(owner)); +} + +std::unique_ptr Win32DisplayBackend::CreateOpenFolderDialog(DisplayWindow* owner) +{ + return std::make_unique(static_cast(owner)); +} diff --git a/libraries/ZWidget/src/window/win32/win32_display_backend.h b/libraries/ZWidget/src/window/win32/win32_display_backend.h new file mode 100644 index 000000000..f641249c2 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_display_backend.h @@ -0,0 +1,23 @@ +#pragma once + +#include "window/window.h" + +class Win32DisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) override; + void ProcessEvents() override; + void RunLoop() override; + void ExitLoop() override; + + void* StartTimer(int timeoutMilliseconds, std::function onTimer) override; + void StopTimer(void* timerID) override; + + Size GetScreenSize() override; + + std::unique_ptr CreateOpenFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateSaveFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateOpenFolderDialog(DisplayWindow* owner) override; + + bool IsWin32() override { return true; } +}; diff --git a/libraries/ZWidget/src/window/win32/win32_display_window.cpp b/libraries/ZWidget/src/window/win32/win32_display_window.cpp new file mode 100644 index 000000000..f45da1a14 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_display_window.cpp @@ -0,0 +1,788 @@ + +#include "win32_display_window.h" +#include +#include +#include +#include +#include + +#pragma comment(lib, "dwmapi.lib") + +#ifndef HID_USAGE_PAGE_GENERIC +#define HID_USAGE_PAGE_GENERIC ((USHORT) 0x01) +#endif + +#ifndef HID_USAGE_GENERIC_MOUSE +#define HID_USAGE_GENERIC_MOUSE ((USHORT) 0x02) +#endif + +#ifndef HID_USAGE_GENERIC_JOYSTICK +#define HID_USAGE_GENERIC_JOYSTICK ((USHORT) 0x04) +#endif + +#ifndef HID_USAGE_GENERIC_GAMEPAD +#define HID_USAGE_GENERIC_GAMEPAD ((USHORT) 0x05) +#endif + +#ifndef RIDEV_INPUTSINK +#define RIDEV_INPUTSINK (0x100) +#endif + +Win32DisplayWindow::Win32DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, Win32DisplayWindow* owner, RenderAPI renderAPI) : WindowHost(windowHost), PopupWindow(popupWindow) +{ + Windows.push_front(this); + WindowsIterator = Windows.begin(); + + WNDCLASSEX classdesc = {}; + classdesc.cbSize = sizeof(WNDCLASSEX); + classdesc.hInstance = GetModuleHandle(0); + classdesc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; + classdesc.lpszClassName = L"ZWidgetWindow"; + classdesc.lpfnWndProc = &Win32DisplayWindow::WndProc; + RegisterClassEx(&classdesc); + + // Microsoft logic at its finest: + // WS_EX_DLGMODALFRAME hides the sysmenu icon + // WS_CAPTION shows the caption (yay! actually a flag that does what it says it does!) + // WS_SYSMENU shows the min/max/close buttons + // WS_THICKFRAME makes the window resizable + + DWORD style = 0, exstyle = 0; + if (popupWindow) + { + exstyle = WS_EX_NOACTIVATE; + style = WS_POPUP; + } + else + { + exstyle = WS_EX_APPWINDOW | WS_EX_DLGMODALFRAME; + style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; + } + CreateWindowEx(exstyle, L"ZWidgetWindow", L"", style, 0, 0, 100, 100, owner ? owner->WindowHandle.hwnd : 0, 0, GetModuleHandle(0), this); +} + +Win32DisplayWindow::~Win32DisplayWindow() +{ + if (WindowHandle.hwnd) + { + DestroyWindow(WindowHandle.hwnd); + WindowHandle.hwnd = 0; + } + + Windows.erase(WindowsIterator); +} + +void Win32DisplayWindow::SetWindowTitle(const std::string& text) +{ + SetWindowText(WindowHandle.hwnd, to_utf16(text).c_str()); +} + +void Win32DisplayWindow::SetBorderColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle.hwnd, 34/*DWMWA_BORDER_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32DisplayWindow::SetCaptionColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle.hwnd, 35/*DWMWA_CAPTION_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32DisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle.hwnd, 36/*DWMWA_TEXT_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32DisplayWindow::SetWindowFrame(const Rect& box) +{ + double dpiscale = GetDpiScale(); + SetWindowPos(WindowHandle.hwnd, 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 Win32DisplayWindow::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.hwnd, GWL_STYLE); + DWORD exstyle = (DWORD)GetWindowLongPtr(WindowHandle.hwnd, GWL_EXSTYLE); + AdjustWindowRectExForDpi(&rect, style, FALSE, exstyle, GetDpiForWindow(WindowHandle.hwnd)); + + SetWindowPos(WindowHandle.hwnd, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE | SWP_NOZORDER); +} + +void Win32DisplayWindow::Show() +{ + ShowWindow(WindowHandle.hwnd, PopupWindow ? SW_SHOWNA : SW_SHOW); +} + +void Win32DisplayWindow::ShowFullscreen() +{ + HDC screenDC = GetDC(0); + int width = GetDeviceCaps(screenDC, HORZRES); + int height = GetDeviceCaps(screenDC, VERTRES); + ReleaseDC(0, screenDC); + DWORD dwStyle = GetWindowLong(WindowHandle.hwnd, GWL_STYLE); + SetWindowLongPtr(WindowHandle.hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW); + SetWindowLongPtr(WindowHandle.hwnd, GWL_STYLE, dwStyle & ~WS_OVERLAPPEDWINDOW); + SetWindowPos(WindowHandle.hwnd, HWND_TOP, 0, 0, width, height, SWP_FRAMECHANGED | SWP_SHOWWINDOW); + Fullscreen = true; +} + +void Win32DisplayWindow::ShowMaximized() +{ + ShowWindow(WindowHandle.hwnd, SW_SHOWMAXIMIZED); +} + +void Win32DisplayWindow::ShowMinimized() +{ + ShowWindow(WindowHandle.hwnd, SW_SHOWMINIMIZED); +} + +void Win32DisplayWindow::ShowNormal() +{ + if (Fullscreen) + { + SetWindowLongPtr(WindowHandle.hwnd, GWL_STYLE, WS_OVERLAPPEDWINDOW); + Fullscreen = false; + } + ShowWindow(WindowHandle.hwnd, SW_NORMAL); +} + +bool Win32DisplayWindow::IsWindowFullscreen() +{ + return Fullscreen; +} + +void Win32DisplayWindow::Hide() +{ + ShowWindow(WindowHandle.hwnd, SW_HIDE); +} + +void Win32DisplayWindow::Activate() +{ + if (!PopupWindow) + SetFocus(WindowHandle.hwnd); +} + +void Win32DisplayWindow::ShowCursor(bool enable) +{ +} + +void Win32DisplayWindow::LockCursor() +{ + if (!MouseLocked) + { + MouseLocked = true; + GetCursorPos(&MouseLockPos); + ::ShowCursor(FALSE); + + RAWINPUTDEVICE rid = {}; + rid.usUsagePage = HID_USAGE_PAGE_GENERIC; + rid.usUsage = HID_USAGE_GENERIC_MOUSE; + rid.dwFlags = RIDEV_INPUTSINK; + rid.hwndTarget = WindowHandle.hwnd; + RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE)); + } +} + +void Win32DisplayWindow::UnlockCursor() +{ + if (MouseLocked) + { + RAWINPUTDEVICE rid = {}; + rid.usUsagePage = HID_USAGE_PAGE_GENERIC; + rid.usUsage = HID_USAGE_GENERIC_MOUSE; + rid.dwFlags = RIDEV_REMOVE; + rid.hwndTarget = 0; + RegisterRawInputDevices(&rid, 1, sizeof(rid)); + + MouseLocked = false; + SetCursorPos(MouseLockPos.x, MouseLockPos.y); + ::ShowCursor(TRUE); + } +} + +void Win32DisplayWindow::CaptureMouse() +{ + SetCapture(WindowHandle.hwnd); +} + +void Win32DisplayWindow::ReleaseMouseCapture() +{ + ReleaseCapture(); +} + +void Win32DisplayWindow::Update() +{ + InvalidateRect(WindowHandle.hwnd, nullptr, FALSE); +} + +bool Win32DisplayWindow::GetKeyState(InputKey key) +{ + return ::GetKeyState((int)key) & 0x8000; // High bit (0x8000) means key is down, Low bit (0x0001) means key is sticky on (like Caps Lock, Num Lock, etc.) +} + +void Win32DisplayWindow::SetCursor(StandardCursor cursor) +{ + if (cursor != CurrentCursor) + { + CurrentCursor = cursor; + UpdateCursor(); + } +} + +Rect Win32DisplayWindow::GetWindowFrame() const +{ + RECT box = {}; + GetWindowRect(WindowHandle.hwnd, &box); + double dpiscale = GetDpiScale(); + return Rect(box.left / dpiscale, box.top / dpiscale, (box.right - box.left) / dpiscale, (box.bottom - box.top) / dpiscale); +} + +Point Win32DisplayWindow::MapFromGlobal(const Point& pos) const +{ + double dpiscale = GetDpiScale(); + POINT point = {}; + point.x = (LONG)std::round(pos.x / dpiscale); + point.y = (LONG)std::round(pos.y / dpiscale); + ScreenToClient(WindowHandle.hwnd, &point); + return Point(point.x * dpiscale, point.y * dpiscale); +} + +Point Win32DisplayWindow::MapToGlobal(const Point& pos) const +{ + double dpiscale = GetDpiScale(); + POINT point = {}; + point.x = (LONG)std::round(pos.x * dpiscale); + point.y = (LONG)std::round(pos.y * dpiscale); + ClientToScreen(WindowHandle.hwnd, &point); + return Point(point.x / dpiscale, point.y / dpiscale); +} + +Size Win32DisplayWindow::GetClientSize() const +{ + RECT box = {}; + GetClientRect(WindowHandle.hwnd, &box); + double dpiscale = GetDpiScale(); + return Size(box.right / dpiscale, box.bottom / dpiscale); +} + +int Win32DisplayWindow::GetPixelWidth() const +{ + RECT box = {}; + GetClientRect(WindowHandle.hwnd, &box); + return box.right; +} + +int Win32DisplayWindow::GetPixelHeight() const +{ + RECT box = {}; + GetClientRect(WindowHandle.hwnd, &box); + return box.bottom; +} + +double Win32DisplayWindow::GetDpiScale() const +{ + return GetDpiForWindow(WindowHandle.hwnd) / 96.0; +} + +std::string Win32DisplayWindow::GetClipboardText() +{ + BOOL result = OpenClipboard(WindowHandle.hwnd); + if (result == FALSE) + throw std::runtime_error("Unable to open clipboard"); + + HANDLE handle = GetClipboardData(CF_UNICODETEXT); + if (handle == 0) + { + CloseClipboard(); + return std::string(); + } + + std::wstring::value_type* data = (std::wstring::value_type*)GlobalLock(handle); + if (data == 0) + { + CloseClipboard(); + return std::string(); + } + std::string str = from_utf16(data); + GlobalUnlock(handle); + + CloseClipboard(); + return str; +} + +void Win32DisplayWindow::SetClipboardText(const std::string& text) +{ + std::wstring text16 = to_utf16(text); + + BOOL result = OpenClipboard(WindowHandle.hwnd); + if (result == FALSE) + throw std::runtime_error("Unable to open clipboard"); + + result = EmptyClipboard(); + if (result == FALSE) + { + CloseClipboard(); + throw std::runtime_error("Unable to empty clipboard"); + } + + unsigned int length = (unsigned int)((text16.length() + 1) * sizeof(std::wstring::value_type)); + HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, length); + if (handle == 0) + { + CloseClipboard(); + throw std::runtime_error("Unable to allocate clipboard memory"); + } + + void* data = GlobalLock(handle); + if (data == 0) + { + GlobalFree(handle); + CloseClipboard(); + throw std::runtime_error("Unable to lock clipboard memory"); + } + memcpy(data, text16.c_str(), length); + GlobalUnlock(handle); + + HANDLE data_result = SetClipboardData(CF_UNICODETEXT, handle); + + if (data_result == 0) + { + GlobalFree(handle); + CloseClipboard(); + throw std::runtime_error("Unable to set clipboard data"); + } + + CloseClipboard(); +} + +void Win32DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + BITMAPV5HEADER header = {}; + header.bV5Size = sizeof(BITMAPV5HEADER); + header.bV5Width = width; + header.bV5Height = -height; + header.bV5Planes = 1; + header.bV5BitCount = 32; + header.bV5Compression = BI_BITFIELDS; + header.bV5AlphaMask = 0xff000000; + header.bV5RedMask = 0x00ff0000; + header.bV5GreenMask = 0x0000ff00; + header.bV5BlueMask = 0x000000ff; + header.bV5SizeImage = width * height * sizeof(uint32_t); + header.bV5CSType = LCS_sRGB; + + HDC dc = PaintDC; + if (dc != 0) + { + SetDIBitsToDevice(dc, 0, 0, width, height, 0, 0, 0, height, pixels, (const BITMAPINFO*)&header, BI_RGB); + } +} + +LRESULT Win32DisplayWindow::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) +{ + LPARAM result = 0; + if (DwmDefWindowProc(WindowHandle.hwnd, msg, wparam, lparam, &result)) + return result; + + if (msg == WM_INPUT) + { + bool hasFocus = GetFocus() != 0; + + HRAWINPUT handle = (HRAWINPUT)lparam; + UINT size = 0; + UINT result = GetRawInputData(handle, RID_INPUT, 0, &size, sizeof(RAWINPUTHEADER)); + if (result == 0 && size > 0) + { + size *= 2; + std::vector buffer(size); + result = GetRawInputData(handle, RID_INPUT, buffer.data(), &size, sizeof(RAWINPUTHEADER)); + if (result >= 0) + { + RAWINPUT* rawinput = (RAWINPUT*)buffer.data(); + if (rawinput->header.dwType == RIM_TYPEMOUSE) + { + if (hasFocus) + WindowHost->OnWindowRawMouseMove(rawinput->data.mouse.lLastX, rawinput->data.mouse.lLastY); + } + } + } + return DefWindowProc(WindowHandle.hwnd, msg, wparam, lparam); + } + else if (msg == WM_PAINT) + { + PAINTSTRUCT paintStruct = {}; + PaintDC = BeginPaint(WindowHandle.hwnd, &paintStruct); + if (PaintDC) + { + WindowHost->OnWindowPaint(); + EndPaint(WindowHandle.hwnd, &paintStruct); + PaintDC = 0; + } + return 0; + } + else if (msg == WM_ACTIVATE) + { + WindowHost->OnWindowActivated(); + } + else if (msg == WM_MOUSEACTIVATE) + { + // We don't want to activate the window on mouse clicks as that changes the focus from the popup owner to the popup itself + if (PopupWindow) + return MA_NOACTIVATE; + } + else if (msg == WM_MOUSEMOVE) + { + if (MouseLocked && GetFocus() != 0) + { + RECT box = {}; + GetClientRect(WindowHandle.hwnd, &box); + + POINT center = {}; + center.x = box.right / 2; + center.y = box.bottom / 2; + ClientToScreen(WindowHandle.hwnd, ¢er); + + SetCursorPos(center.x, center.y); + } + else + { + UpdateCursor(); + } + + if (!TrackMouseActive) + { + TRACKMOUSEEVENT eventTrack = {}; + eventTrack.cbSize = sizeof(TRACKMOUSEEVENT); + eventTrack.hwndTrack = WindowHandle.hwnd; + eventTrack.dwFlags = TME_LEAVE; + if (TrackMouseEvent(&eventTrack)) + TrackMouseActive = true; + } + + WindowHost->OnWindowMouseMove(GetLParamPos(lparam)); + } + else if (msg == WM_MOUSELEAVE) + { + TrackMouseActive = false; + WindowHost->OnWindowMouseLeave(); + } + else if (msg == WM_LBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), InputKey::LeftMouse); + } + else if (msg == WM_LBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), InputKey::LeftMouse); + } + else if (msg == WM_LBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), InputKey::LeftMouse); + } + else if (msg == WM_MBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), InputKey::MiddleMouse); + } + else if (msg == WM_MBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), InputKey::MiddleMouse); + } + else if (msg == WM_MBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), InputKey::MiddleMouse); + } + else if (msg == WM_RBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), InputKey::RightMouse); + } + else if (msg == WM_RBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), InputKey::RightMouse); + } + else if (msg == WM_RBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), InputKey::RightMouse); + } + else if (msg == WM_MOUSEWHEEL) + { + double delta = GET_WHEEL_DELTA_WPARAM(wparam) / (double)WHEEL_DELTA; + + // Note: WM_MOUSEWHEEL uses screen coordinates. GetLParamPos assumes client coordinates. + double dpiscale = GetDpiScale(); + POINT pos; + pos.x = GET_X_LPARAM(lparam); + pos.y = GET_Y_LPARAM(lparam); + ScreenToClient(WindowHandle.hwnd, &pos); + + WindowHost->OnWindowMouseWheel(Point(pos.x / dpiscale, pos.y / dpiscale), delta < 0.0 ? InputKey::MouseWheelDown : InputKey::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((InputKey)wparam); + } + else if (msg == WM_KEYUP) + { + WindowHost->OnWindowKeyUp((InputKey)wparam); + } + else if (msg == WM_SETFOCUS) + { + if (MouseLocked) + { + ::ShowCursor(FALSE); + } + } + else if (msg == WM_KILLFOCUS) + { + if (MouseLocked) + { + ::ShowCursor(TRUE); + } + } + else if (msg == WM_CLOSE) + { + WindowHost->OnWindowClose(); + return 0; + } + else if (msg == WM_SIZE) + { + WindowHost->OnWindowGeometryChanged(); + return 0; + } + /*else if (msg == WM_NCCALCSIZE && wparam == TRUE) // calculate client area for the window + { + NCCALCSIZE_PARAMS* calcsize = (NCCALCSIZE_PARAMS*)lparam; + return WVR_REDRAW; + }*/ + + return DefWindowProc(WindowHandle.hwnd, msg, wparam, lparam); +} + +void Win32DisplayWindow::UpdateCursor() +{ + LPCWSTR cursor = IDC_ARROW; + switch (CurrentCursor) + { + case StandardCursor::arrow: cursor = IDC_ARROW; break; + case StandardCursor::appstarting: cursor = IDC_APPSTARTING; break; + case StandardCursor::cross: cursor = IDC_CROSS; break; + case StandardCursor::hand: cursor = IDC_HAND; break; + case StandardCursor::ibeam: cursor = IDC_IBEAM; break; + case StandardCursor::no: cursor = IDC_NO; break; + case StandardCursor::size_all: cursor = IDC_SIZEALL; break; + case StandardCursor::size_nesw: cursor = IDC_SIZENESW; break; + case StandardCursor::size_ns: cursor = IDC_SIZENS; break; + case StandardCursor::size_nwse: cursor = IDC_SIZENWSE; break; + case StandardCursor::size_we: cursor = IDC_SIZEWE; break; + case StandardCursor::uparrow: cursor = IDC_UPARROW; break; + case StandardCursor::wait: cursor = IDC_WAIT; break; + default: break; + } + + ::SetCursor((HCURSOR)LoadImage(0, cursor, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_DEFAULTSIZE, LR_SHARED)); +} + +Point Win32DisplayWindow::GetLParamPos(LPARAM lparam) const +{ + double dpiscale = GetDpiScale(); + return Point(GET_X_LPARAM(lparam) / dpiscale, GET_Y_LPARAM(lparam) / dpiscale); +} + +LRESULT Win32DisplayWindow::WndProc(HWND windowhandle, UINT msg, WPARAM wparam, LPARAM lparam) +{ + if (msg == WM_CREATE) + { + CREATESTRUCT* createstruct = (CREATESTRUCT*)lparam; + Win32DisplayWindow* viewport = (Win32DisplayWindow*)createstruct->lpCreateParams; + viewport->WindowHandle.hwnd = windowhandle; + SetWindowLongPtr(windowhandle, GWLP_USERDATA, (LONG_PTR)viewport); + return viewport->OnWindowMessage(msg, wparam, lparam); + } + else + { + Win32DisplayWindow* viewport = (Win32DisplayWindow*)GetWindowLongPtr(windowhandle, GWLP_USERDATA); + if (viewport) + { + LRESULT result = viewport->OnWindowMessage(msg, wparam, lparam); + if (msg == WM_DESTROY) + { + SetWindowLongPtr(windowhandle, GWLP_USERDATA, 0); + viewport->WindowHandle.hwnd = 0; + } + return result; + } + else + { + return DefWindowProc(windowhandle, msg, wparam, lparam); + } + } +} + +void Win32DisplayWindow::ProcessEvents() +{ + while (true) + { + MSG msg = {}; + if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE) <= 0) + break; + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +void Win32DisplayWindow::RunLoop() +{ + while (!ExitRunLoop && !Windows.empty()) + { + MSG msg = {}; + if (GetMessage(&msg, 0, 0, 0) <= 0) + break; + TranslateMessage(&msg); + DispatchMessage(&msg); + } + ExitRunLoop = false; +} + +void Win32DisplayWindow::ExitLoop() +{ + ExitRunLoop = true; +} + +Size Win32DisplayWindow::GetScreenSize() +{ + HDC screenDC = GetDC(0); + int screenWidth = GetDeviceCaps(screenDC, HORZRES); + int screenHeight = GetDeviceCaps(screenDC, VERTRES); + double dpiScale = GetDeviceCaps(screenDC, LOGPIXELSX) / 96.0; + ReleaseDC(0, screenDC); + + return Size(screenWidth / dpiScale, screenHeight / dpiScale); +} + +// This is to avoid needing all the Vulkan headers and the volk binding library just for this: +#ifndef VK_VERSION_1_0 + +#define VKAPI_CALL __stdcall +#define VKAPI_PTR VKAPI_CALL + +typedef uint32_t VkFlags; +typedef enum VkStructureType { VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR = 1000009000, VK_OBJECT_TYPE_MAX_ENUM = 0x7FFFFFFF } VkStructureType; +typedef enum VkResult { VK_SUCCESS = 0, VK_RESULT_MAX_ENUM = 0x7FFFFFFF } VkResult; +typedef struct VkAllocationCallbacks VkAllocationCallbacks; + +typedef void (VKAPI_PTR* PFN_vkVoidFunction)(void); +typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_vkGetInstanceProcAddr)(VkInstance instance, const char* pName); + +#ifndef VK_KHR_win32_surface + +typedef VkFlags VkWin32SurfaceCreateFlagsKHR; +typedef struct VkWin32SurfaceCreateInfoKHR +{ + VkStructureType sType; + const void* pNext; + VkWin32SurfaceCreateFlagsKHR flags; + HINSTANCE hinstance; + HWND hwnd; +} VkWin32SurfaceCreateInfoKHR; + +typedef VkResult(VKAPI_PTR* PFN_vkCreateWin32SurfaceKHR)(VkInstance instance, const VkWin32SurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface); + +#endif +#endif + +class ZWidgetVulkanLoader +{ +public: + ZWidgetVulkanLoader() + { + module = LoadLibraryA("vulkan-1.dll"); + if (!module) + throw std::runtime_error("Could not load vulkan-1.dll"); + + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)(void(*)(void))GetProcAddress(module, "vkGetInstanceProcAddr"); + if (!vkGetInstanceProcAddr) + { + FreeLibrary(module); + throw std::runtime_error("vkGetInstanceProcAddr not found in vulkan-1.dll"); + } + } + + ~ZWidgetVulkanLoader() + { + FreeLibrary(module); + } + + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = nullptr; + HMODULE module = {}; +}; + +VkSurfaceKHR Win32DisplayWindow::CreateVulkanSurface(VkInstance instance) +{ + static ZWidgetVulkanLoader loader; + + auto vkCreateWin32SurfaceKHR = (PFN_vkCreateWin32SurfaceKHR)loader.vkGetInstanceProcAddr(instance, "vkCreateWin32SurfaceKHR"); + if (!vkCreateWin32SurfaceKHR) + throw std::runtime_error("Could not create vulkan surface"); + + VkWin32SurfaceCreateInfoKHR createInfo = { VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR }; + createInfo.hwnd = WindowHandle.hwnd; + createInfo.hinstance = GetModuleHandle(nullptr); + + VkSurfaceKHR surface = {}; + VkResult result = vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface); + if (result != VK_SUCCESS) + throw std::runtime_error("Could not create vulkan surface"); + return surface; +} + +std::vector Win32DisplayWindow::GetVulkanInstanceExtensions() +{ + return { "VK_KHR_surface", "VK_KHR_win32_surface" }; +} + +static void CALLBACK Win32TimerCallback(HWND handle, UINT message, UINT_PTR timerID, DWORD timestamp) +{ + auto it = Win32DisplayWindow::Timers.find(timerID); + if (it != Win32DisplayWindow::Timers.end()) + { + auto callback = it->second; + callback(); + } +} + +void* Win32DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + UINT_PTR result = SetTimer(0, 0, timeoutMilliseconds, Win32TimerCallback); + if (result == 0) + throw std::runtime_error("Could not create timer"); + Timers[result] = std::move(onTimer); + return (void*)result; +} + +void Win32DisplayWindow::StopTimer(void* timerID) +{ + auto it = Timers.find((UINT_PTR)timerID); + if (it != Timers.end()) + { + Timers.erase(it); + KillTimer(0, (UINT_PTR)timerID); + } +} + +std::list Win32DisplayWindow::Windows; +bool Win32DisplayWindow::ExitRunLoop; + +std::unordered_map> Win32DisplayWindow::Timers; diff --git a/libraries/ZWidget/src/window/win32/win32_display_window.h b/libraries/ZWidget/src/window/win32/win32_display_window.h new file mode 100644 index 000000000..7666274bd --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_display_window.h @@ -0,0 +1,94 @@ +#pragma once + +#include "win32_util.h" + +#include +#include +#include +#include + +class Win32DisplayWindow : public DisplayWindow +{ +public: + Win32DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, Win32DisplayWindow* owner, RenderAPI renderAPI); + ~Win32DisplayWindow(); + + 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; + bool IsWindowFullscreen() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(InputKey key) override; + + void SetCursor(StandardCursor cursor) override; + void UpdateCursor(); + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + Point MapFromGlobal(const Point& pos) const override; + Point MapToGlobal(const Point& pos) const override; + + Point GetLParamPos(LPARAM lparam) const; + + void* GetNativeHandle() override { return &WindowHandle; } + + std::vector GetVulkanInstanceExtensions() override; + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) override; + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + static Size GetScreenSize(); + + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + + static bool ExitRunLoop; + static std::list Windows; + std::list::iterator WindowsIterator; + + static std::unordered_map> Timers; + + LRESULT OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam); + static LRESULT CALLBACK WndProc(HWND windowhandle, UINT msg, WPARAM wparam, LPARAM lparam); + + DisplayWindowHost* WindowHost = nullptr; + bool PopupWindow = false; + + Win32NativeHandle WindowHandle; + bool Fullscreen = false; + + bool MouseLocked = false; + POINT MouseLockPos = {}; + + bool TrackMouseActive = false; + + HDC PaintDC = 0; + + StandardCursor CurrentCursor = StandardCursor::arrow; +}; diff --git a/libraries/ZWidget/src/window/win32/win32_open_file_dialog.cpp b/libraries/ZWidget/src/window/win32/win32_open_file_dialog.cpp new file mode 100644 index 000000000..092a78da1 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_open_file_dialog.cpp @@ -0,0 +1,227 @@ + +#include "win32_open_file_dialog.h" +#include "win32_display_window.h" +#include "core/widget.h" +#include +#include +#include +#include +#include + +Win32OpenFileDialog::Win32OpenFileDialog(Win32DisplayWindow* owner) : owner(owner) +{ +} + +bool Win32OpenFileDialog::Show() +{ + std::wstring title16 = to_utf16(title); + std::wstring initial_directory16 = to_utf16(initial_directory); + + HRESULT result; + ComPtr open_dialog; + + result = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, open_dialog.GetIID(), open_dialog.InitPtr()); + throw_if_failed(result, "CoCreateInstance(FileOpenDialog) failed"); + + result = open_dialog->SetTitle(title16.c_str()); + throw_if_failed(result, "IFileOpenDialog.SetTitle failed"); + + if (!initial_filename.empty()) + { + result = open_dialog->SetFileName(to_utf16(initial_filename).c_str()); + throw_if_failed(result, "IFileOpenDialog.SetFileName failed"); + } + + FILEOPENDIALOGOPTIONS options = FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST; + if (multi_select) + options |= FOS_ALLOWMULTISELECT; + result = open_dialog->SetOptions(options); + throw_if_failed(result, "IFileOpenDialog.SetOptions() failed"); + + if (!filters.empty()) + { + std::vector filterspecs(filters.size()); + std::vector descriptions(filters.size()); + std::vector extensions(filters.size()); + for (size_t i = 0; i < filters.size(); i++) + { + descriptions[i] = to_utf16(filters[i].description); + extensions[i] = to_utf16(filters[i].extension); + COMDLG_FILTERSPEC& spec = filterspecs[i]; + spec.pszName = descriptions[i].c_str(); + spec.pszSpec = extensions[i].c_str(); + } + result = open_dialog->SetFileTypes((UINT)filterspecs.size(), filterspecs.data()); + throw_if_failed(result, "IFileOpenDialog.SetFileTypes() failed"); + + if ((size_t)filterindex < filters.size()) + { + result = open_dialog->SetFileTypeIndex(filterindex); + throw_if_failed(result, "IFileOpenDialog.SetFileTypeIndex() failed"); + } + } + + if (!defaultext.empty()) + { + result = open_dialog->SetDefaultExtension(to_utf16(defaultext).c_str()); + throw_if_failed(result, "IFileOpenDialog.SetDefaultExtension() failed"); + } + + if (initial_directory16.length() > 0) + { + LPITEMIDLIST item_id_list = nullptr; + SFGAOF flags = 0; + result = SHParseDisplayName(initial_directory16.c_str(), nullptr, &item_id_list, SFGAO_FILESYSTEM, &flags); + throw_if_failed(result, "SHParseDisplayName failed"); + + ComPtr folder_item; + result = SHCreateShellItem(nullptr, nullptr, item_id_list, folder_item.TypedInitPtr()); + ILFree(item_id_list); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + + /* This code requires Windows Vista or newer: + ComPtr folder_item; + result = SHCreateItemFromParsingName(initial_directory16.c_str(), nullptr, folder_item.GetIID(), folder_item.InitPtr()); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + */ + + if (folder_item) + { + result = open_dialog->SetFolder(folder_item); + throw_if_failed(result, "IFileOpenDialog.SetFolder failed"); + } + } + + // For some reason this can hang deep inside Win32 if we do it on the calling thread! + { + bool done = false; + std::mutex mutex; + std::condition_variable condvar; + + std::thread thread([&]() { + + if (owner) + result = open_dialog->Show(owner->WindowHandle.hwnd); + else + result = open_dialog->Show(0); + + std::unique_lock lock(mutex); + done = true; + condvar.notify_all(); + + }); + + std::unique_lock lock(mutex); + while (!done) + { + DisplayBackend::Get()->ProcessEvents(); + using namespace std::chrono_literals; + condvar.wait_for(lock, 50ms, [&]() { return done; }); + } + lock.unlock(); + thread.join(); + } + + if (SUCCEEDED(result)) + { + ComPtr items; + result = open_dialog->GetResults(items.TypedInitPtr()); + throw_if_failed(result, "IFileOpenDialog.GetSelectedItems failed"); + + DWORD num_items = 0; + result = items->GetCount(&num_items); + throw_if_failed(result, "IShellItemArray.GetCount failed"); + + for (DWORD i = 0; i < num_items; i++) + { + ComPtr item; + result = items->GetItemAt(i, item.TypedInitPtr()); + throw_if_failed(result, "IShellItemArray.GetItemAt failed"); + + WCHAR* buffer = nullptr; + result = item->GetDisplayName(SIGDN_FILESYSPATH, &buffer); + throw_if_failed(result, "IShellItem.GetDisplayName failed"); + + std::wstring output16; + if (buffer) + { + try + { + output16 = buffer; + } + catch (...) + { + CoTaskMemFree(buffer); + throw; + } + } + + CoTaskMemFree(buffer); + filenames.push_back(from_utf16(output16)); + } + return true; + } + else + { + return false; + } +} + +std::string Win32OpenFileDialog::Filename() const +{ + return !filenames.empty() ? filenames.front() : std::string(); +} + +std::vector Win32OpenFileDialog::Filenames() const +{ + return filenames; +} + +void Win32OpenFileDialog::SetMultiSelect(bool new_multi_select) +{ + multi_select = new_multi_select; +} + +void Win32OpenFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void Win32OpenFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void Win32OpenFileDialog::ClearFilters() +{ + filters.clear(); +} + +void Win32OpenFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void Win32OpenFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void Win32OpenFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void Win32OpenFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} + +void Win32OpenFileDialog::throw_if_failed(HRESULT result, const std::string& error) +{ + if (FAILED(result)) + throw std::runtime_error(error); +} diff --git a/libraries/ZWidget/src/window/win32/win32_open_file_dialog.h b/libraries/ZWidget/src/window/win32/win32_open_file_dialog.h new file mode 100644 index 000000000..52ac858f0 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_open_file_dialog.h @@ -0,0 +1,44 @@ +#pragma once + +#include "systemdialogs/open_file_dialog.h" +#include "win32_util.h" + +class Win32DisplayWindow; + +class Win32OpenFileDialog : public OpenFileDialog +{ +public: + Win32OpenFileDialog(Win32DisplayWindow* owner); + + bool Show() override; + std::string Filename() const override; + std::vector Filenames() const override; + void SetMultiSelect(bool new_multi_select) override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + void throw_if_failed(HRESULT result, const std::string& error); + + Win32DisplayWindow* owner = nullptr; + + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + bool multi_select = false; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.cpp b/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.cpp new file mode 100644 index 000000000..f579f0ce7 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.cpp @@ -0,0 +1,111 @@ + +#include "win32_open_folder_dialog.h" +#include "win32_display_window.h" +#include "core/widget.h" +#include + +Win32OpenFolderDialog::Win32OpenFolderDialog(Win32DisplayWindow* owner) : owner(owner) +{ +} + +bool Win32OpenFolderDialog::Show() +{ + std::wstring title16 = to_utf16(title); + std::wstring initial_directory16 = to_utf16(initial_directory); + + HRESULT result; + ComPtr open_dialog; + + result = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, open_dialog.GetIID(), open_dialog.InitPtr()); + throw_if_failed(result, "CoCreateInstance(FileOpenDialog) failed"); + + result = open_dialog->SetTitle(title16.c_str()); + throw_if_failed(result, "IFileOpenDialog.SetTitle failed"); + + result = open_dialog->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST); + throw_if_failed(result, "IFileOpenDialog.SetOptions((FOS_PICKFOLDERS) failed"); + + if (initial_directory16.length() > 0) + { + LPITEMIDLIST item_id_list = nullptr; + SFGAOF flags = 0; + result = SHParseDisplayName(initial_directory16.c_str(), nullptr, &item_id_list, SFGAO_FILESYSTEM, &flags); + throw_if_failed(result, "SHParseDisplayName failed"); + + ComPtr folder_item; + result = SHCreateShellItem(nullptr, nullptr, item_id_list, folder_item.TypedInitPtr()); + ILFree(item_id_list); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + + /* This code requires Windows Vista or newer: + ComPtr folder_item; + result = SHCreateItemFromParsingName(initial_directory16.c_str(), nullptr, folder_item.GetIID(), folder_item.InitPtr()); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + */ + + if (folder_item) + { + result = open_dialog->SetFolder(folder_item); + throw_if_failed(result, "IFileOpenDialog.SetFolder failed"); + } + } + + if (owner) + result = open_dialog->Show(owner->WindowHandle.hwnd); + else + result = open_dialog->Show(0); + + if (SUCCEEDED(result)) + { + ComPtr chosen_folder; + result = open_dialog->GetResult(chosen_folder.TypedInitPtr()); + throw_if_failed(result, "IFileOpenDialog.GetResult failed"); + + WCHAR* buffer = nullptr; + result = chosen_folder->GetDisplayName(SIGDN_FILESYSPATH, &buffer); + throw_if_failed(result, "IShellItem.GetDisplayName failed"); + + std::wstring output_directory16; + if (buffer) + { + try + { + output_directory16 = buffer; + } + catch (...) + { + CoTaskMemFree(buffer); + throw; + } + } + + CoTaskMemFree(buffer); + selected_path = from_utf16(output_directory16); + return true; + } + else + { + return false; + } +} + +std::string Win32OpenFolderDialog::SelectedPath() const +{ + return selected_path; +} + +void Win32OpenFolderDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void Win32OpenFolderDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void Win32OpenFolderDialog::throw_if_failed(HRESULT result, const std::string& error) +{ + if (FAILED(result)) + throw std::runtime_error(error); +} diff --git a/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.h b/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.h new file mode 100644 index 000000000..45f05a26b --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.h @@ -0,0 +1,26 @@ +#pragma once + +#include "systemdialogs/open_folder_dialog.h" +#include "win32_util.h" + +class Win32DisplayWindow; + +class Win32OpenFolderDialog : public OpenFolderDialog +{ +public: + Win32OpenFolderDialog(Win32DisplayWindow* owner); + + bool Show() override; + std::string SelectedPath() const override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + +private: + void throw_if_failed(HRESULT result, const std::string& error); + + Win32DisplayWindow* owner = nullptr; + + std::string selected_path; + std::string initial_directory; + std::string title; +}; diff --git a/libraries/ZWidget/src/window/win32/win32_save_file_dialog.cpp b/libraries/ZWidget/src/window/win32/win32_save_file_dialog.cpp new file mode 100644 index 000000000..02568f1ff --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_save_file_dialog.cpp @@ -0,0 +1,174 @@ + +#include "win32_save_file_dialog.h" +#include "win32_display_window.h" +#include "core/widget.h" + +Win32SaveFileDialog::Win32SaveFileDialog(Win32DisplayWindow* owner) : owner(owner) +{ +} + +bool Win32SaveFileDialog::Show() +{ + std::wstring title16 = to_utf16(title); + std::wstring initial_directory16 = to_utf16(initial_directory); + + HRESULT result; + ComPtr save_dialog; + + result = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_ALL, save_dialog.GetIID(), save_dialog.InitPtr()); + throw_if_failed(result, "CoCreateInstance(FileSaveDialog) failed"); + + result = save_dialog->SetTitle(title16.c_str()); + throw_if_failed(result, "IFileSaveDialog.SetTitle failed"); + + if (!initial_filename.empty()) + { + result = save_dialog->SetFileName(to_utf16(initial_filename).c_str()); + throw_if_failed(result, "IFileSaveDialog.SetFileName failed"); + } + + FILEOPENDIALOGOPTIONS options = FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST; + result = save_dialog->SetOptions(options); + throw_if_failed(result, "IFileSaveDialog.SetOptions() failed"); + + if (!filters.empty()) + { + std::vector filterspecs(filters.size()); + std::vector descriptions(filters.size()); + std::vector extensions(filters.size()); + for (size_t i = 0; i < filters.size(); i++) + { + descriptions[i] = to_utf16(filters[i].description); + extensions[i] = to_utf16(filters[i].extension); + COMDLG_FILTERSPEC& spec = filterspecs[i]; + spec.pszName = descriptions[i].c_str(); + spec.pszSpec = extensions[i].c_str(); + } + result = save_dialog->SetFileTypes((UINT)filterspecs.size(), filterspecs.data()); + throw_if_failed(result, "IFileOpenDialog.SetFileTypes() failed"); + + if ((size_t)filterindex < filters.size()) + { + result = save_dialog->SetFileTypeIndex(filterindex); + throw_if_failed(result, "IFileOpenDialog.SetFileTypeIndex() failed"); + } + } + + if (!defaultext.empty()) + { + result = save_dialog->SetDefaultExtension(to_utf16(defaultext).c_str()); + throw_if_failed(result, "IFileOpenDialog.SetDefaultExtension() failed"); + } + + if (initial_directory16.length() > 0) + { + LPITEMIDLIST item_id_list = nullptr; + SFGAOF flags = 0; + result = SHParseDisplayName(initial_directory16.c_str(), nullptr, &item_id_list, SFGAO_FILESYSTEM, &flags); + throw_if_failed(result, "SHParseDisplayName failed"); + + ComPtr folder_item; + result = SHCreateShellItem(nullptr, nullptr, item_id_list, folder_item.TypedInitPtr()); + ILFree(item_id_list); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + + /* This code requires Windows Vista or newer: + ComPtr folder_item; + result = SHCreateItemFromParsingName(initial_directory16.c_str(), nullptr, folder_item.GetIID(), folder_item.InitPtr()); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + */ + + if (folder_item) + { + result = save_dialog->SetFolder(folder_item); + throw_if_failed(result, "IFileSaveDialog.SetFolder failed"); + } + } + + if (owner) + result = save_dialog->Show(owner->WindowHandle.hwnd); + else + result = save_dialog->Show(0); + + if (SUCCEEDED(result)) + { + ComPtr chosen_folder; + result = save_dialog->GetResult(chosen_folder.TypedInitPtr()); + throw_if_failed(result, "IFileSaveDialog.GetResult failed"); + + WCHAR* buffer = nullptr; + result = chosen_folder->GetDisplayName(SIGDN_FILESYSPATH, &buffer); + throw_if_failed(result, "IShellItem.GetDisplayName failed"); + + std::wstring output16; + if (buffer) + { + try + { + output16 = buffer; + } + catch (...) + { + CoTaskMemFree(buffer); + throw; + } + } + + CoTaskMemFree(buffer); + filename = from_utf16(output16); + return true; + } + else + { + return false; + } +} + +std::string Win32SaveFileDialog::Filename() const +{ + return filename; +} + +void Win32SaveFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void Win32SaveFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void Win32SaveFileDialog::ClearFilters() +{ + filters.clear(); +} + +void Win32SaveFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void Win32SaveFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void Win32SaveFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void Win32SaveFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} + +void Win32SaveFileDialog::throw_if_failed(HRESULT result, const std::string& error) +{ + if (FAILED(result)) + throw std::runtime_error(error); +} diff --git a/libraries/ZWidget/src/window/win32/win32_save_file_dialog.h b/libraries/ZWidget/src/window/win32/win32_save_file_dialog.h new file mode 100644 index 000000000..5a5071423 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_save_file_dialog.h @@ -0,0 +1,42 @@ +#pragma once + +#include "systemdialogs/save_file_dialog.h" +#include "win32_util.h" + +class Win32DisplayWindow; + +class Win32SaveFileDialog : public SaveFileDialog +{ +public: + Win32SaveFileDialog(Win32DisplayWindow* owner); + + bool Show() override; + std::string Filename() const override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + void throw_if_failed(HRESULT result, const std::string& error); + + Win32DisplayWindow* owner = nullptr; + + std::string filename; + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/win32/win32_util.h b/libraries/ZWidget/src/window/win32/win32_util.h new file mode 100644 index 000000000..06f89540e --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_util.h @@ -0,0 +1,60 @@ +#pragma once + +#define NOMINMAX +#define WIN32_MEAN_AND_LEAN +#ifndef WINVER +#define WINVER 0x0605 +#endif +#include +#include +#include + +namespace +{ + 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; + } + + template + class ComPtr + { + public: + ComPtr() { Ptr = nullptr; } + ComPtr(const ComPtr& other) { Ptr = other.Ptr; if (Ptr) Ptr->AddRef(); } + ComPtr(ComPtr&& move) { Ptr = move.Ptr; move.Ptr = nullptr; } + ~ComPtr() { reset(); } + ComPtr& operator=(const ComPtr& other) { if (this != &other) { if (Ptr) Ptr->Release(); Ptr = other.Ptr; if (Ptr) Ptr->AddRef(); } return *this; } + void reset() { if (Ptr) Ptr->Release(); Ptr = nullptr; } + T* get() { return Ptr; } + static IID GetIID() { return __uuidof(T); } + void** InitPtr() { return (void**)TypedInitPtr(); } + T** TypedInitPtr() { reset(); return &Ptr; } + operator T* () const { return Ptr; } + T* operator ->() const { return Ptr; } + T* Ptr; + }; +} diff --git a/libraries/ZWidget/src/window/window.cpp b/libraries/ZWidget/src/window/window.cpp new file mode 100644 index 000000000..5fc35b230 --- /dev/null +++ b/libraries/ZWidget/src/window/window.cpp @@ -0,0 +1,201 @@ + +#include "window/window.h" +#include "window/stub/stub_open_folder_dialog.h" +#include "window/stub/stub_open_file_dialog.h" +#include "window/stub/stub_save_file_dialog.h" +#include "window/sdl2nativehandle.h" +#include "core/widget.h" +#include + +std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return DisplayBackend::Get()->Create(windowHost, popupWindow, owner, renderAPI); +} + +void DisplayWindow::ProcessEvents() +{ + DisplayBackend::Get()->ProcessEvents(); +} + +void DisplayWindow::RunLoop() +{ + DisplayBackend::Get()->RunLoop(); +} + +void DisplayWindow::ExitLoop() +{ + DisplayBackend::Get()->ExitLoop(); +} + +void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return DisplayBackend::Get()->StartTimer(timeoutMilliseconds, onTimer); +} + +void DisplayWindow::StopTimer(void* timerID) +{ + DisplayBackend::Get()->StopTimer(timerID); +} + +Size DisplayWindow::GetScreenSize() +{ + return DisplayBackend::Get()->GetScreenSize(); +} + +///////////////////////////////////////////////////////////////////////////// + +static std::unique_ptr& GetBackendVar() +{ + // In C++, static variables in functions are constructed on first encounter and is destructed in the reverse order when main() ends. + static std::unique_ptr p; + return p; +} + +DisplayBackend* DisplayBackend::Get() +{ + return GetBackendVar().get(); +} + +void DisplayBackend::Set(std::unique_ptr instance) +{ + GetBackendVar() = std::move(instance); +} + +std::unique_ptr DisplayBackend::CreateOpenFileDialog(DisplayWindow* owner) +{ + return std::make_unique(owner); +} + +std::unique_ptr DisplayBackend::CreateSaveFileDialog(DisplayWindow* owner) +{ + return std::make_unique(owner); +} + +std::unique_ptr DisplayBackend::CreateOpenFolderDialog(DisplayWindow* owner) +{ + return std::make_unique(owner); +} + +#ifdef _MSC_VER +#pragma warning(disable: 4996) // warning C4996 : 'getenv' : This function or variable may be unsafe.Consider using _dupenv_s instead.To disable deprecation, use _CRT_SECURE_NO_WARNINGS.See online help for details. +#endif + +std::unique_ptr DisplayBackend::TryCreateBackend() +{ + std::unique_ptr backend; + + // Check if there is an environment variable specified for the desired backend + const char* backendSelectionEnv = std::getenv("ZWIDGET_DISPLAY_BACKEND"); + if (backendSelectionEnv) + { + std::string backendSelectionStr(backendSelectionEnv); + if (backendSelectionStr == "Win32") + { + backend = TryCreateWin32(); + } + else if (backendSelectionStr == "X11") + { + backend = TryCreateX11(); + } + else if (backendSelectionStr == "SDL2") + { + backend = TryCreateSDL2(); + } + } + + if (!backend) + { + backend = TryCreateWin32(); + if (!backend) backend = TryCreateWayland(); + if (!backend) backend = TryCreateX11(); + if (!backend) backend = TryCreateSDL2(); + } + + return backend; +} + +#ifdef WIN32 + +#include "win32/win32_display_backend.h" + +std::unique_ptr DisplayBackend::TryCreateWin32() +{ + return std::make_unique(); +} + +#else + +std::unique_ptr DisplayBackend::TryCreateWin32() +{ + return nullptr; +} + +#endif + +#ifdef USE_SDL2 + +#include "sdl2/sdl2_display_backend.h" + +std::unique_ptr DisplayBackend::TryCreateSDL2() +{ + return std::make_unique(); +} + +#else + +std::unique_ptr DisplayBackend::TryCreateSDL2() +{ + return nullptr; +} + +#endif + +#ifdef USE_X11 + +#include "x11/x11_display_backend.h" + +std::unique_ptr DisplayBackend::TryCreateX11() +{ + try + { + return std::make_unique(); + } + catch (...) + { + return nullptr; + } +} + +#else + +std::unique_ptr DisplayBackend::TryCreateX11() +{ + return nullptr; +} + +#endif + +#ifdef USE_WAYLAND + +#include "wayland/wayland_display_backend.h" + +std::unique_ptr DisplayBackend::TryCreateWayland() +{ + try + { + return std::make_unique(); + } + catch (...) + { + return nullptr; + } +} + +#else + +std::unique_ptr DisplayBackend::TryCreateWayland() +{ + return nullptr; +} + +#endif diff --git a/libraries/ZWidget/src/window/x11/x11_display_backend.cpp b/libraries/ZWidget/src/window/x11/x11_display_backend.cpp new file mode 100644 index 000000000..b97760b3a --- /dev/null +++ b/libraries/ZWidget/src/window/x11/x11_display_backend.cpp @@ -0,0 +1,70 @@ + +#include "x11_display_backend.h" +#include "x11_display_window.h" + +#ifdef USE_DBUS +#include "window/dbus/dbus_open_file_dialog.h" +#include "window/dbus/dbus_save_file_dialog.h" +#include "window/dbus/dbus_open_folder_dialog.h" +#endif + +std::unique_ptr X11DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return std::make_unique(windowHost, popupWindow, static_cast(owner), renderAPI); +} + +void X11DisplayBackend::ProcessEvents() +{ + X11DisplayWindow::ProcessEvents(); +} + +void X11DisplayBackend::RunLoop() +{ + X11DisplayWindow::RunLoop(); +} + +void X11DisplayBackend::ExitLoop() +{ + X11DisplayWindow::ExitLoop(); +} + +Size X11DisplayBackend::GetScreenSize() +{ + return X11DisplayWindow::GetScreenSize(); +} + +void* X11DisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return X11DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void X11DisplayBackend::StopTimer(void* timerID) +{ + X11DisplayWindow::StopTimer(timerID); +} + +#ifdef USE_DBUS +std::unique_ptr X11DisplayBackend::CreateOpenFileDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "x11:" + std::to_string(static_cast(owner)->window); + return std::make_unique(ownerHandle); +} + +std::unique_ptr X11DisplayBackend::CreateSaveFileDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "x11:" + std::to_string(static_cast(owner)->window); + return std::make_unique(ownerHandle); +} + +std::unique_ptr X11DisplayBackend::CreateOpenFolderDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "x11:" + std::to_string(static_cast(owner)->window); + return std::make_unique(ownerHandle); +} +#endif diff --git a/libraries/ZWidget/src/window/x11/x11_display_backend.h b/libraries/ZWidget/src/window/x11/x11_display_backend.h new file mode 100644 index 000000000..9d8046353 --- /dev/null +++ b/libraries/ZWidget/src/window/x11/x11_display_backend.h @@ -0,0 +1,25 @@ +#pragma once + +#include "window/window.h" + +class X11DisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) override; + void ProcessEvents() override; + void RunLoop() override; + void ExitLoop() override; + + void* StartTimer(int timeoutMilliseconds, std::function onTimer) override; + void StopTimer(void* timerID) override; + + Size GetScreenSize() override; + + bool IsX11() override { return true; } + +#ifdef USE_DBUS + std::unique_ptr CreateOpenFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateSaveFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateOpenFolderDialog(DisplayWindow* owner) override; +#endif +}; diff --git a/libraries/ZWidget/src/window/x11/x11_display_window.cpp b/libraries/ZWidget/src/window/x11/x11_display_window.cpp new file mode 100644 index 000000000..5797b0b4f --- /dev/null +++ b/libraries/ZWidget/src/window/x11/x11_display_window.cpp @@ -0,0 +1,1184 @@ + +#include "x11_display_window.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class X11Connection +{ +public: + X11Connection() + { + // If we ever want to support windows on multiple threads: + // XInitThreads(); + + display = XOpenDisplay(nullptr); + if (!display) + throw std::runtime_error("Could not open X11 display"); + + // Make auto-repeat keys detectable + Bool supports_detectable_autorepeat = {}; + XkbSetDetectableAutoRepeat(display, True, &supports_detectable_autorepeat); + + // Loads the XMODIFIERS environment variable to see what IME to use + XSetLocaleModifiers(""); + xim = XOpenIM(display, 0, 0, 0); + if (!xim) + { + // fallback to internal input method + XSetLocaleModifiers("@im=none"); + xim = XOpenIM(display, 0, 0, 0); + } + } + + ~X11Connection() + { + for (auto& it : standardCursors) + XFreeCursor(display, it.second); + if (xim) + XCloseIM(xim); + XCloseDisplay(display); + } + + Display* display = nullptr; + std::map atoms; + std::map windows; + std::map standardCursors; + bool ExitRunLoop = false; + + XIM xim = nullptr; +}; + +static X11Connection* GetX11Connection() +{ + static X11Connection connection; + return &connection; +} + +static Atom GetAtom(const std::string& name) +{ + auto connection = GetX11Connection(); + auto it = connection->atoms.find(name); + if (it != connection->atoms.end()) + return it->second; + + Atom atom = XInternAtom(connection->display, name.c_str(), True); + connection->atoms[name] = atom; + return atom; +} + +X11DisplayWindow::X11DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, X11DisplayWindow* owner, RenderAPI renderAPI) : windowHost(windowHost), owner(owner) +{ + display = GetX11Connection()->display; + + screen = XDefaultScreen(display); + depth = XDefaultDepth(display, screen); + visual = XDefaultVisual(display, screen); + colormap = XDefaultColormap(display, screen); + + int disp_width_px = XDisplayWidth(display, screen); + int disp_height_px = XDisplayHeight(display, screen); + int disp_width_mm = XDisplayWidthMM(display, screen); + double ppi = (disp_width_mm < 24) ? 96.0 : (25.4 * static_cast(disp_width_px) / static_cast(disp_width_mm)); + dpiScale = std::round(ppi / 96.0 * 4.0) / 4.0; // 100%, 125%, 150%, 175%, 200%, etc. + + XSetWindowAttributes attributes = {}; + attributes.backing_store = Always; + attributes.override_redirect = popupWindow ? True : False; + attributes.save_under = popupWindow ? True : False; + attributes.colormap = colormap; + attributes.event_mask = + KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | + EnterWindowMask | LeaveWindowMask | PointerMotionMask | KeymapStateMask | + ExposureMask | StructureNotifyMask | FocusChangeMask | PropertyChangeMask; + + unsigned long mask = CWBackingStore | CWSaveUnder | CWEventMask | CWOverrideRedirect; + + window = XCreateWindow(display, XRootWindow(display, screen), 0, 0, 100, 100, 0, depth, InputOutput, visual, mask, &attributes); + GetX11Connection()->windows[window] = this; + + if (owner) + { + XSetTransientForHint(display, window, owner->window); + } + + // Tell window manager which process this window came from + if (GetAtom("_NET_WM_PID") != None) + { + int32_t pid = getpid(); + if (pid != 0) + { + XChangeProperty(display, window, GetAtom("_NET_WM_PID"), XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&pid, 1); + } + } + + // Tell window manager which machine this window came from + if (GetAtom("WM_CLIENT_MACHINE") != None) + { + std::vector hostname(256); + if (gethostname(hostname.data(), hostname.size()) >= 0) + { + hostname.push_back(0); + XChangeProperty(display, window, GetAtom("WM_CLIENT_MACHINE"), XA_STRING, 8, PropModeReplace, (unsigned char *)hostname.data(), strlen(hostname.data())); + } + } + + // Tell window manager we want to listen to close events + if (GetAtom("WM_DELETE_WINDOW") != None) + { + Atom protocol = GetAtom("WM_DELETE_WINDOW"); + XSetWMProtocols(display, window, &protocol, 1); + } + + // Tell window manager what type of window we are + if (GetAtom("_NET_WM_WINDOW_TYPE") != None) + { + Atom type = None; + if (popupWindow) + { + type = GetAtom("_NET_WM_WINDOW_TYPE_DROPDOWN_MENU"); + if (type == None) + type = GetAtom("_NET_WM_WINDOW_TYPE_POPUP_MENU"); + if (type == None) + type = GetAtom("_NET_WM_WINDOW_TYPE_COMBO"); + } + if (type == None) + type = GetAtom("_NET_WM_WINDOW_TYPE_NORMAL"); + + if (type != None) + { + XChangeProperty(display, window, GetAtom("_NET_WM_WINDOW_TYPE"), XA_ATOM, 32, PropModeReplace, (unsigned char *)&type, 1); + } + } + + // Create input context + if (GetX11Connection()->xim) + { + xic = XCreateIC( + GetX11Connection()->xim, + XNInputStyle, + XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, window, + XNFocusWindow, window, + nullptr); + } +} + +X11DisplayWindow::~X11DisplayWindow() +{ + if (hidden_cursor != None) + { + XFreeCursor(display, hidden_cursor); + XFreePixmap(display, cursor_bitmap); + } + + DestroyBackbuffer(); + XDestroyWindow(display, window); + GetX11Connection()->windows.erase(GetX11Connection()->windows.find(window)); +} + +void X11DisplayWindow::SetWindowTitle(const std::string& text) +{ + XSetStandardProperties(display, window, text.c_str(), text.c_str(), None, nullptr, 0, nullptr); +} + +void X11DisplayWindow::SetWindowFrame(const Rect& box) +{ + // To do: this requires cooperation with the window manager + + SetClientFrame(box); +} + +void X11DisplayWindow::SetClientFrame(const Rect& box) +{ + double dpiscale = GetDpiScale(); + int x = (int)std::round(box.x * dpiscale); + int y = (int)std::round(box.y * dpiscale); + int width = (int)std::round(box.width * dpiscale); + int height = (int)std::round(box.height * dpiscale); + + XWindowChanges changes = {}; + changes.x = x; + changes.y = y; + changes.width = width; + changes.height = height; + unsigned int mask = CWX | CWY | CWWidth | CWHeight; + + XConfigureWindow(display, window, mask, &changes); +} + +void X11DisplayWindow::Show() +{ + if (!isMapped) + { + XMapRaised(display, window); + isMapped = true; + } +} + +void X11DisplayWindow::ShowFullscreen() +{ + Show(); + + if (GetAtom("_NET_WM_STATE") != None && GetAtom("_NET_WM_STATE_FULLSCREEN") != None) + { + Atom state = GetAtom("_NET_WM_STATE_FULLSCREEN"); + XChangeProperty(display, window, GetAtom("_NET_WM_STATE"), XA_ATOM, 32, PropModeReplace, (unsigned char *)&state, 1); + isFullscreen = true; + } +} + +bool X11DisplayWindow::IsWindowFullscreen() +{ + return isFullscreen; +} + +void X11DisplayWindow::ShowMaximized() +{ + Show(); +} + +void X11DisplayWindow::ShowMinimized() +{ + if (!isMinimized) + { + Show(); // To do: can this be avoided? WMHints has an initial state that can make it show minimized + XIconifyWindow(display, window, screen); + isMinimized = true; + } +} + +void X11DisplayWindow::ShowNormal() +{ + Show(); + isFullscreen = false; +} + +void X11DisplayWindow::Hide() +{ + if (isMapped) + { + XUnmapWindow(display, window); + isMapped = false; + } +} + +void X11DisplayWindow::Activate() +{ + XRaiseWindow(display, window); +} + +void X11DisplayWindow::ShowCursor(bool enable) +{ + if (isCursorEnabled != enable) + { + isCursorEnabled = enable; + UpdateCursor(); + } +} + +void X11DisplayWindow::LockCursor() +{ + ShowCursor(false); +} + +void X11DisplayWindow::UnlockCursor() +{ + ShowCursor(true); +} + +void X11DisplayWindow::CaptureMouse() +{ + ShowCursor(false); +} + +void X11DisplayWindow::ReleaseMouseCapture() +{ + ShowCursor(true); +} + +void X11DisplayWindow::Update() +{ + needsUpdate = true; +} + +bool X11DisplayWindow::GetKeyState(InputKey key) +{ + auto it = keyState.find(key); + return it != keyState.end() ? it->second : false; +} + +void X11DisplayWindow::SetCursor(StandardCursor newcursor) +{ + if (cursor != newcursor) + { + cursor = newcursor; + UpdateCursor(); + } +} + +void X11DisplayWindow::UpdateCursor() +{ + if (isCursorEnabled) + { + Cursor& x11cursor = GetX11Connection()->standardCursors[cursor]; + if (x11cursor == None) + { + unsigned int index = XC_left_ptr; + switch (cursor) + { + default: + case StandardCursor::arrow: index = XC_left_ptr; break; + case StandardCursor::appstarting: index = XC_watch; break; + case StandardCursor::cross: index = XC_cross; break; + case StandardCursor::hand: index = XC_hand2; break; + case StandardCursor::ibeam: index = XC_xterm; break; + case StandardCursor::size_all: index = XC_fleur; break; + case StandardCursor::size_ns: index = XC_double_arrow; break; + case StandardCursor::size_we: index = XC_sb_h_double_arrow; break; + case StandardCursor::uparrow: index = XC_sb_up_arrow; break; + case StandardCursor::wait: index = XC_watch; break; + case StandardCursor::no: index = XC_X_cursor; break; + case StandardCursor::size_nesw: break; // To do: need to map this + case StandardCursor::size_nwse: break; + } + x11cursor = XCreateFontCursor(display, index); + } + XDefineCursor(display, window, x11cursor); + } + else + { + if (hidden_cursor == None) + { + char data[64] = {}; + XColor black_color = {}; + cursor_bitmap = XCreateBitmapFromData(display, window, data, 8, 8); + hidden_cursor = XCreatePixmapCursor(display, cursor_bitmap, cursor_bitmap, &black_color, &black_color, 0, 0); + } + XDefineCursor(display, window, hidden_cursor); + } +} + +Rect X11DisplayWindow::GetWindowFrame() const +{ + // To do: this needs to include the window manager frame + + double dpiscale = GetDpiScale(); + + Window root = {}; + int x = 0; + int y = 0; + unsigned int width = 0; + unsigned int height = 0; + unsigned int borderwidth = 0; + unsigned int depth = 0; + Status status = XGetGeometry(display, window, &root, &x, &y, &width, &height, &borderwidth, &depth); + + return Rect::xywh(x / dpiscale, y / dpiscale, width / dpiscale, height / dpiscale); +} + +Size X11DisplayWindow::GetClientSize() const +{ + double dpiscale = GetDpiScale(); + + Window root = {}; + int x = 0; + int y = 0; + unsigned int width = 0; + unsigned int height = 0; + unsigned int borderwidth = 0; + unsigned int depth = 0; + Status status = XGetGeometry(display, window, &root, &x, &y, &width, &height, &borderwidth, &depth); + + return Size(width / dpiscale, height / dpiscale); +} + +int X11DisplayWindow::GetPixelWidth() const +{ + Window root = {}; + int x = 0; + int y = 0; + unsigned int width = 0; + unsigned int height = 0; + unsigned int borderwidth = 0; + unsigned int depth = 0; + Status status = XGetGeometry(display, window, &root, &x, &y, &width, &height, &borderwidth, &depth); + return width; +} + +int X11DisplayWindow::GetPixelHeight() const +{ + Window root = {}; + int x = 0; + int y = 0; + unsigned int width = 0; + unsigned int height = 0; + unsigned int borderwidth = 0; + unsigned int depth = 0; + Status status = XGetGeometry(display, window, &root, &x, &y, &width, &height, &borderwidth, &depth); + return height; +} + +double X11DisplayWindow::GetDpiScale() const +{ + return dpiScale; +} + +void X11DisplayWindow::CreateBackbuffer(int width, int height) +{ + backbuffer.pixels = malloc(width * height * sizeof(uint32_t)); + backbuffer.image = XCreateImage(display, DefaultVisual(display, screen), depth, ZPixmap, 0, (char*)backbuffer.pixels, width, height, 32, 0); + backbuffer.pixmap = XCreatePixmap(display, window, width, height, depth); + backbuffer.width = width; + backbuffer.height = height; +} + +void X11DisplayWindow::DestroyBackbuffer() +{ + if (backbuffer.width > 0 && backbuffer.height > 0) + { + XDestroyImage(backbuffer.image); + XFreePixmap(display, backbuffer.pixmap); + backbuffer.width = 0; + backbuffer.height = 0; + backbuffer.pixmap = None; + backbuffer.image = nullptr; + backbuffer.pixels = nullptr; + } +} + +void X11DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + if (backbuffer.width != width || backbuffer.height != height) + { + DestroyBackbuffer(); + if (width > 0 && height > 0) + CreateBackbuffer(width, height); + } + + if (backbuffer.width == width && backbuffer.height == height) + { + memcpy(backbuffer.pixels, pixels, width * height * sizeof(uint32_t)); + GC gc = XDefaultGC(display, screen); + XPutImage(display, backbuffer.pixmap, gc, backbuffer.image, 0, 0, 0, 0, width, height); + XCopyArea(display, backbuffer.pixmap, window, gc, 0, 0, width, height, BlackPixel(display, screen), WhitePixel(display, screen)); + } +} + +void X11DisplayWindow::SetBorderColor(uint32_t bgra8) +{ +} + +void X11DisplayWindow::SetCaptionColor(uint32_t bgra8) +{ +} + +void X11DisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ +} + +std::vector X11DisplayWindow::GetWindowProperty(Atom property, Atom &actual_type, int &actual_format, unsigned long &item_count) +{ + long read_bytes = 0; + Atom _actual_type = actual_type; + int _actual_format = actual_format; + unsigned long _item_count = item_count; + unsigned long bytes_remaining = 0; + unsigned char *read_data = nullptr; + do + { + int result = XGetWindowProperty( + display, window, property, 0ul, read_bytes, + False, AnyPropertyType, &actual_type, &actual_format, + &_item_count, &bytes_remaining, &read_data); + if (result != Success) + { + actual_type = None; + actual_format = 0; + item_count = 0; + return {}; + } + } while (bytes_remaining > 0); + + item_count = _item_count; + if (!read_data) + return {}; + std::vector buffer(read_data, read_data + read_bytes); + XFree(read_data); + return buffer; +} + +std::string X11DisplayWindow::GetClipboardText() +{ + Atom clipboard = GetAtom("CLIPBOARD"); + if (clipboard == None) + return {}; + + XConvertSelection(display, clipboard, XA_STRING, clipboard, window, CurrentTime); + XFlush(display); + + // Wait 500 ms for a response + XEvent event = {}; + while (true) + { + if (XCheckTypedWindowEvent(display, window, SelectionNotify, &event)) + break; + if (!WaitForEvents(500)) + return {}; + } + + Atom type = None; + int format = 0; + unsigned long count = 0; + std::vector data = GetWindowProperty(clipboard, type, format, count); + if (type != XA_STRING || format != 8 || count <= 0 || data.empty()) + return {}; + + data.push_back(0); + return (char*)data.data(); +} + +void X11DisplayWindow::SetClipboardText(const std::string& text) +{ + clipboardText = text; + + Atom clipboard = GetAtom("CLIPBOARD"); + if (clipboard == None) + return; + + XSetSelectionOwner(display, XA_PRIMARY, window, CurrentTime); + XSetSelectionOwner(display, clipboard, window, CurrentTime); +} + +Point X11DisplayWindow::MapFromGlobal(const Point& pos) const +{ + double dpiscale = GetDpiScale(); + Window root = XRootWindow(display, screen); + Window child = {}; + int srcx = (int)std::round(pos.x * dpiscale); + int srcy = (int)std::round(pos.y * dpiscale); + int destx = 0; + int desty = 0; + Bool result = XTranslateCoordinates(display, root, window, srcx, srcy, &destx, &desty, &child); + return Point(destx / dpiscale, desty / dpiscale); +} + +Point X11DisplayWindow::MapToGlobal(const Point& pos) const +{ + double dpiscale = GetDpiScale(); + Window root = XRootWindow(display, screen); + Window child = {}; + int srcx = (int)std::round(pos.x * dpiscale); + int srcy = (int)std::round(pos.y * dpiscale); + int destx = 0; + int desty = 0; + Bool result = XTranslateCoordinates(display, window, root, srcx, srcy, &destx, &desty, &child); + return Point(destx / dpiscale, desty / dpiscale); +} + +void* X11DisplayWindow::GetNativeHandle() +{ + return reinterpret_cast(window); +} + +bool X11DisplayWindow::WaitForEvents(int timeout) +{ + Display* display = GetX11Connection()->display; + int fd = XConnectionNumber(display); + + struct timeval tv; + if (timeout > 0) + { + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) / 1000; + } + + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + int result = select(fd + 1, &rfds, nullptr, nullptr, timeout >= 0 ? &tv : nullptr); + return result > 0 && FD_ISSET(fd, &rfds); +} + +void X11DisplayWindow::CheckNeedsUpdate() +{ + for (auto& it : GetX11Connection()->windows) + { + if (it.second->needsUpdate) + { + it.second->needsUpdate = false; + it.second->windowHost->OnWindowPaint(); + } + } +} + +void X11DisplayWindow::ProcessEvents() +{ + CheckNeedsUpdate(); + Display* display = GetX11Connection()->display; + while (XPending(display) > 0) + { + XEvent event = {}; + XNextEvent(display, &event); + DispatchEvent(&event); + } +} + +void X11DisplayWindow::RunLoop() +{ + X11Connection* connection = GetX11Connection(); + connection->ExitRunLoop = false; + while (!connection->ExitRunLoop && !connection->windows.empty()) + { + CheckNeedsUpdate(); + XEvent event = {}; + XNextEvent(connection->display, &event); + DispatchEvent(&event); + } +} + +void X11DisplayWindow::ExitLoop() +{ + X11Connection* connection = GetX11Connection(); + connection->ExitRunLoop = true; +} + +void X11DisplayWindow::DispatchEvent(XEvent* event) +{ + X11Connection* connection = GetX11Connection(); + auto it = connection->windows.find(event->xany.window); + if (it != connection->windows.end()) + { + X11DisplayWindow* window = it->second; + window->OnEvent(event); + } +} + +void X11DisplayWindow::OnEvent(XEvent* event) +{ + if (event->type == ConfigureNotify) + OnConfigureNotify(event); + else if (event->type == ClientMessage) + OnClientMessage(event); + else if (event->type == Expose) + OnExpose(event); + else if (event->type == FocusIn) + OnFocusIn(event); + else if (event->type == FocusOut) + OnFocusOut(event); + else if (event->type == PropertyNotify) + OnPropertyNotify(event); + else if (event->type == KeyPress) + OnKeyPress(event); + else if (event->type == KeyRelease) + OnKeyRelease(event); + else if (event->type == ButtonPress) + OnButtonPress(event); + else if (event->type == ButtonRelease) + OnButtonRelease(event); + else if (event->type == MotionNotify) + OnMotionNotify(event); + else if (event->type == LeaveNotify) + OnLeaveNotify(event); + else if (event->type == SelectionClear) + OnSelectionClear(event); + else if (event->type == SelectionNotify) + OnSelectionNotify(event); + else if (event->type == SelectionRequest) + OnSelectionRequest(event); +} + +void X11DisplayWindow::OnConfigureNotify(XEvent* event) +{ + ClientSizeX = event->xconfigure.width; + ClientSizeY = event->xconfigure.height; + windowHost->OnWindowGeometryChanged(); +} + +void X11DisplayWindow::OnClientMessage(XEvent* event) +{ + Atom protocolsAtom = GetAtom("WM_PROTOCOLS"); + if (protocolsAtom != None && event->xclient.message_type == protocolsAtom) + { + Atom deleteAtom = GetAtom("WM_DELETE_WINDOW"); + Atom pingAtom = GetAtom("_NET_WM_PING"); + + Atom protocol = event->xclient.data.l[0]; + if (deleteAtom != None && protocol == deleteAtom) + { + windowHost->OnWindowClose(); + } + else if (pingAtom != None && protocol == pingAtom) + { + XSendEvent(display, RootWindow(display, screen), False, SubstructureNotifyMask | SubstructureRedirectMask, event); + } + } +} + +void X11DisplayWindow::OnExpose(XEvent* event) +{ + windowHost->OnWindowPaint(); +} + +void X11DisplayWindow::OnFocusIn(XEvent* event) +{ + if (xic) + XSetICFocus(xic); + + windowHost->OnWindowActivated(); +} + +void X11DisplayWindow::OnFocusOut(XEvent* event) +{ + windowHost->OnWindowDeactivated(); +} + +void X11DisplayWindow::OnPropertyNotify(XEvent* event) +{ + // Sent when window is minimized, maximized, etc. +} + +InputKey X11DisplayWindow::GetInputKey(XEvent* event) +{ + if (event->type == KeyPress || event->type == KeyRelease) + { + KeySym keysymbol = XkbKeycodeToKeysym(display, event->xkey.keycode, 0, 0); + switch (keysymbol) + { + case XK_BackSpace: return InputKey::Backspace; + case XK_Tab: return InputKey::Tab; + case XK_Return: return InputKey::Enter; + // To do: should we merge them or not? Windows merges them + case XK_Shift_L: return InputKey::Shift; // InputKey::LShift + case XK_Shift_R: return InputKey::Shift; // InputKey::RShift + case XK_Control_L: return InputKey::Ctrl; // InputKey::LControl + case XK_Control_R: return InputKey::Ctrl; // InputKey::RControl + case XK_Meta_L: return InputKey::Alt; + case XK_Meta_R: return InputKey::Alt; + case XK_Pause: return InputKey::Pause; + case XK_Caps_Lock: return InputKey::CapsLock; + case XK_Escape: return InputKey::Escape; + case XK_space: return InputKey::Space; + case XK_Page_Up: return InputKey::PageUp; + case XK_Page_Down: return InputKey::PageDown; + case XK_End: return InputKey::End; + case XK_Home: return InputKey::Home; + case XK_Left: return InputKey::Left; + case XK_Up: return InputKey::Up; + case XK_Right: return InputKey::Right; + case XK_Down: return InputKey::Down; + case XK_Print: return InputKey::Print; + case XK_Execute: return InputKey::Execute; + // case XK_Print_Screen: return InputKey::PrintScrn; + case XK_Insert: return InputKey::Insert; + case XK_Delete: return InputKey::Delete; + case XK_Help: return InputKey::Help; + case XK_0: return InputKey::_0; + case XK_1: return InputKey::_1; + case XK_2: return InputKey::_2; + case XK_3: return InputKey::_3; + case XK_4: return InputKey::_4; + case XK_5: return InputKey::_5; + case XK_6: return InputKey::_6; + case XK_7: return InputKey::_7; + case XK_8: return InputKey::_8; + case XK_9: return InputKey::_9; + case XK_A: case XK_a: return InputKey::A; + case XK_B: case XK_b: return InputKey::B; + case XK_C: case XK_c: return InputKey::C; + case XK_D: case XK_d: return InputKey::D; + case XK_E: case XK_e: return InputKey::E; + case XK_F: case XK_f: return InputKey::F; + case XK_G: case XK_g: return InputKey::G; + case XK_H: case XK_h: return InputKey::H; + case XK_I: case XK_i: return InputKey::I; + case XK_J: case XK_j: return InputKey::J; + case XK_K: case XK_k: return InputKey::K; + case XK_L: case XK_l: return InputKey::L; + case XK_M: case XK_m: return InputKey::M; + case XK_N: case XK_n: return InputKey::N; + case XK_O: case XK_o: return InputKey::O; + case XK_P: case XK_p: return InputKey::P; + case XK_Q: case XK_q: return InputKey::Q; + case XK_R: case XK_r: return InputKey::R; + case XK_S: case XK_s: return InputKey::S; + case XK_T: case XK_t: return InputKey::T; + case XK_U: case XK_u: return InputKey::U; + case XK_V: case XK_v: return InputKey::V; + case XK_W: case XK_w: return InputKey::W; + case XK_X: case XK_x: return InputKey::X; + case XK_Y: case XK_y: return InputKey::Y; + case XK_Z: case XK_z: return InputKey::Z; + case XK_KP_0: return InputKey::NumPad0; + case XK_KP_1: return InputKey::NumPad1; + case XK_KP_2: return InputKey::NumPad2; + case XK_KP_3: return InputKey::NumPad3; + case XK_KP_4: return InputKey::NumPad4; + case XK_KP_5: return InputKey::NumPad5; + case XK_KP_6: return InputKey::NumPad6; + case XK_KP_7: return InputKey::NumPad7; + case XK_KP_8: return InputKey::NumPad8; + case XK_KP_9: return InputKey::NumPad9; + case XK_KP_Multiply: return InputKey::GreyStar; + case XK_KP_Add: return InputKey::GreyPlus; + case XK_KP_Separator: return InputKey::Separator; + case XK_KP_Subtract: return InputKey::GreyMinus; + case XK_KP_Decimal: return InputKey::NumPadPeriod; + case XK_KP_Divide: return InputKey::GreySlash; + case XK_F1: return InputKey::F1; + case XK_F2: return InputKey::F2; + case XK_F3: return InputKey::F3; + case XK_F4: return InputKey::F4; + case XK_F5: return InputKey::F5; + case XK_F6: return InputKey::F6; + case XK_F7: return InputKey::F7; + case XK_F8: return InputKey::F8; + case XK_F9: return InputKey::F9; + case XK_F10: return InputKey::F10; + case XK_F11: return InputKey::F11; + case XK_F12: return InputKey::F12; + case XK_F13: return InputKey::F13; + case XK_F14: return InputKey::F14; + case XK_F15: return InputKey::F15; + case XK_F16: return InputKey::F16; + case XK_F17: return InputKey::F17; + case XK_F18: return InputKey::F18; + case XK_F19: return InputKey::F19; + case XK_F20: return InputKey::F20; + case XK_F21: return InputKey::F21; + case XK_F22: return InputKey::F22; + case XK_F23: return InputKey::F23; + case XK_F24: return InputKey::F24; + case XK_Num_Lock: return InputKey::NumLock; + case XK_Scroll_Lock: return InputKey::ScrollLock; + case XK_semicolon: return InputKey::Semicolon; + case XK_equal: return InputKey::Equals; + case XK_comma: return InputKey::Comma; + case XK_minus: return InputKey::Minus; + case XK_period: return InputKey::Period; + case XK_slash: return InputKey::Slash; + case XK_dead_tilde: return InputKey::Tilde; + case XK_bracketleft: return InputKey::LeftBracket; + case XK_backslash: return InputKey::Backslash; + case XK_bracketright: return InputKey::RightBracket; + case XK_apostrophe: return InputKey::SingleQuote; + default: return (InputKey)(((uint32_t)keysymbol) << 8); + } + } + else if (event->type == ButtonPress || event->type == ButtonRelease) + { + switch (event->xbutton.button) + { + case 1: return InputKey::LeftMouse; + case 2: return InputKey::MiddleMouse; + case 3: return InputKey::RightMouse; + case 4: return InputKey::MouseWheelUp; + case 5: return InputKey::MouseWheelDown; + // case 6: return InputKey::XButton1; + // case 7: return InputKey::XButton2; + default: break; + } + } + return {}; +} + +Point X11DisplayWindow::GetMousePos(XEvent* event) +{ + double dpiScale = GetDpiScale(); + int x = event->xbutton.x; + int y = event->xbutton.y; + return Point(x / dpiScale, y / dpiScale); +} + +void X11DisplayWindow::OnKeyPress(XEvent* event) +{ + // If we ever want to track keypress repeat: + // char keyboard_state[32]; + // XQueryKeymap(display, keyboard_state); + // unsigned int keycode = event->xkey.keycode; + // bool isrepeat = event->type == KeyPress && keyboard_state[keycode / 8] & (1 << keycode % 8); + + InputKey key = GetInputKey(event); + keyState[key] = true; + windowHost->OnWindowKeyDown(key); + + std::string text; + if (xic) // utf-8 text input + { + Status status = {}; + KeySym keysym = NoSymbol; + char buffer[32] = {}; + Xutf8LookupString(xic, &event->xkey, buffer, sizeof(buffer) - 1, &keysym, &status); + if (status == XLookupChars || status == XLookupBoth) + text = buffer; + } + else // latin-1 input fallback + { + const int buff_size = 16; + char buff[buff_size]; + int result = XLookupString(&event->xkey, buff, buff_size - 1, nullptr, nullptr); + if (result < 0) result = 0; + if (result > (buff_size - 1)) result = buff_size - 1; + buff[result] = 0; + text = std::string(buff, result); + + // Lazy way to convert to utf-8 + for (char& c : text) + { + if (c < 0) + c = '?'; + } + } + + if (!text.empty()) + windowHost->OnWindowKeyChar(std::move(text)); +} + +void X11DisplayWindow::OnKeyRelease(XEvent* event) +{ + InputKey key = GetInputKey(event); + keyState[key] = false; + windowHost->OnWindowKeyUp(key); +} + +void X11DisplayWindow::OnButtonPress(XEvent* event) +{ + InputKey key = GetInputKey(event); + keyState[key] = true; + windowHost->OnWindowMouseDown(GetMousePos(event), key); + // if (lastClickWithin400ms) + // windowHost->OnWindowMouseDoubleclick(GetMousePos(event), InputKey::LeftMouse); +} + +void X11DisplayWindow::OnButtonRelease(XEvent* event) +{ + InputKey key = GetInputKey(event); + keyState[key] = false; + windowHost->OnWindowMouseUp(GetMousePos(event), key); +} + +void X11DisplayWindow::OnMotionNotify(XEvent* event) +{ + double dpiScale = GetDpiScale(); + int x = event->xmotion.x; + int y = event->xmotion.y; + if (isCursorEnabled) + { + windowHost->OnWindowMouseMove(Point(x / dpiScale, y / dpiScale)); + } + else + { + MouseX = ClientSizeX / 2; + MouseY = ClientSizeY / 2; + + if (MouseX != -1 && MouseY != -1) + { + windowHost->OnWindowRawMouseMove(x - MouseX, y - MouseY); + } + + // Warp pointer to the center of the window + XWarpPointer(display, window, window, 0, 0, ClientSizeX, ClientSizeY, ClientSizeX / 2, ClientSizeY / 2); + } + +} + +void X11DisplayWindow::OnLeaveNotify(XEvent* event) +{ + windowHost->OnWindowMouseLeave(); +} + +void X11DisplayWindow::OnSelectionClear(XEvent* event) +{ + clipboardText.clear(); +} + +void X11DisplayWindow::OnSelectionNotify(XEvent* event) +{ + // This is handled in GetClipboardText +} + +void X11DisplayWindow::OnSelectionRequest(XEvent* event) +{ + Atom requestor = event->xselectionrequest.requestor; + if (requestor == window) + return; + + Atom targetsAtom = GetAtom("TARGETS"); + Atom multipleAtom = GetAtom("MULTIPLE"); + + struct Request { Window target; Atom property; }; + std::vector requests; + + if (event->xselectionrequest.target == multipleAtom) + { + Atom actualType = None; + int actualFormat = 0; + unsigned long itemCount = 0; + std::vector data = GetWindowProperty(requestor, actualType, actualFormat, itemCount); + if (data.size() < itemCount * sizeof(Atom)) + return; + + Atom* atoms = (Atom*)data.data(); + for (unsigned long i = 0; i + 1 < itemCount; i += 2) + { + requests.push_back({ atoms[i], atoms[i + 1]}); + } + } + else + { + requests.push_back({ event->xselectionrequest.target, event->xselectionrequest.property }); + } + + for (const Request& request : requests) + { + Window xtarget = request.target; + Atom xproperty = request.property; + + XEvent response = {}; + response.xselection.type = SelectionNotify; + response.xselection.display = event->xselectionrequest.display; + response.xselection.requestor = event->xselectionrequest.requestor; + response.xselection.selection = event->xselectionrequest.selection; + response.xselection.target = event->xselectionrequest.target; + response.xselection.property = xproperty; + response.xselection.time = event->xselectionrequest.time; + + if (xtarget == targetsAtom) + { + Atom newTargets = XA_STRING; + XChangeProperty(display, requestor, xproperty, targetsAtom, 32, PropModeReplace, (unsigned char *)&newTargets, 1); + } + else if (xtarget == XA_STRING) + { + XChangeProperty(display, requestor, xproperty, xtarget, 8, PropModeReplace, (const unsigned char*)clipboardText.c_str(), clipboardText.size()); + } + else + { + response.xselection.property = None; // Is this correct? + } + + XSendEvent(display, requestor, False, 0, &response); + } +} + +Size X11DisplayWindow::GetScreenSize() +{ + X11Connection* connection = GetX11Connection(); + Display* display = connection->display; + int screen = XDefaultScreen(display); + + int disp_width_px = XDisplayWidth(display, screen); + int disp_height_px = XDisplayHeight(display, screen); + int disp_width_mm = XDisplayWidthMM(display, screen); + double ppi = (disp_width_mm < 24) ? 96.0 : (25.4 * static_cast(disp_width_px) / static_cast(disp_width_mm)); + double dpiScale = ppi / 96.0; + + return Size(disp_width_px / dpiScale, disp_height_px / dpiScale); +} + +void* X11DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return nullptr; +} + +void X11DisplayWindow::StopTimer(void* timerID) +{ +} + +// This is to avoid needing all the Vulkan headers and the volk binding library just for this: +#ifndef VK_VERSION_1_0 + +#define VKAPI_CALL +#define VKAPI_PTR VKAPI_CALL + +typedef uint32_t VkFlags; +typedef enum VkStructureType { VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR = 1000004000, VK_OBJECT_TYPE_MAX_ENUM = 0x7FFFFFFF } VkStructureType; +typedef enum VkResult { VK_SUCCESS = 0, VK_RESULT_MAX_ENUM = 0x7FFFFFFF } VkResult; +typedef struct VkAllocationCallbacks VkAllocationCallbacks; + +typedef void (VKAPI_PTR* PFN_vkVoidFunction)(void); +typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_vkGetInstanceProcAddr)(VkInstance instance, const char* pName); + +#ifndef VK_KHR_xlib_surface + +typedef VkFlags VkXlibSurfaceCreateFlagsKHR; +typedef struct VkXlibSurfaceCreateInfoKHR +{ + VkStructureType sType; + const void* pNext; + VkXlibSurfaceCreateFlagsKHR flags; + Display* dpy; + Window window; +} VkXlibSurfaceCreateInfoKHR; + +typedef VkResult(VKAPI_PTR* PFN_vkCreateXlibSurfaceKHR)(VkInstance instance, const VkXlibSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface); + +#endif +#endif + +class ZWidgetX11VulkanLoader +{ +public: + ZWidgetX11VulkanLoader() + { +#if defined(__APPLE__) + module = dlopen("libvulkan.dylib", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libvulkan.1.dylib", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libMoltenVK.dylib", RTLD_NOW | RTLD_LOCAL); +#else + module = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); +#endif + + if (!module) + throw std::runtime_error("Could not load vulkan"); + + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym(module, "vkGetInstanceProcAddr"); + if (!vkGetInstanceProcAddr) + { + dlclose(module); + throw std::runtime_error("vkGetInstanceProcAddr not found"); + } + } + + ~ZWidgetX11VulkanLoader() + { + dlclose(module); + } + + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = nullptr; + void* module = nullptr; +}; + +VkSurfaceKHR X11DisplayWindow::CreateVulkanSurface(VkInstance instance) +{ + static ZWidgetX11VulkanLoader loader; + + auto vkCreateXlibSurfaceKHR = (PFN_vkCreateXlibSurfaceKHR)loader.vkGetInstanceProcAddr(instance, "vkCreateXlibSurfaceKHR"); + if (!vkCreateXlibSurfaceKHR) + throw std::runtime_error("Could not create vulkan surface"); + + VkXlibSurfaceCreateInfoKHR createInfo = { VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR }; + createInfo.dpy = display; + createInfo.window = window; + + VkSurfaceKHR surface = {}; + VkResult result = vkCreateXlibSurfaceKHR(instance, &createInfo, nullptr, &surface); + if (result != VK_SUCCESS) + throw std::runtime_error("Could not create vulkan surface"); + return surface; +} + +std::vector X11DisplayWindow::GetVulkanInstanceExtensions() +{ + return { "VK_KHR_surface", "VK_KHR_xlib_surface" }; +} diff --git a/libraries/ZWidget/src/window/x11/x11_display_window.h b/libraries/ZWidget/src/window/x11/x11_display_window.h new file mode 100644 index 000000000..62a63f151 --- /dev/null +++ b/libraries/ZWidget/src/window/x11/x11_display_window.h @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class X11DisplayWindow : public DisplayWindow +{ +public: + X11DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, X11DisplayWindow* owner, RenderAPI renderAPI); + ~X11DisplayWindow(); + + 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; + bool IsWindowFullscreen() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(InputKey key) override; + + void SetCursor(StandardCursor cursor) override; + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + Point MapFromGlobal(const Point& pos) const override; + Point MapToGlobal(const Point& pos) const override; + + void* GetNativeHandle() override; + + std::vector GetVulkanInstanceExtensions() override; + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) override; + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + static Size GetScreenSize(); + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + +private: + void UpdateCursor(); + + void OnEvent(XEvent* event); + void OnConfigureNotify(XEvent* event); + void OnClientMessage(XEvent* event); + void OnExpose(XEvent* event); + void OnFocusIn(XEvent* event); + void OnFocusOut(XEvent* event); + void OnPropertyNotify(XEvent* event); + void OnKeyPress(XEvent* event); + void OnKeyRelease(XEvent* event); + void OnButtonPress(XEvent* event); + void OnButtonRelease(XEvent* event); + void OnMotionNotify(XEvent* event); + void OnLeaveNotify(XEvent* event); + void OnSelectionClear(XEvent* event); + void OnSelectionNotify(XEvent* event); + void OnSelectionRequest(XEvent* event); + + void CreateBackbuffer(int width, int height); + void DestroyBackbuffer(); + + InputKey GetInputKey(XEvent* event); + Point GetMousePos(XEvent* event); + + static bool WaitForEvents(int timeout); + static void DispatchEvent(XEvent* event); + + static void CheckNeedsUpdate(); + + std::vector GetWindowProperty(Atom property, Atom &actual_type, int &actual_format, unsigned long &item_count); + + DisplayWindowHost* windowHost = nullptr; + X11DisplayWindow* owner = nullptr; + Display* display = nullptr; + Window window = {}; + int screen = 0; + int depth = 0; + Visual* visual = nullptr; + Colormap colormap = {}; + XIC xic = nullptr; + StandardCursor cursor = {}; + bool isCursorEnabled = true; + bool isMapped = false; + bool isMinimized = false; + bool isFullscreen = false; + double dpiScale = 1.0; + + int ClientSizeX = 0; + int ClientSizeY = 0; + int MouseX = -1; + int MouseY = -1; + + Pixmap cursor_bitmap = None; + Cursor hidden_cursor = None; + + std::map keyState; + + std::string clipboardText; + + struct + { + Pixmap pixmap = None; + XImage* image = nullptr; + void* pixels = nullptr; + int width = 0; + int height = 0; + } backbuffer; + + bool needsUpdate = false; + + friend class X11DisplayBackend; +}; diff --git a/libraries/ZWidget/src/window/ztimer/ztimer.cpp b/libraries/ZWidget/src/window/ztimer/ztimer.cpp new file mode 100644 index 000000000..23e82d10a --- /dev/null +++ b/libraries/ZWidget/src/window/ztimer/ztimer.cpp @@ -0,0 +1,73 @@ +#include "ztimer.h" + +ZTimer::ZTimer() +{ +} + +ZTimer::ZTimer(Duration duration_ms) : m_timerDuration(duration_ms) +{ +} + +ZTimer::ZTimer(Duration duration_ms, CallbackFunc callback) + : m_timerDuration(duration_ms), m_callback(callback) +{ +} + +ZTimer::~ZTimer() +{ + Stop(); +} + +void ZTimer::Start() +{ + m_startTime = Clock::now(); + m_currentTime = m_startTime; + m_timerStarted = true; + m_timerFinished = false; +} + +void ZTimer::Stop() +{ + m_timerStarted = false; +} + +void ZTimer::SetDuration(Duration duration_ms) +{ + if (m_timerStarted) + return; + m_timerDuration = duration_ms; +} + +void ZTimer::SetCallback(std::function callback) +{ + if (m_timerStarted) + return; + m_callback = callback; +} + +void ZTimer::SetRepeating(bool value) +{ + if (m_timerStarted) + return; + m_repeatingTimer = value; +} + +void ZTimer::Update(Duration deltaTime) +{ + if (!m_timerStarted) + return; + + m_currentTime += deltaTime; + + if (m_currentTime >= m_startTime + m_timerDuration) + { + m_callback(); + if (!m_repeatingTimer) + { + Stop(); + m_timerFinished = true; + } + else + Start(); + } +} diff --git a/libraries/ZWidget/src/window/ztimer/ztimer.h b/libraries/ZWidget/src/window/ztimer/ztimer.h new file mode 100644 index 000000000..e72489884 --- /dev/null +++ b/libraries/ZWidget/src/window/ztimer/ztimer.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +// ZTimer: A small, independent timer +// Useful for implementing timed events on your backends +class ZTimer +{ +public: + using Duration = std::chrono::duration; + using Clock = std::chrono::system_clock; + using TimePoint = std::chrono::time_point; + using CallbackFunc = std::function; + + ZTimer(); + ZTimer(Duration duration_ms); + ZTimer(Duration duration_ms, CallbackFunc callback); + + ~ZTimer(); + + void Start(); + void Stop(); + void SetDuration(Duration duration_ms); + void SetCallback(CallbackFunc callback); + void SetRepeating(bool value); + void Update(Duration deltaTime); + + bool IsStarted() { return m_timerStarted; } + bool IsFinished() { return m_timerFinished; } + +private: + bool m_timerStarted = false; + bool m_repeatingTimer = false; + bool m_timerFinished = false; + + TimePoint m_startTime; + TimePoint m_currentTime; + Duration m_timerDuration; + CallbackFunc m_callback; +};