From b927a53488d957fddc9013fe7f9c242ca0aae079 Mon Sep 17 00:00:00 2001 From: Magnus Norddahl Date: Tue, 4 Jun 2024 19:01:14 +0200 Subject: [PATCH] Add DisplayBackend for customizing the active backend Add X11 backend Add line drawing to bitmap canvas --- CMakeLists.txt | 12 +- include/zwidget/window/window.h | 24 + src/core/canvas.cpp | 119 ++- src/systemdialogs/open_file_dialog.cpp | 152 +++- src/window/window.cpp | 275 +++++- src/window/x11/x11displaywindow.cpp | 1062 ++++++++++++++++++++++++ src/window/x11/x11displaywindow.h | 130 +++ 7 files changed, 1734 insertions(+), 40 deletions(-) create mode 100644 src/window/x11/x11displaywindow.cpp create mode 100644 src/window/x11/x11displaywindow.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9967b4e..dcc97c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,11 @@ set(ZWIDGET_SDL2_SOURCES src/window/sdl2/sdl2displaywindow.h ) +set(ZWIDGET_X11_SOURCES + src/window/x11/x11displaywindow.cpp + src/window/x11/x11displaywindow.h +) + source_group("src" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/.+") source_group("src\\core" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/core/.+") source_group("src\\core\\picopng" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/core/picopng/.+") @@ -123,6 +128,7 @@ source_group("include\\widgets\\tabwidget" REGULAR_EXPRESSION "${CMAKE_CURRENT_S source_group("include\\window" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/.+") source_group("include\\window\\win32" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/win32/.+") source_group("include\\window\\sdl2" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/sdl2/.+") +source_group("include\\window\\x11" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/x11/.+") source_group("include\\systemdialogs" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/systemdialogs/.+") include_directories(include include/zwidget src) @@ -136,9 +142,9 @@ elseif(APPLE) add_definitions(-DUNIX -D_UNIX) add_link_options(-pthread) else() - set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES}) - set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl) - add_definitions(-DUNIX -D_UNIX) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES} ${ZWIDGET_X11_SOURCES}) + set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl -lX11) + add_definitions(-DUNIX -D_UNIX -DUSE_SDL2 -DUSE_X11) add_link_options(-pthread) endif() diff --git a/include/zwidget/window/window.h b/include/zwidget/window/window.h index a51519a..87412da 100644 --- a/include/zwidget/window/window.h +++ b/include/zwidget/window/window.h @@ -172,3 +172,27 @@ class DisplayWindow virtual void* GetNativeHandle() = 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(); + + virtual ~DisplayBackend() = default; + + virtual std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) = 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; +}; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp index 990fd4b..75fd996 100644 --- a/src/core/canvas.cpp +++ b/src/core/canvas.cpp @@ -216,6 +216,8 @@ class BitmapCanvas : public Canvas void drawImage(const std::shared_ptr& image, const Point& pos) override; void drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color); + void drawLineAntialiased(float x0, float y0, float x1, float y1, Colorf color); + void plot(float x, float y, float alpha, const Colorf& color); void fillTile(float x, float y, float width, float height, Colorf color); void drawTile(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color); @@ -503,7 +505,122 @@ void BitmapCanvas::drawLineUnclipped(const Point& p0, const Point& p1, const Col } else { - // To do: draw line using bresenham + drawLineAntialiased((float)(p0.x * uiscale), (float)(p0.y * uiscale), (float)(p1.x * uiscale), (float)(p1.y * uiscale), color); + } +} + +static float fpart(float x) +{ + return x - std::floor(x); +} + +static float rfpart(float x) +{ + return 1 - fpart(x); +} + +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; +} + +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; + } } } diff --git a/src/systemdialogs/open_file_dialog.cpp b/src/systemdialogs/open_file_dialog.cpp index 0dac3c5..3fca658 100644 --- a/src/systemdialogs/open_file_dialog.cpp +++ b/src/systemdialogs/open_file_dialog.cpp @@ -276,11 +276,161 @@ std::unique_ptr OpenFileDialog::Create(Widget* owner) return std::make_unique(owner); } +#elif defined(__APPLE__) + +class OpenFileDialogImpl : public OpenFileDialog +{ +public: + OpenFileDialogImpl(Widget* owner) + { + } + + std::string Filename() const override + { + return {}; + } + + std::vector Filenames() const override + { + return {}; + } + + 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 &title) override + { + } + + bool Show() override + { + return false; + } +}; + +std::unique_ptr OpenFileDialog::Create(Widget* owner) +{ + return std::make_unique(owner); +} + #else +class OpenFileDialogImpl : public OpenFileDialog +{ +public: + OpenFileDialogImpl(Widget* owner) : owner(owner) + { + } + + std::string Filename() const override + { + return outputFilenames.empty() ? std::string() : outputFilenames.front(); + } + + std::vector Filenames() const override + { + return outputFilenames; + } + + void SetMultiSelect(bool multiselect) override + { + this->multiSelect = multiSelect; + } + + void SetFilename(const std::string &filename) override + { + inputFilename = filename; + } + + void SetDefaultExtension(const std::string& extension) override + { + defaultExt = extension; + } + + void AddFilter(const std::string &filter_description, const std::string &filter_extension) override + { + filters.push_back({ filter_description, filter_extension }); + } + + void ClearFilters() override + { + filters.clear(); + } + + void SetFilterIndex(int filter_index) override + { + this->filter_index = filter_index; + } + + void SetInitialDirectory(const std::string &path) override + { + initialDirectory = path; + } + + void SetTitle(const std::string &title) override + { + this->title = title; + } + + bool Show() override + { + // To do: do a bunch of d-bus stuff. See the following sources: + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html + // https://github.com/makercrew/dbus-sample + // + // To do: create a way to detect if the window is x11 vs wayland + unsigned long x11windowhandle = 0; + if (owner) + x11windowhandle = reinterpret_cast(owner->GetNativeHandle()); + + return false; + } + + Widget* owner = nullptr; + 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; +}; + std::unique_ptr OpenFileDialog::Create(Widget* owner) { - return {}; + return std::make_unique(owner); } #endif diff --git a/src/window/window.cpp b/src/window/window.cpp index b19945f..96998a7 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -2,119 +2,324 @@ #include "window/window.h" #include -#ifdef WIN32 - -#include "win32/win32displaywindow.h" - std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) { - return std::make_unique(windowHost, popupWindow, static_cast(owner)); + return DisplayBackend::Get()->Create(windowHost, popupWindow, owner); } void DisplayWindow::ProcessEvents() { - Win32DisplayWindow::ProcessEvents(); + DisplayBackend::Get()->ProcessEvents(); } void DisplayWindow::RunLoop() { - Win32DisplayWindow::RunLoop(); + DisplayBackend::Get()->RunLoop(); } void DisplayWindow::ExitLoop() { - Win32DisplayWindow::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 Win32DisplayWindow::GetScreenSize(); + return DisplayBackend::Get()->GetScreenSize(); } -void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +///////////////////////////////////////////////////////////////////////////// + +static std::unique_ptr& GetBackendVar() { - return Win32DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); + // 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; } -void DisplayWindow::StopTimer(void* timerID) +DisplayBackend* DisplayBackend::Get() { - Win32DisplayWindow::StopTimer(timerID); + return GetBackendVar().get(); } -#elif defined(__APPLE__) +void DisplayBackend::Set(std::unique_ptr instance) +{ + GetBackendVar() = std::move(instance); +} -std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) +#ifdef WIN32 + +#include "win32/win32displaywindow.h" + +class Win32DisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) 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 Win32DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) { - throw std::runtime_error("DisplayWindow::Create not implemented"); + return std::make_unique(windowHost, popupWindow, static_cast(owner)); } -void DisplayWindow::ProcessEvents() +void Win32DisplayBackend::ProcessEvents() { - throw std::runtime_error("DisplayWindow::ProcessEvents not implemented"); + Win32DisplayWindow::ProcessEvents(); } -void DisplayWindow::RunLoop() +void Win32DisplayBackend::RunLoop() { - throw std::runtime_error("DisplayWindow::RunLoop not implemented"); + Win32DisplayWindow::RunLoop(); } -void DisplayWindow::ExitLoop() +void Win32DisplayBackend::ExitLoop() { - throw std::runtime_error("DisplayWindow::ExitLoop not implemented"); + Win32DisplayWindow::ExitLoop(); } -Size DisplayWindow::GetScreenSize() +Size Win32DisplayBackend::GetScreenSize() { - throw std::runtime_error("DisplayWindow::GetScreenSize not implemented"); + return Win32DisplayWindow::GetScreenSize(); } -void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +void* Win32DisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) { - throw std::runtime_error("DisplayWindow::StartTimer not implemented"); + return Win32DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); } -void DisplayWindow::StopTimer(void* timerID) +void Win32DisplayBackend::StopTimer(void* timerID) { - throw std::runtime_error("DisplayWindow::StopTimer not implemented"); + Win32DisplayWindow::StopTimer(timerID); +} + +std::unique_ptr DisplayBackend::TryCreateWin32() +{ + return std::make_unique(); } #else +std::unique_ptr DisplayBackend::TryCreateWin32() +{ + return nullptr; +} + +#endif + +#ifdef USE_SDL2 + #include "sdl2/sdl2displaywindow.h" -std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) +class SDL2DisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) 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 SDL2DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) { return std::make_unique(windowHost, popupWindow, static_cast(owner)); } -void DisplayWindow::ProcessEvents() +void SDL2DisplayBackend::ProcessEvents() { SDL2DisplayWindow::ProcessEvents(); } -void DisplayWindow::RunLoop() +void SDL2DisplayBackend::RunLoop() { SDL2DisplayWindow::RunLoop(); } -void DisplayWindow::ExitLoop() +void SDL2DisplayBackend::ExitLoop() { SDL2DisplayWindow::ExitLoop(); } -Size DisplayWindow::GetScreenSize() +Size SDL2DisplayBackend::GetScreenSize() { return SDL2DisplayWindow::GetScreenSize(); } -void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +void* SDL2DisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) { return SDL2DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); } -void DisplayWindow::StopTimer(void* timerID) +void SDL2DisplayBackend::StopTimer(void* timerID) { SDL2DisplayWindow::StopTimer(timerID); } +std::unique_ptr DisplayBackend::TryCreateSDL2() +{ + return std::make_unique(); +} + +#else + +std::unique_ptr DisplayBackend::TryCreateSDL2() +{ + return nullptr; +} + +#endif + +#ifdef USE_X11 + +#include "x11/x11displaywindow.h" + +class X11DisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) 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 X11DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) +{ + return std::make_unique(windowHost, popupWindow, static_cast(owner)); +} + +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); +} + +std::unique_ptr DisplayBackend::TryCreateX11() +{ + return std::make_unique(); +} + +#else + +std::unique_ptr DisplayBackend::TryCreateX11() +{ + return nullptr; +} + +#endif + +#ifdef USE_WAYLAND + +#include "wayland/waylanddisplaywindow.h" + +class WaylandDisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) 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 WaylandDisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner) +{ + return std::make_unique(windowHost, popupWindow, static_cast(owner)); +} + +void WaylandDisplayBackend::ProcessEvents() +{ + WaylandDisplayWindow::ProcessEvents(); +} + +void WaylandDisplayBackend::RunLoop() +{ + WaylandDisplayWindow::RunLoop(); +} + +void WaylandDisplayBackend::ExitLoop() +{ + WaylandDisplayWindow::ExitLoop(); +} + +Size WaylandDisplayBackend::GetScreenSize() +{ + return WaylandDisplayWindow::GetScreenSize(); +} + +void* WaylandDisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return WaylandDisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void WaylandDisplayBackend::StopTimer(void* timerID) +{ + WaylandDisplayWindow::StopTimer(timerID); +} + +std::unique_ptr DisplayBackend::TryCreateWayland() +{ + return std::make_unique(); +} + +#else + +std::unique_ptr DisplayBackend::TryCreateWayland() +{ + return nullptr; +} + #endif diff --git a/src/window/x11/x11displaywindow.cpp b/src/window/x11/x11displaywindow.cpp new file mode 100644 index 0000000..2b82717 --- /dev/null +++ b/src/window/x11/x11displaywindow.cpp @@ -0,0 +1,1062 @@ + +#include "x11displaywindow.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) : 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); + } +} + +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(); +} + +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() +{ +} + +void X11DisplayWindow::UnlockCursor() +{ +} + +void X11DisplayWindow::CaptureMouse() +{ +} + +void X11DisplayWindow::ReleaseMouseCapture() +{ +} + +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) +{ + 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::Select; + 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: return InputKey::A; + case XK_B: return InputKey::B; + case XK_C: return InputKey::C; + case XK_D: return InputKey::D; + case XK_E: return InputKey::E; + case XK_F: return InputKey::F; + case XK_G: return InputKey::G; + case XK_H: return InputKey::H; + case XK_I: return InputKey::I; + case XK_J: return InputKey::J; + case XK_K: return InputKey::K; + case XK_L: return InputKey::L; + case XK_M: return InputKey::M; + case XK_N: return InputKey::N; + case XK_O: return InputKey::O; + case XK_P: return InputKey::P; + case XK_Q: return InputKey::Q; + case XK_R: return InputKey::R; + case XK_S: return InputKey::S; + case XK_T: return InputKey::T; + case XK_U: return InputKey::U; + case XK_V: return InputKey::V; + case XK_W: return InputKey::W; + case XK_X: return InputKey::X; + case XK_Y: return InputKey::Y; + 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; + windowHost->OnWindowMouseMove(Point(x / dpiScale, y / dpiScale)); +} + +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) +{ +} diff --git a/src/window/x11/x11displaywindow.h b/src/window/x11/x11displaywindow.h new file mode 100644 index 0000000..1faaf10 --- /dev/null +++ b/src/window/x11/x11displaywindow.h @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class X11DisplayWindow : public DisplayWindow +{ +public: + X11DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, X11DisplayWindow* owner); + ~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; + 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; + + 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; + double dpiScale = 1.0; + + 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; +};