ultralight-ux / Ultralight

Lightweight, high-performance HTML renderer for game and app developers.
https://ultralig.ht
4.67k stars 195 forks source link

Add support for custom window drag handles to AppCore #299

Open adamjs opened 4 years ago

adamjs commented 4 years ago

Some users may want to draw their own titlebar and close/minimize/maximize buttons (similar to Spotify and Discord).

To accomplish this, we need to support borderless windows + custom window drag handles (basically, redefine a section of window coordinates as the drag handle for the window).

WinAPI supports this via the WM_NCHITTEST message (see https://stackoverflow.com/questions/35522488/moving-frameless-window-by-dragging-it-from-a-portion-of-client-area)

joepriit commented 3 years ago

This is how you can currently do it, in case anyone wants to rush ahead:

<div onmousedown="softwareDragMove(true)" onmouseup="softwareDragMove(false)">
    Drag me!
</div>

void CMyApp::OnDragMove(const JSObject& obj, const JSArgs& args)
{
    dragging = args[0];
}

void CMyApp::OnDOMReady(View* caller, uint64_t frame_id, bool is_main_frame, const String& url)
{
    Ref<JSContext> locked_context = caller->LockJSContext();
    SetJSContext(locked_context.get());

    JSObject global = JSGlobalObject();

    global["softwareDragMove"] = BindJSCallback(&CMyApp::OnDragMove);
}

void CMyApp::OnUpdate()
{
    if (dragging) {
        ReleaseCapture();
        SendMessageA(hwnd_, WM_SYSCOMMAND, 0xf012, 0);
    }

    // JS onmouseup won't be fired while dragging the window for some reason
    // ... so we check it one more time, manually and disable dragging
    if (!GetAsyncKeyState(VK_LBUTTON & 0x8000)) {
        dragging = false;
    }
}
jonatino commented 3 years ago

The solution above had issues with my app (hover/mouse up events not firing when dragging finished until u clicked inside ultralight window) so I came up with another solution.

POINT mouseStartPoint;
RECT windowStartPoint;

bool isEmpty(RECT rect) {
    return rect.left == 0 && rect.bottom == 0 && rect.right == 0 && rect.top == 0;
}

void PvPHero::OnUpdate() {
    if (!isEmpty(windowStartPoint)) {
        if (!GetAsyncKeyState(VK_LBUTTON)) {
            windowStartPoint = {};
        } else {
            POINT pt;
            GetCursorPos(&pt);
            SetWindowPos((HWND) window->native_handle(), 0, windowStartPoint.left + pt.x - mouseStartPoint.x,
                         windowStartPoint.top + pt.y - mouseStartPoint.y,
                         0, 0,
                         SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE);
        }
    }
}

void PvPHero::StartDrag(const JSObject &obj, const JSArgs &args) {
    GetWindowRect((HWND) window->native_handle(), &windowStartPoint);
    GetCursorPos(&mouseStartPoint);
}