Files
ReWindow/src/platform/linux/Window.cpp
Redacted 2291ee6215
All checks were successful
Run ReCI Build Test / Explore-Gitea-Actions (push) Successful in 1m33s
Linux notify
2025-07-14 19:47:09 -04:00

595 lines
21 KiB
C++

#include <cstring>
#include <thread>
#include <ReWindow/types/Window.h>
#include <ReWindow/types/Cursors.h>
#include <ReWindow/Logger.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <thread>
#include <dbus/dbus.h>
class ReWindow::RWindow::Platform {
public:
Window window;
XEvent xev;
Display* display;
int defaultScreen;
XVisualInfo* visual;
XSetWindowAttributes xSetWindowAttributes;
XWindowAttributes windowAttributes;
Atom wmDeleteWindow;
Atom windowTypeAtom;
Atom windowTypeUtilityAtom;
XSizeHints hints;
Cursor invisible_cursor = 0;
XWMHints* wm_hints = nullptr;
bool window_visible = true;
std::pair<int, int> position = { 0, 0 };
std::pair<int, int> size_before_fullscreen {0, 0};
};
using namespace ReWindow;
bool RWindow::Notify(const std::string& title, const std::string& content, const std::filesystem::path& icon) {
DBusError db_error;
dbus_error_init(&db_error);
DBusConnection* db_connection = dbus_bus_get(DBUS_BUS_SESSION, &db_error);
if (!db_connection) {
dbus_error_free(&db_error);
return false;
}
DBusMessage* db_message = dbus_message_new_method_call(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"Notify"
);
if (!db_message)
return false;
const char* sender = this->title.c_str();
uint32_t replaces_id = 0;
const char* app_icon = icon.c_str();
const char* summary = title.c_str();
const char* body = content.c_str();
const char** actions = nullptr;
DBusMessageIter db_args;
dbus_message_iter_init_append(db_message, &db_args);
dbus_message_iter_append_basic(&db_args, DBUS_TYPE_STRING, &sender);
dbus_message_iter_append_basic(&db_args, DBUS_TYPE_UINT32, &replaces_id);
dbus_message_iter_append_basic(&db_args, DBUS_TYPE_STRING, &app_icon);
dbus_message_iter_append_basic(&db_args, DBUS_TYPE_STRING, &summary);
dbus_message_iter_append_basic(&db_args, DBUS_TYPE_STRING, &body);
DBusMessageIter db_actions_array_iterator;
dbus_message_iter_open_container(&db_args, DBUS_TYPE_ARRAY, "s", &db_actions_array_iterator);
dbus_message_iter_close_container(&db_args, &db_actions_array_iterator);
dbus_message_iter_open_container(&db_args, DBUS_TYPE_ARRAY, "{sv}", &db_actions_array_iterator);
dbus_message_iter_close_container(&db_args, &db_actions_array_iterator);
int32_t notif_timeout = 5000;
dbus_message_iter_append_basic(&db_args, DBUS_TYPE_INT32, &notif_timeout);
DBusMessage* db_reply = dbus_connection_send_with_reply_and_block(db_connection, db_message, -1, &db_error);
dbus_message_unref(db_message);
if (!db_reply) {
dbus_error_free(&db_error);
return false;
}
dbus_message_unref(db_reply);
return true;
}
void RWindow::SetSize(const std::pair<int, int>& size) {
this->SetSize(size.first, size.second);
}
bool RWindow::SetCursorPosition(const std::pair<int, int>& position) {
if (!IsFocused())
return false;
if (!IsVisible())
return false;
if (position.first > GetWidth() || position.first < 0)
return false;
if (position.second > GetHeight() || position.second < 0)
return false;
// TODO XWarpPointer has no way to verify it actually happened.
XWarpPointer(platform->display, None, platform->window, 0, 0, 0, 0, position.first, position.second);
return true;
}
void RWindow::SetKeyRepeatEnabled(bool state) {
key_repeat = state;
if (state)
return (void) XAutoRepeatOn(platform->display);
XAutoRepeatOff(platform->display);
}
void RWindow::Flash() {
if (IsFocused())
return;
if (platform->wm_hints == nullptr)
platform->wm_hints = XAllocWMHints();
platform->wm_hints->flags = XUrgencyHint;
XSetWMHints(platform->display, platform->window, platform->wm_hints);
}
RWindow::RWindow() {
platform = new Platform();
extant = this;
}
void RWindow::SetCursorFocused(bool state) {
toggling_cursor_focus = state;
if (!state && cursor_focused) {
XUngrabPointer(platform->display, CurrentTime);
cursor_focused = state;
return;
}
}
std::pair<int, int> RWindow::SetCursorCenter() {
auto current_pos = GetAccurateCursorCoordinates();
XWarpPointer(platform->display, None, platform->window, 0, 0, 0, 0, width / 2, height / 2);
return current_pos;
}
RWindow::RWindow(const std::string& wTitle, int wWidth, int wHeight, bool wFullscreen, bool wResizable, bool wVsync)
: title(wTitle), width(wWidth), height(wHeight), fullscreen_mode(wFullscreen), resizable(wResizable), vsync(wVsync)
{ platform = new Platform(); extant = this; }
void RWindow::Raise() {
Logger::Debug(std::format("Raising window '{}'", this->title));
// Get the position of the renderable area relative to the rest of the window.
XGetWindowAttributes(platform->display, platform->window, &platform->windowAttributes);
XRaiseWindow(platform->display, platform->window);
}
void RWindow::DialogOK(const std::string& window_title, const std::string& message) {
int padding = 10;
XFontStruct* font = XLoadQueryFont(platform->display, "6x13");
int text_width = XTextWidth(font, message.c_str(), message.size());
int button_text_width = XTextWidth(font, "OK", 2);
std::pair<int, int> button_size = {80, 30};
std::pair<int, int> dimensions = { std::max(text_width + 2 * padding, button_size.first + 2 * padding), 82};
std::pair<int, int> text_pos = {(dimensions.first - text_width) / 2, padding + font->ascent};
std::pair<int, int> button_pos = { (dimensions.first - button_size.first) / 2, text_pos.second + font->ascent + padding };
Window window = XCreateSimpleWindow(platform->display, RootWindow(platform->display, platform->defaultScreen),
100, 100, dimensions.first, dimensions.second, 1,
BlackPixel(platform->display, platform->defaultScreen),
WhitePixel(platform->display, platform->defaultScreen));
XStoreName(platform->display, window, window_title.c_str());
XSelectInput(platform->display, window, ExposureMask | ButtonPressMask | StructureNotifyMask);
// No resizing.
XSizeHints hints;
hints.flags = PMinSize | PMaxSize;
hints.min_width = hints.max_width = dimensions.first;
hints.min_height = hints.max_height = dimensions.second;
XSetWMNormalHints(platform->display, window, &hints);
Atom wm_delete_window = XInternAtom(platform->display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(platform->display, window, &wm_delete_window, 1);
XMapWindow(platform->display, window);
GC gc = XCreateGC(platform->display, window, 0, nullptr);
XSetForeground(platform->display, gc, BlackPixel(platform->display, platform->defaultScreen));
XEvent event;
while (true) {
XNextEvent(platform->display, &event);
if (event.type == Expose) {
XDrawString(platform->display, window, gc, text_pos.first, text_pos.second, message.c_str(), message.size());
XDrawString(platform->display, window, gc, button_pos.first + (button_size.first - button_text_width) / 2, button_pos.second + 20, "OK", 2);
XDrawRectangle(platform->display, window, gc, button_pos.first, button_pos.second, button_size.first, button_size.second);
}
else if (event.type == ClientMessage) {
if (event.xclient.message_type == XInternAtom(platform->display, "WM_PROTOCOLS", False) &&
static_cast<Atom>(event.xclient.data.l[0]) == wm_delete_window)
break;
}
else if (event.type == ButtonPress) {
if (event.xbutton.x >= button_pos.first && event.xbutton.x <= button_pos.first + button_size.first
&& event.xbutton.y >= button_pos.second && event.xbutton.y <= button_pos.second + button_size.second)
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
XFreeFont(platform->display, font);
XFreeGC(platform->display, gc);
XDestroyWindow(platform->display, window);
}
void RWindow::Lower()
{
Logger::Debug(std::format("Lowering window '{}'", this->title));
XLowerWindow(platform->display, platform->window);
}
void RWindow::DestroyOSWindowHandle() {
// Turn key repeat back on.
XAutoRepeatOn(platform->display);
XFlush(platform->display);
Logger::Debug(std::format("Destroying sub-windows for window '{}'", this->title));
XDestroySubwindows(platform->display, platform->window);
Logger::Debug(std::format("Destroyed window '{}'", this->title));
XDestroyWindow(platform->display, platform->window);
XCloseDisplay(platform->display);
delete platform;
}
void RWindow::SetCursorVisible(bool cursor_enable) {
cursor_visible = cursor_enable;
if (platform->invisible_cursor == 0) {
Pixmap blank_pixmap = XCreatePixmap(platform->display, platform->window, 1, 1, 1);
XColor dummy; dummy.pixel = 0; dummy.red = 0; dummy.flags = 0;
platform->invisible_cursor = XCreatePixmapCursor(platform->display, blank_pixmap, blank_pixmap, &dummy, &dummy, 0, 0);
XFreePixmap(platform->display, blank_pixmap);
}
if (!cursor_enable)
XDefineCursor(platform->display, platform->window, platform->invisible_cursor);
if (cursor_enable)
XUndefineCursor(platform->display, platform->window);
}
void RWindow::DisableResizing() {
XGetWindowAttributes(platform->display, platform->window, &platform->windowAttributes);
platform->hints.flags = PMinSize | PMaxSize;
platform->hints.min_width = platform->hints.max_width = platform->windowAttributes.width;
platform->hints.min_height = platform->hints.max_height = platform->windowAttributes.height;
XSetWMNormalHints(platform->display, platform->window, &platform->hints);
this->resizable = false;
}
void RWindow::PollEvents() {
while(XPending(platform->display)) {
XNextEvent(platform->display, &platform->xev);
if (platform->xev.type == ClientMessage)
Logger::Info(std::format("Event '{}'", "ClientMessage"));
if (platform->xev.xclient.message_type == XInternAtom(platform->display, "WM_PROTOCOLS", False) &&
static_cast<Atom>(platform->xev.xclient.data.l[0]) == platform->wmDeleteWindow) {
Close();
}
if (platform->xev.type == FocusIn) {
Logger::Debug(std::format("Event'{}'", "FocusIn"));
if (!key_repeat)
XAutoRepeatOff(platform->display);
else
XAutoRepeatOn(platform->display);
XFlush(platform->display);
if (cursor_focused) {
cursor_focused = false;
SetCursorFocused(true);
}
if (platform->wm_hints) {
platform->wm_hints->flags &= ~XUrgencyHint;
XSetWMHints(platform->display, platform->window, platform->wm_hints);
XFree(platform->wm_hints);
}
if (!cursor_visible)
XDefineCursor(platform->display, platform->window, platform->invisible_cursor);
// Get the position of the renderable area relative to the rest of the window.
XGetWindowAttributes(platform->display, platform->window, &platform->windowAttributes);
processFocusIn();
focused = true;
}
if (platform->xev.type == UnmapNotify) {
if (cursor_focused) {
SetCursorFocused(false);
cursor_focused = true;
}
}
if (platform->xev.type == FocusOut) {
Logger::Debug(std::format("Event '{}'", "FocusOut"));
XAutoRepeatOn(platform->display);
XFlush(platform->display);
if (cursor_focused) {
SetCursorFocused(false);
cursor_focused = true;
}
if (!cursor_visible)
XUndefineCursor(platform->display, platform->window);
processFocusOut();
focused = false;
}
if (platform->xev.type == KeyRelease) {
Logger::Debug(std::format("Event '{}'", "KeyRelease"));
auto scancode = (X11Scancode) platform->xev.xkey.keycode;
auto key = GetKeyFromX11Scancode(scancode);
processKeyRelease(key);
}
if (platform->xev.type == KeyPress) {
Logger::Debug(std::format("Event '{}'", "KeyPress"));
auto scancode = (X11Scancode) platform->xev.xkey.keycode;
auto key = GetKeyFromX11Scancode(scancode);
processKeyPress(key);
}
if (platform->xev.type == ButtonRelease) {
// Mouse Wheel fires both the ButtonPress and ButtonRelease instantaneously.
// Therefore, we handle it as a specific MouseWheel event rather than a MouseButton event,
// and only call on ButtonPress, otherwise it will appear to duplicate the mouse wheel scroll.
if (platform->xev.xbutton.button != 4 && platform->xev.xbutton.button != 5) {
MouseButton button = GetMouseButtonFromXButton(platform->xev.xbutton.button);
Logger::Debug(std::format("Event '{}'", "ButtonRelease"));
processMouseRelease(button);
}
}
if (platform->xev.type == ButtonPress) {
// Mouse Wheel fires both the ButtonPress and ButtonRelease instantaneously.
// Therefore, we handle it as a specific MouseWheel event rather than a MouseButton event,
// and only call on ButtonPress, otherwise it will appear to duplicate the mouse wheel scroll.
if (platform->xev.xbutton.button == 4) {
processMouseWheel(-1);
} else if (platform->xev.xbutton.button == 5) {
processMouseWheel(1);
} else {
MouseButton button = GetMouseButtonFromXButton(platform->xev.xbutton.button);
Logger::Debug(std::format("Event: MouseButtonPress {}", button.Mnemonic));
processMousePress(button);
}
}
if (platform->xev.type == Expose) {
Logger::Debug(std::format("Event '{}'", "Expose"));
}
if (platform->xev.type == MotionNotify) {
if (toggling_cursor_focus) {
XWindowAttributes attrs;
XGetWindowAttributes(platform->display, platform->window, &attrs);
if (attrs.map_state == IsViewable) {
int result = XGrabPointer
(
platform->display, platform->window, True,
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
GrabModeAsync, GrabModeAsync, platform->window, None, CurrentTime
);
if (result == GrabSuccess) {
toggling_cursor_focus = false;
cursor_focused = true;
}
}
}
}
if (platform->xev.type == ConfigureNotify) {
if (this->width != platform->xev.xconfigurerequest.width ||
this->height != platform->xev.xconfigurerequest.height) {
Logger::Debug(std::format("Event '{}'", "ResizeRequest"));
this->width = platform->xev.xconfigurerequest.width;
this->height = platform->xev.xconfigurerequest.height;
auto eventData = WindowResizeRequestEvent();
eventData.Size = {platform->xev.xconfigurerequest.width, platform->xev.xconfigurerequest.height};
OnResizeRequest(eventData);
OnResizeRequestEvent(eventData);
}
//Window Moved.
if (platform->position.first != platform->xev.xconfigurerequest.x || platform->position.second != platform->xev.xconfigurerequest.y)
platform->position = { platform->xev.xconfigurerequest.x, platform->xev.xconfigurerequest.y };
}
if (platform->xev.type == VisibilityNotify) {
if (platform->xev.xvisibility.state == VisibilityFullyObscured)
platform->window_visible = false;
else if (platform->xev.xvisibility.state == VisibilityUnobscured ||
platform->xev.xvisibility.state == VisibilityPartiallyObscured)
platform->window_visible = true;
}
}
previousKeyboard = currentKeyboard;
previousMouse.Buttons = currentMouse.Buttons;
}
// Might make the window go off the screen on some window managers.
void RWindow::SetSize(int newWidth, int newHeight) {
if (!resizable)
return;
this->width = newWidth;
this->height = newHeight;
XResizeWindow(platform->display, platform->window, newWidth, newHeight);
XFlush(platform->display);
Logger::Info(std::format("Set size for '{}' to {} x {}", this->title, newWidth, newHeight));
}
std::pair<int, int> RWindow::GetAccurateCursorCoordinates() const {
Window root_return, child_return;
int root_x_ret, root_y_ret;
int win_x_ret, win_y_ret;
uint32_t mask_return;
// This seems to be relative to the top left corner of the renderable area.
bool mouseAvailable = XQueryPointer(platform->display, platform->window, &root_return, &child_return, &root_x_ret, &root_y_ret, &win_x_ret, &win_y_ret, &mask_return);
if (mouseAvailable) {
// TODO: normalize coordinates from platform->displaySpace to windowSpace
// TODO: fire mouse movement event
std::pair<int, int> m_coords = { win_x_ret, win_y_ret };
return m_coords;
}
return {};
}
bool RWindow::IsVisible() const {
return platform->window_visible;
}
// TODO: implement integer std::pair<int, int>/3 types
std::pair<int, int> RWindow::GetPosition() const {
return platform->position;
}
std::pair<int, int> RWindow::GetSize() const { return { this->width, this->height }; }
void RWindow::SetPosition(int x, int y) {
XMoveWindow(platform->display, platform->window, x, y);
platform->position = {x, y};
}
void RWindow::SetPosition(const std::pair<int, int>& pos) {
SetPosition(pos.first, pos.second);
}
void RWindow::Fullscreen() {
platform->size_before_fullscreen = GetSize();
if (!this->resizable) {
XSizeHints hints;
hints.flags = PMinSize | PMaxSize;
hints.min_width = 0;
hints.min_height = 0;
hints.max_width = 100000;
hints.max_height = 100000;
XSetWMNormalHints(platform->display, platform->window, &hints);
XFlush(platform->display);
}
Atom wm_state = XInternAtom(platform->display, "_NET_WM_STATE", False);
Atom wm_fullscreen = XInternAtom(platform->display, "_NET_WM_STATE_FULLSCREEN", False);
if (!wm_state || !wm_fullscreen) {
Logger::Error("We don't have the required atom for fullscreen graphics mode?");
return;
}
XEvent xev{};
xev.type = ClientMessage;
xev.xclient.window = platform->window;
xev.xclient.message_type = wm_state;
xev.xclient.format = 32;
xev.xclient.data.l[0] = 1;
xev.xclient.data.l[1] = wm_fullscreen;
xev.xclient.data.l[2] = 0;
xev.xclient.data.l[3] = 1;
xev.xclient.data.l[4] = 0;
XSendEvent(platform->display,
DefaultRootWindow(platform->display),
False,
SubstructureNotifyMask | SubstructureRedirectMask,
&xev);
XFlush(platform->display);
fullscreen_mode = true;
Logger::Debug(std::format("Fullscreened '{}'", this->title));
}
void RWindow::RestoreFromFullscreen() {
Atom wm_state = XInternAtom(platform->display, "_NET_WM_STATE", False);
Atom fullscreen = XInternAtom(platform->display, "_NET_WM_STATE_FULLSCREEN", False);
if (!wm_state || !fullscreen) {
Logger::Error("We don't have the required atom for fullscreen graphics mode?");
return;
}
XEvent xev{};
xev.type = ClientMessage;
xev.xclient.window = platform->window;
xev.xclient.message_type = wm_state;
xev.xclient.format = 32;
xev.xclient.data.l[0] = 0;
xev.xclient.data.l[1] = fullscreen;
xev.xclient.data.l[2] = 0;
xev.xclient.data.l[3] = 1;
xev.xclient.data.l[4] = 0;
XSendEvent(
platform->display,
DefaultRootWindow(platform->display),
False,
SubstructureNotifyMask | SubstructureRedirectMask,
&xev
);
if (!this->resizable) {
XSizeHints hints;
hints.flags = PMinSize | PMaxSize;
hints.min_width = hints.max_width = platform->size_before_fullscreen.first;
hints.min_height = hints.max_height = platform->size_before_fullscreen.second;
XSetWMNormalHints(platform->display, platform->window, &hints);
this->width = platform->size_before_fullscreen.first;
this->height = platform->size_before_fullscreen.second;
}
XFlush(platform->display);
fullscreen_mode = false;
Logger::Debug(std::format("Restored '{}' from Fullscreen", this->title));
}
void RWindow::SetCursorStyle(CursorStyle style) const {
auto x11_cursor_resolved_enum = (unsigned int) style.X11Cursor;
Cursor c = XCreateFontCursor(platform->display, x11_cursor_resolved_enum);
XDefineCursor(platform->display, platform->window, c);
}
void RWindow::SetTitle(const std::string& title) {
this->title = title;
XStoreName(platform->display, platform->window, title.c_str());
}