Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
240 lines
8.9 KiB
C++
240 lines
8.9 KiB
C++
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
#include "GHOST_SystemWeb.hh"
|
|
#include <algorithm>
|
|
|
|
#include "GHOST_TimerManager.hh"
|
|
#include "GHOST_WindowManager.hh"
|
|
#include "GHOST_WindowWeb.hh"
|
|
#include "GHOST_EventCursor.hh"
|
|
|
|
#include <cmath>
|
|
#include <emscripten.h>
|
|
|
|
bool GHOST_SystemWeb::bindCanvas(const char *selector, float pixel_ratio)
|
|
{
|
|
if (canvas_window_ || !selector || selector[0] != '#' || selector[1] == '\0' ||
|
|
!std::isfinite(pixel_ratio) || pixel_ratio <= 0.0f)
|
|
{
|
|
return false;
|
|
}
|
|
canvas_selector_ = selector;
|
|
pixel_ratio_ = pixel_ratio;
|
|
return true;
|
|
}
|
|
|
|
GHOST_IWindow *GHOST_SystemWeb::createWindow(const char *title,
|
|
int32_t /*left*/,
|
|
int32_t /*top*/,
|
|
uint32_t width,
|
|
uint32_t height,
|
|
GHOST_TWindowState state,
|
|
GHOST_GPUSettings gpu_settings,
|
|
bool exclusive,
|
|
bool is_dialog,
|
|
const GHOST_IWindow *parent_window)
|
|
{
|
|
/* One transferred canvas per module. Reject desktop window requests until
|
|
* the host supplies another canvas, rather than aliasing the live context. */
|
|
if (canvas_window_ || canvas_selector_.empty() || exclusive || is_dialog || parent_window ||
|
|
state != GHOST_kWindowStateNormal ||
|
|
gpu_settings.context_type != GHOST_kDrawingContextTypeWebGL)
|
|
{
|
|
return nullptr;
|
|
}
|
|
auto *window = new GHOST_WindowWeb(canvas_selector_.c_str(), width, height, pixel_ratio_);
|
|
if (!window->getValid() || !window_manager_->addWindow(window)) {
|
|
delete window;
|
|
return nullptr;
|
|
}
|
|
window->setTitle(title);
|
|
canvas_window_ = window;
|
|
return window;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWeb::disposeWindow(GHOST_IWindow *window)
|
|
{
|
|
const bool is_canvas = window == canvas_window_;
|
|
const GHOST_TSuccess result = GHOST_System::disposeWindow(window);
|
|
if (result && is_canvas) {
|
|
canvas_window_ = nullptr;
|
|
modifiers_.clear();
|
|
buttons_.clear();
|
|
cursor_valid_ = false;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool GHOST_SystemWeb::processEvents(bool /*wait_for_event*/)
|
|
{
|
|
/* The browser owns the event loop; blocking here would prevent new input. */
|
|
const bool timers_fired = timer_manager_->fireTimers(getMilliSeconds());
|
|
return timers_fired || event_manager_->getNumEvents() != 0;
|
|
}
|
|
|
|
uint64_t GHOST_SystemWeb::getMilliSeconds() const
|
|
{
|
|
return uint64_t(emscripten_get_now());
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWeb::getModifierKeys(GHOST_ModifierKeys &keys) const
|
|
{
|
|
keys = modifiers_;
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWeb::getButtons(GHOST_Buttons &buttons) const
|
|
{
|
|
buttons = buttons_;
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWeb::getCursorPosition(int32_t &x, int32_t &y) const
|
|
{
|
|
if (!cursor_valid_) return GHOST_kFailure;
|
|
x = cursor_x_;
|
|
y = cursor_y_;
|
|
/* Match Wayland's relative pointer: WM receives continuous coordinates,
|
|
* while cursor queries return the location within the native wrap region. */
|
|
if (canvas_window_ && canvas_window_->getCursorGrabModeIsWarp()) {
|
|
GHOST_Rect bounds;
|
|
if (!canvas_window_->getCursorGrabBounds(bounds)) canvas_window_->getClientBounds(bounds);
|
|
bounds.wrapPoint(x, y, 0, canvas_window_->getCursorGrabAxis());
|
|
}
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWeb::setCursorPosition(int32_t x, int32_t y)
|
|
{
|
|
/* Browsers cannot warp the OS pointer. During native grab the pointer is
|
|
* relative and WM owns its software cursor, so reposition that native cursor. */
|
|
if (!canvas_window_ || !canvas_window_->getCursorGrabModeIsWarp()) return GHOST_kFailure;
|
|
cursor_remainder_[0] = cursor_remainder_[1] = 0;
|
|
return pushBrowserEvent(std::make_unique<GHOST_EventCursor>(
|
|
getMilliSeconds(), GHOST_kEventCursorMove, canvas_window_, x, y, GHOST_TABLET_DATA_NONE));
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWeb::moveBrowserCursor(float x, float y, bool relative)
|
|
{
|
|
if (!canvas_window_ || !std::isfinite(x) || !std::isfinite(y)) return GHOST_kFailure;
|
|
if (relative) {
|
|
/* A final browser packet can arrive after WM confirmed/cancelled the tool. */
|
|
if (canvas_window_->getCursorGrabMode() == GHOST_kGrabDisable) return GHOST_kSuccess;
|
|
if (!cursor_valid_) return GHOST_kFailure;
|
|
cursor_remainder_[0] += x;
|
|
cursor_remainder_[1] += y;
|
|
const int dx = int(cursor_remainder_[0]), dy = int(cursor_remainder_[1]);
|
|
cursor_remainder_[0] -= dx;
|
|
cursor_remainder_[1] -= dy;
|
|
/* Keep native integer conversion defined even for a very long grab. */
|
|
if (std::abs(double(cursor_x_) + dx) > 1000000000.0 ||
|
|
std::abs(double(cursor_y_) + dy) > 1000000000.0) return GHOST_kFailure;
|
|
int32_t next_x = cursor_x_ + dx, next_y = cursor_y_ + dy;
|
|
if (canvas_window_->getCursorGrabMode() == GHOST_kGrabNormal) {
|
|
/* Normal grab confines, but does not wrap, the visible software pointer. */
|
|
GHOST_Rect bounds;
|
|
canvas_window_->getClientBounds(bounds);
|
|
next_x = std::clamp(next_x, bounds.l_, bounds.r_ - 1);
|
|
next_y = std::clamp(next_y, bounds.t_, bounds.b_ - 1);
|
|
}
|
|
return pushBrowserEvent(std::make_unique<GHOST_EventCursor>(
|
|
getMilliSeconds(), GHOST_kEventCursorMove, canvas_window_,
|
|
next_x, next_y, GHOST_TABLET_DATA_NONE));
|
|
}
|
|
cursor_remainder_[0] = cursor_remainder_[1] = 0;
|
|
return pushBrowserEvent(std::make_unique<GHOST_EventCursor>(
|
|
getMilliSeconds(), GHOST_kEventCursorMove, canvas_window_, int(x), int(y),
|
|
GHOST_TABLET_DATA_NONE));
|
|
}
|
|
|
|
void GHOST_SystemWeb::getMainDisplayDimensions(uint32_t &width, uint32_t &height) const
|
|
{
|
|
GHOST_Rect bounds;
|
|
if (canvas_window_) canvas_window_->getClientBounds(bounds);
|
|
width = canvas_window_ ? bounds.getWidth() : 0;
|
|
height = canvas_window_ ? bounds.getHeight() : 0;
|
|
}
|
|
|
|
void GHOST_SystemWeb::getAllDisplayDimensions(uint32_t &width, uint32_t &height) const
|
|
{
|
|
getMainDisplayDimensions(width, height);
|
|
}
|
|
|
|
GHOST_IWindow *GHOST_SystemWeb::getWindowUnderCursor(int32_t x, int32_t y)
|
|
{
|
|
if (!canvas_window_ || !cursor_valid_) return nullptr;
|
|
GHOST_Rect bounds;
|
|
canvas_window_->getClientBounds(bounds);
|
|
return bounds.isInside(x, y) ? canvas_window_ : nullptr;
|
|
}
|
|
|
|
void GHOST_SystemWeb::setBrowserModifiers(int modifiers, GHOST_TKey changed_key, bool down)
|
|
{
|
|
const auto update_pair = [&](int bit, GHOST_TModifierKey left, GHOST_TModifierKey right) {
|
|
if (!(modifiers & bit)) {
|
|
modifiers_.set(left, false);
|
|
modifiers_.set(right, false);
|
|
return;
|
|
}
|
|
const bool is_left = changed_key == GHOST_ModifierKeys::getModifierKeyCode(left);
|
|
const bool is_right = changed_key == GHOST_ModifierKeys::getModifierKeyCode(right);
|
|
if (is_left) modifiers_.set(left, down);
|
|
if (is_right) modifiers_.set(right, down);
|
|
// Browser pointer events carry aggregate state; retain known physical sides.
|
|
if (!modifiers_.get(left) && !modifiers_.get(right)) {
|
|
modifiers_.set(is_left && !down ? right : left, true);
|
|
}
|
|
};
|
|
update_pair(1, GHOST_kModifierKeyLeftShift, GHOST_kModifierKeyRightShift);
|
|
update_pair(2, GHOST_kModifierKeyLeftControl, GHOST_kModifierKeyRightControl);
|
|
update_pair(4, GHOST_kModifierKeyLeftAlt, GHOST_kModifierKeyRightAlt);
|
|
update_pair(8, GHOST_kModifierKeyLeftOS, GHOST_kModifierKeyRightOS);
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWeb::pushBrowserEvent(std::unique_ptr<const GHOST_IEvent> event)
|
|
{
|
|
if (!event || event->getWindow() != canvas_window_ || !canvas_window_) return GHOST_kFailure;
|
|
switch (event->getType()) {
|
|
case GHOST_kEventCursorMove: {
|
|
const auto *data = static_cast<const GHOST_TEventCursorData *>(event->getData());
|
|
cursor_x_ = data->x;
|
|
cursor_y_ = data->y;
|
|
cursor_valid_ = true;
|
|
break;
|
|
}
|
|
case GHOST_kEventTrackpad: {
|
|
const auto *data = static_cast<const GHOST_TEventTrackpadData *>(event->getData());
|
|
cursor_x_ = data->x;
|
|
cursor_y_ = data->y;
|
|
cursor_valid_ = true;
|
|
break;
|
|
}
|
|
case GHOST_kEventDraggingDropDone: {
|
|
const auto *data = static_cast<const GHOST_TEventDragnDropData *>(event->getData());
|
|
cursor_x_ = data->x;
|
|
cursor_y_ = data->y;
|
|
cursor_valid_ = true;
|
|
buttons_.clear();
|
|
window_manager_->setActiveWindow(canvas_window_);
|
|
break;
|
|
}
|
|
case GHOST_kEventButtonDown:
|
|
case GHOST_kEventButtonUp:
|
|
buttons_.set(static_cast<const GHOST_TEventButtonData *>(event->getData())->button,
|
|
event->getType() == GHOST_kEventButtonDown);
|
|
break;
|
|
case GHOST_kEventWindowActivate:
|
|
window_manager_->setActiveWindow(canvas_window_);
|
|
break;
|
|
case GHOST_kEventWindowDeactivate:
|
|
window_manager_->setWindowInactive(canvas_window_);
|
|
modifiers_.clear();
|
|
buttons_.clear();
|
|
cursor_valid_ = false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return pushEvent(std::move(event));
|
|
}
|