View > Lock view freezes panning, zooming and rotating from every input until it is turned off again. View > Full screen and F11 toggle full screen; the Graphics window gains setFullScreen and isFullScreen, backed by the xdg toplevel full screen state on Wayland while Android windows always report full screen.
493 lines
17 KiB
C++
493 lines
17 KiB
C++
#include "WaylandWindow.hpp"
|
|
|
|
#include "KeyMapping.hpp"
|
|
#include "WaylandConnection.hpp"
|
|
#include "WaylandSeat.hpp"
|
|
#include "Window.hpp"
|
|
#include "WindowInputDispatcher.hpp"
|
|
|
|
#include "Timing.hpp"
|
|
|
|
#include "fractional-scale-v1-client-protocol.h"
|
|
#include "viewporter-client-protocol.h"
|
|
#include "xdg-decoration-unstable-v1-client-protocol.h"
|
|
#include "xdg-shell-client-protocol.h"
|
|
|
|
#include <vulkan/vulkan.h>
|
|
#include <vulkan/vulkan_wayland.h>
|
|
#include <wayland-client.h>
|
|
|
|
#include <linux/input-event-codes.h>
|
|
#include <poll.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <stdexcept>
|
|
|
|
namespace tp {
|
|
|
|
namespace {
|
|
constexpr halnf kFractionalScaleDenominator = 120;
|
|
constexpr halnf kMinimumContentWidth = 320;
|
|
constexpr halnf kMinimumContentHeight = 200;
|
|
constexpr int kConfigureWaitMs = 1000;
|
|
}
|
|
|
|
struct WaylandWindow::Objects {
|
|
wl_surface* surface = nullptr;
|
|
xdg_surface* shellSurface = nullptr;
|
|
xdg_toplevel* toplevel = nullptr;
|
|
wp_viewport* viewport = nullptr;
|
|
wp_fractional_scale_v1* fractionalScale = nullptr;
|
|
zxdg_toplevel_decoration_v1* decoration = nullptr;
|
|
WindowConfigure pending;
|
|
wl_callback* frameCallback = nullptr;
|
|
};
|
|
|
|
struct WaylandListeners {
|
|
static WaylandWindow& window(void* data) { return *static_cast<WaylandWindow*>(data); }
|
|
|
|
static void surfaceConfigure(void* data, xdg_surface*, uint32_t serial) {
|
|
WaylandWindow& self = window(data);
|
|
WindowConfigure configure = self.mObjects->pending;
|
|
configure.serial = serial;
|
|
self.mCoalescer.configure(configure);
|
|
}
|
|
|
|
static void toplevelConfigure(void* data, xdg_toplevel*, int32_t width, int32_t height, wl_array* states) {
|
|
WindowConfigure configuration;
|
|
configuration.width = width;
|
|
configuration.height = height;
|
|
|
|
const auto* entries = static_cast<const uint32_t*>(states->data);
|
|
const size_t count = states->size / sizeof(uint32_t);
|
|
for (size_t index = 0; index < count; index++) {
|
|
switch (entries[index]) {
|
|
case XDG_TOPLEVEL_STATE_MAXIMIZED: configuration.maximized = true; break;
|
|
case XDG_TOPLEVEL_STATE_FULLSCREEN: configuration.fullscreen = true; break;
|
|
case XDG_TOPLEVEL_STATE_ACTIVATED: configuration.activated = true; break;
|
|
case XDG_TOPLEVEL_STATE_TILED_LEFT:
|
|
case XDG_TOPLEVEL_STATE_TILED_RIGHT:
|
|
case XDG_TOPLEVEL_STATE_TILED_TOP:
|
|
case XDG_TOPLEVEL_STATE_TILED_BOTTOM: configuration.tiled = true; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
window(data).mObjects->pending = configuration;
|
|
}
|
|
|
|
static void toplevelClose(void* data, xdg_toplevel*) { window(data).mCloseRequested = true; }
|
|
static void toplevelBounds(void*, xdg_toplevel*, int32_t, int32_t) {}
|
|
static void toplevelCapabilities(void*, xdg_toplevel*, wl_array*) {}
|
|
|
|
static void preferredScale(void* data, wp_fractional_scale_v1*, uint32_t scale) {
|
|
window(data).applyScale((halnf) scale / kFractionalScaleDenominator);
|
|
}
|
|
|
|
static void decorationConfigure(void*, zxdg_toplevel_decoration_v1*, uint32_t) {}
|
|
|
|
static void frameDone(void* data, wl_callback* callback, uint32_t) {
|
|
WaylandWindow& self = window(data);
|
|
wl_callback_destroy(callback);
|
|
if (self.mObjects->frameCallback == callback) self.mObjects->frameCallback = nullptr;
|
|
}
|
|
};
|
|
|
|
namespace {
|
|
const xdg_surface_listener kShellSurfaceListener = { WaylandListeners::surfaceConfigure };
|
|
|
|
const xdg_toplevel_listener kToplevelListener = {
|
|
WaylandListeners::toplevelConfigure,
|
|
WaylandListeners::toplevelClose,
|
|
WaylandListeners::toplevelBounds,
|
|
WaylandListeners::toplevelCapabilities,
|
|
};
|
|
|
|
const wp_fractional_scale_v1_listener kFractionalScaleListener = { WaylandListeners::preferredScale };
|
|
|
|
const zxdg_toplevel_decoration_v1_listener kDecorationListener = { WaylandListeners::decorationConfigure };
|
|
const wl_callback_listener kFrameListener = { WaylandListeners::frameDone };
|
|
|
|
uint32_t resizeEdgeCode(WindowEdge edge) {
|
|
switch (edge) {
|
|
case WindowEdge::Top: return XDG_TOPLEVEL_RESIZE_EDGE_TOP;
|
|
case WindowEdge::Bottom: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
|
|
case WindowEdge::Left: return XDG_TOPLEVEL_RESIZE_EDGE_LEFT;
|
|
case WindowEdge::Right: return XDG_TOPLEVEL_RESIZE_EDGE_RIGHT;
|
|
case WindowEdge::TopLeft: return XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT;
|
|
case WindowEdge::TopRight: return XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT;
|
|
case WindowEdge::BottomLeft: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT;
|
|
case WindowEdge::BottomRight: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
|
|
case WindowEdge::None: return XDG_TOPLEVEL_RESIZE_EDGE_NONE;
|
|
}
|
|
return XDG_TOPLEVEL_RESIZE_EDGE_NONE;
|
|
}
|
|
|
|
uint32_t scaledLength(halnf length, halnf scale) { return (uint32_t) std::lround(length * scale); }
|
|
}
|
|
|
|
WaylandWindow::WaylandWindow(Window& owner, Vec2F contentSize, const char* title) :
|
|
mOwner(owner),
|
|
mConnection(std::make_unique<WaylandConnection>()),
|
|
mObjects(std::make_unique<Objects>()) {
|
|
mFrame.setContentSize(contentSize);
|
|
mRestoredContentSize = contentSize;
|
|
|
|
WaylandConnection& connection = *mConnection;
|
|
Objects& objects = *mObjects;
|
|
|
|
objects.surface = wl_compositor_create_surface(connection.compositor());
|
|
objects.shellSurface = xdg_wm_base_get_xdg_surface(connection.windowManager(), objects.surface);
|
|
xdg_surface_add_listener(objects.shellSurface, &kShellSurfaceListener, this);
|
|
|
|
objects.toplevel = xdg_surface_get_toplevel(objects.shellSurface);
|
|
xdg_toplevel_add_listener(objects.toplevel, &kToplevelListener, this);
|
|
xdg_toplevel_set_title(objects.toplevel, title);
|
|
xdg_toplevel_set_app_id(objects.toplevel, "modules");
|
|
xdg_toplevel_set_min_size(objects.toplevel, (int32_t) kMinimumContentWidth, (int32_t) kMinimumContentHeight);
|
|
|
|
if (connection.decorationManager()) {
|
|
objects.decoration =
|
|
zxdg_decoration_manager_v1_get_toplevel_decoration(connection.decorationManager(), objects.toplevel);
|
|
zxdg_toplevel_decoration_v1_add_listener(objects.decoration, &kDecorationListener, this);
|
|
zxdg_toplevel_decoration_v1_set_mode(objects.decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
|
|
}
|
|
|
|
if (connection.viewporter())
|
|
objects.viewport = wp_viewporter_get_viewport(connection.viewporter(), objects.surface);
|
|
if (connection.fractionalScaleManager() && objects.viewport) {
|
|
objects.fractionalScale =
|
|
wp_fractional_scale_manager_v1_get_fractional_scale(connection.fractionalScaleManager(), objects.surface);
|
|
wp_fractional_scale_v1_add_listener(objects.fractionalScale, &kFractionalScaleListener, this);
|
|
}
|
|
|
|
mSeat = std::make_unique<WaylandSeat>(connection, *this);
|
|
|
|
updateFrameGeometry();
|
|
wl_surface_commit(objects.surface);
|
|
connection.flush();
|
|
}
|
|
|
|
WaylandWindow::~WaylandWindow() {
|
|
mSeat.reset();
|
|
|
|
Objects& objects = *mObjects;
|
|
if (objects.frameCallback) wl_callback_destroy(objects.frameCallback);
|
|
if (objects.decoration) zxdg_toplevel_decoration_v1_destroy(objects.decoration);
|
|
if (objects.fractionalScale) wp_fractional_scale_v1_destroy(objects.fractionalScale);
|
|
if (objects.viewport) wp_viewport_destroy(objects.viewport);
|
|
if (objects.toplevel) xdg_toplevel_destroy(objects.toplevel);
|
|
if (objects.shellSurface) xdg_surface_destroy(objects.shellSurface);
|
|
if (objects.surface) wl_surface_destroy(objects.surface);
|
|
mConnection->flush();
|
|
}
|
|
|
|
std::vector<const char*> WaylandWindow::requiredInstanceExtensions() const {
|
|
return { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME };
|
|
}
|
|
|
|
VkSurfaceKHR WaylandWindow::createSurface(VkInstance instance) const {
|
|
VkWaylandSurfaceCreateInfoKHR createInfo{
|
|
.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR,
|
|
.display = mConnection->display(),
|
|
.surface = mObjects->surface,
|
|
};
|
|
|
|
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
|
if (vkCreateWaylandSurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) return VK_NULL_HANDLE;
|
|
return surface;
|
|
}
|
|
|
|
VkExtent2D WaylandWindow::framebufferExtent() const {
|
|
const Vec2F surface = mFrame.surfaceSize();
|
|
return { scaledLength(surface.x, mScale), scaledLength(surface.y, mScale) };
|
|
}
|
|
|
|
void WaylandWindow::onTranslucencyResolved(bool translucent) {
|
|
mFrame.setTranslucent(translucent);
|
|
mGeometryDirty = true;
|
|
}
|
|
|
|
halnf WaylandWindow::refreshIntervalMs() const { return 1.0e6f / (halnf) mConnection->refreshMillihertz(); }
|
|
|
|
void WaylandWindow::dispatchEvents(halnf timeoutMs) {
|
|
const bool waitsForever = timeoutMs < 0;
|
|
const time_ns deadline = get_time_ns() + (time_ns) (std::max(timeoutMs, 0.0f) * 1.0e6f);
|
|
|
|
while (true) {
|
|
const halnf remainingMs = waitsForever ? -1.0f : (halnf) ((double) (deadline - get_time_ns()) / 1.0e6);
|
|
if (pollOnce(waitsForever ? -1 : std::max(0, (int) std::ceil(remainingMs)))) return;
|
|
if (!waitsForever && get_time_ns() >= deadline) return;
|
|
}
|
|
}
|
|
|
|
bool WaylandWindow::pollOnce(int timeoutMs) {
|
|
wl_display* display = mConnection->display();
|
|
|
|
mConnection->flush();
|
|
if (wl_display_dispatch_pending(display) > 0) return true;
|
|
|
|
while (wl_display_prepare_read(display) != 0) {
|
|
if (wl_display_dispatch_pending(display) > 0) return true;
|
|
}
|
|
mConnection->flush();
|
|
|
|
const int repeatTimer = mSeat->repeatTimerDescriptor();
|
|
pollfd descriptors[2] = {
|
|
{ wl_display_get_fd(display), POLLIN, 0 },
|
|
{ repeatTimer, POLLIN, 0 },
|
|
};
|
|
const nfds_t descriptorCount = repeatTimer >= 0 ? 2 : 1;
|
|
|
|
const int ready = poll(descriptors, descriptorCount, timeoutMs);
|
|
if (ready > 0 && (descriptors[0].revents & POLLIN)) {
|
|
if (wl_display_read_events(display) < 0) throw std::runtime_error("lost the Wayland connection");
|
|
} else {
|
|
wl_display_cancel_read(display);
|
|
}
|
|
|
|
bool handled = wl_display_dispatch_pending(display) > 0;
|
|
if (ready > 0 && descriptorCount == 2 && (descriptors[1].revents & POLLIN)) {
|
|
mSeat->onRepeatTimer();
|
|
handled = true;
|
|
}
|
|
|
|
mConnection->flush();
|
|
return handled;
|
|
}
|
|
|
|
void WaylandWindow::waitUntilConfigured() {
|
|
while (!mConfigured) {
|
|
if (!mConnection->dispatch(kConfigureWaitMs)) break;
|
|
flushPendingInput();
|
|
}
|
|
}
|
|
|
|
void WaylandWindow::applyConfiguration(const WindowConfigure& configuration) {
|
|
mMaximized = configuration.maximized || configuration.fullscreen;
|
|
mFullScreen = configuration.fullscreen;
|
|
mActivated = configuration.activated;
|
|
|
|
const bool tiled = mMaximized || configuration.tiled;
|
|
if (tiled != mFrame.isTiled()) {
|
|
mFrame.setTiled(tiled);
|
|
mGeometryDirty = true;
|
|
}
|
|
|
|
Vec2F contentSize = mFrame.contentSize();
|
|
if (configuration.width > 0 && configuration.height > 0) {
|
|
contentSize = { (halnf) configuration.width, (halnf) configuration.height };
|
|
} else if (!tiled) {
|
|
contentSize = mRestoredContentSize;
|
|
}
|
|
|
|
contentSize.x = std::max(contentSize.x, kMinimumContentWidth);
|
|
contentSize.y = std::max(contentSize.y, kMinimumContentHeight);
|
|
if (!tiled) mRestoredContentSize = contentSize;
|
|
|
|
if (contentSize != mFrame.contentSize()) {
|
|
mFrame.setContentSize(contentSize);
|
|
mGeometryDirty = true;
|
|
}
|
|
}
|
|
|
|
void WaylandWindow::applyScale(halnf scale) {
|
|
if (scale <= 0 || scale == mScale) return;
|
|
|
|
mScale = scale;
|
|
mGeometryDirty = true;
|
|
if (mConfigured) notifyOwnerResized();
|
|
}
|
|
|
|
void WaylandWindow::notifyOwnerResized() { mOwner.notifyResized(); }
|
|
|
|
namespace {
|
|
class MutedInput : public WindowInputObserver {};
|
|
}
|
|
|
|
WindowInputObserver& WaylandWindow::platformInput() {
|
|
static MutedInput muted;
|
|
if (!mAcceptsInput) return muted;
|
|
return mCoalescer.recorder();
|
|
}
|
|
|
|
void WaylandWindow::flushPendingInput() {
|
|
if (mCoalescer.hasPendingConfigure()) {
|
|
const WindowConfigure configure = mCoalescer.takeConfigure();
|
|
applyConfiguration(configure);
|
|
xdg_surface_ack_configure(mObjects->shellSurface, configure.serial);
|
|
mConfigured = true;
|
|
notifyOwnerResized();
|
|
}
|
|
|
|
mCoalescer.deliver(mOwner.dispatcher());
|
|
}
|
|
|
|
void WaylandWindow::setAcceptsInput(bool accepts) {
|
|
mAcceptsInput = accepts;
|
|
mGeometryDirty = true;
|
|
updateFrameGeometry();
|
|
wl_surface_commit(mObjects->surface);
|
|
mConnection->flush();
|
|
}
|
|
|
|
void WaylandWindow::commitFrameGeometry() {
|
|
if (mGeometryDirty) updateFrameGeometry();
|
|
requestFrameCallback();
|
|
}
|
|
|
|
void WaylandWindow::requestFrameCallback() {
|
|
if (mObjects->frameCallback) return;
|
|
mObjects->frameCallback = wl_surface_frame(mObjects->surface);
|
|
wl_callback_add_listener(mObjects->frameCallback, &kFrameListener, this);
|
|
}
|
|
|
|
bool WaylandWindow::isWaitingForFrame() const { return mObjects->frameCallback != nullptr; }
|
|
|
|
void WaylandWindow::updateFrameGeometry() {
|
|
Objects& objects = *mObjects;
|
|
const Vec2F surface = mFrame.surfaceSize();
|
|
const RectF content = mFrame.contentRect();
|
|
const RectF input = mFrame.inputRect();
|
|
|
|
xdg_surface_set_window_geometry(
|
|
objects.shellSurface, (int32_t) content.x, (int32_t) content.y, (int32_t) content.z, (int32_t) content.w
|
|
);
|
|
|
|
wl_region* inputRegion = wl_compositor_create_region(mConnection->compositor());
|
|
if (mAcceptsInput) {
|
|
wl_region_add(inputRegion, (int32_t) input.x, (int32_t) input.y, (int32_t) input.z, (int32_t) input.w);
|
|
}
|
|
wl_surface_set_input_region(objects.surface, inputRegion);
|
|
wl_region_destroy(inputRegion);
|
|
|
|
if (!mFrame.hasShadow()) {
|
|
wl_region* opaqueRegion = wl_compositor_create_region(mConnection->compositor());
|
|
wl_region_add(opaqueRegion, 0, 0, (int32_t) surface.x, (int32_t) surface.y);
|
|
wl_surface_set_opaque_region(objects.surface, opaqueRegion);
|
|
wl_region_destroy(opaqueRegion);
|
|
} else {
|
|
wl_surface_set_opaque_region(objects.surface, nullptr);
|
|
}
|
|
|
|
if (objects.viewport) {
|
|
wp_viewport_set_destination(objects.viewport, (int32_t) surface.x, (int32_t) surface.y);
|
|
} else {
|
|
wl_surface_set_buffer_scale(objects.surface, (int32_t) std::lround(mScale));
|
|
}
|
|
|
|
mGeometryDirty = false;
|
|
}
|
|
|
|
Vec2F WaylandWindow::toContent(Vec2F surfacePoint) const {
|
|
const RectF content = mFrame.contentRect();
|
|
return { surfacePoint.x - content.x, surfacePoint.y - content.y };
|
|
}
|
|
|
|
void WaylandWindow::onPointerSurfaceMove(Vec2F surfacePoint) {
|
|
const bool contentHoldsPointer = mHeldButtons > 0;
|
|
const WindowEdge edge = contentHoldsPointer ? WindowEdge::None : mFrame.edgeAt(surfacePoint);
|
|
mHoveredEdge = edge;
|
|
|
|
if (edge != WindowEdge::None) {
|
|
const halnf outside = WindowInputDispatcher::kPointerOutsideCoordinate;
|
|
mSeat->setCursor(cursorForEdge(edge));
|
|
platformInput().onPointerMoved({ outside, outside });
|
|
return;
|
|
}
|
|
|
|
mSeat->setCursor(mClientCursor);
|
|
platformInput().onPointerMoved(toContent(surfacePoint));
|
|
}
|
|
|
|
void WaylandWindow::onPointerSurfaceLeft() {
|
|
mHeldButtons = 0;
|
|
mHoveredEdge = WindowEdge::None;
|
|
platformInput().onPointerLeft();
|
|
}
|
|
|
|
void WaylandWindow::onPointerSurfaceButton(uint32_t button, bool pressed, uint32_t serial) {
|
|
if (pressed && mHoveredEdge != WindowEdge::None) {
|
|
if (button == BTN_LEFT)
|
|
xdg_toplevel_resize(mObjects->toplevel, mConnection->seat(), serial, resizeEdgeCode(mHoveredEdge));
|
|
return;
|
|
}
|
|
|
|
const std::optional<InputID> id = inputFromPointerButton(button);
|
|
if (!id) return;
|
|
|
|
if (pressed) mHeldButtons++;
|
|
else if (mHeldButtons > 0) mHeldButtons--;
|
|
platformInput().onPointerButton(*id, pressed);
|
|
}
|
|
|
|
void WaylandWindow::minimize() { xdg_toplevel_set_minimized(mObjects->toplevel); }
|
|
|
|
void WaylandWindow::toggleMaximize() {
|
|
if (mMaximized) xdg_toplevel_unset_maximized(mObjects->toplevel);
|
|
else xdg_toplevel_set_maximized(mObjects->toplevel);
|
|
}
|
|
|
|
void WaylandWindow::setFullScreen(bool fullScreen) {
|
|
if (fullScreen) xdg_toplevel_set_fullscreen(mObjects->toplevel, nullptr);
|
|
else xdg_toplevel_unset_fullscreen(mObjects->toplevel);
|
|
}
|
|
|
|
void WaylandWindow::beginMove() {
|
|
xdg_toplevel_move(mObjects->toplevel, mConnection->seat(), mSeat->lastInputSerial());
|
|
}
|
|
|
|
void WaylandWindow::beginResize(WindowEdge edge) {
|
|
if (edge == WindowEdge::None) return;
|
|
xdg_toplevel_resize(mObjects->toplevel, mConnection->seat(), mSeat->lastInputSerial(), resizeEdgeCode(edge));
|
|
}
|
|
|
|
void WaylandWindow::showWindowMenu(Vec2F contentPosition) {
|
|
const RectF content = mFrame.contentRect();
|
|
xdg_toplevel_show_window_menu(
|
|
mObjects->toplevel,
|
|
mConnection->seat(),
|
|
mSeat->lastInputSerial(),
|
|
(int32_t) (contentPosition.x + content.x),
|
|
(int32_t) (contentPosition.y + content.y)
|
|
);
|
|
}
|
|
|
|
void WaylandWindow::setCursor(CursorShape shape) {
|
|
mClientCursor = shape;
|
|
if (mHoveredEdge == WindowEdge::None) mSeat->setCursor(shape);
|
|
}
|
|
|
|
void WaylandWindow::setClipboardText(const std::string& text) { mSeat->setClipboardText(text); }
|
|
|
|
std::string WaylandWindow::clipboardText() { return mSeat->clipboardText(); }
|
|
|
|
void WaylandWindow::setTextInputEnabled(bool enabled) { mSeat->setTextInputEnabled(enabled); }
|
|
|
|
void WaylandWindow::setTextInputRect(const RectF& contentRect) {
|
|
const RectF content = mFrame.contentRect();
|
|
mSeat->setTextInputRect({ contentRect.x + content.x, contentRect.y + content.y, contentRect.z, contentRect.w });
|
|
}
|
|
|
|
PlatformInfo WaylandWindow::platformInfo() const {
|
|
PlatformInfo info;
|
|
info.system = OperatingSystem::Linux;
|
|
info.formFactor = FormFactor::Desktop;
|
|
info.contentScale = mScale;
|
|
info.screenSize = mFrame.contentSize();
|
|
info.hasMouse = mSeat->hasPointer();
|
|
info.hasTouch = mSeat->hasTouch();
|
|
info.hasPen = mSeat->hasTablet();
|
|
info.hasPhysicalKeyboard = mSeat->hasKeyboard();
|
|
info.hasWindowControls = true;
|
|
info.canMoveWindow = true;
|
|
info.canResizeWindow = true;
|
|
info.hasHoverPointer = info.hasMouse || info.hasPen;
|
|
return info;
|
|
}
|
|
|
|
std::unique_ptr<PlatformWindow> createPlatformWindow(Window& owner, Vec2F contentSize, const char* title) {
|
|
return std::make_unique<WaylandWindow>(owner, contentSize, title);
|
|
}
|
|
}
|