Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement draggable regions #200

Closed
nicholasdgoodman opened this issue May 22, 2020 · 14 comments
Closed

Implement draggable regions #200

nicholasdgoodman opened this issue May 22, 2020 · 14 comments
Assignees
Labels
feature request feature request

Comments

@nicholasdgoodman
Copy link

Draggable regions are a feature implemented in Chrome apps (formerly) and are also available in Electron.

Elements with the style:

--webkit-app-region: drag;

behave as a non-client area of the window and allow the window to be dragged (like a titlebar).

@david-risney david-risney added the feature request feature request label May 22, 2020
@david-risney
Copy link
Contributor

Thanks for trying out WebView2!

In the case of Chrome apps & Electron, the app framework owns the top level app window and can drag it along with the app region. To support this with WebView2, I suppose you would need a callback to the host app to tell it to move the parent window?

@nicholasdgoodman
Copy link
Author

That is a fantastic question on the "mechanics of how".

There is a reference implementation I am looking through for CefSharp that does just this:
https://github.com/qwqcode/CefSharpDraggableRegion

This weekend (if I am allowed 😄) I will look into reverse engineering how that was done. Otherwise, I'll do some hook injection and see what can would be the "minimal" eventing to hook something like this up.

Cross-process HWND hierarchies is an interesting problem, indeed!

@nicholasdgoodman
Copy link
Author

Turns out it is fairly easy for implementors to fill in this behavior with a registered host object and an injected script. I suppose that raises the question of whether or not the WebView2 control needs to have this behavior built-in, but I would still think its a "nice to have".

For those who might stumble across this issue while searching for something similar - here's the approach to implement it yourself:

Host Object (C#)

[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class EventForwarder
{
    public const int WM_NCLBUTTONDOWN = 0xA1;
    public const int HT_CAPTION = 0x2;

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
    [DllImport("user32.dll")]
    public static extern bool ReleaseCapture();

    readonly IntPtr target;

    public EventForwarder(IntPtr target)
    {
        this.target = target;
    }

    public void MouseDownDrag()
    {
        ReleaseCapture();
        SendMessage(target, WM_NCLBUTTONDOWN, HT_CAPTION, 0);
    }
}

Host Hookup (C#)

private void WebView_CoreWebView2Ready(object sender, EventArgs e)
{
    var eventForwarder = new EventForwarder(this.Handle);

    webView.CoreWebView2.AddHostObjectToScript("eventForwarder", eventForwarder);
    webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(File.ReadAllText("preload.js"));

}

Preload (js)

window.addEventListener('DOMContentLoaded', () => {
    document.body.addEventListener('mousedown', evt => {
        const { target } = evt;
        const appRegion = getComputedStyle(target)['-webkit-app-region'];

        if (appRegion === 'drag') {
            chrome.webview.hostObjects.sync.eventForwarder.MouseDownDrag();
            evt.preventDefault();
            evt.stopPropagation();
        }
    });
});

Maybe I'll put this up in a demo repo somewhere 😄 ...

drag-regions

@sdotter
Copy link

sdotter commented Jul 14, 2020

Turns out it is fairly easy for implementors to fill in this behavior with a registered host object and an injected script. I suppose that raises the question of whether or not the WebView2 control needs to have this behavior built-in, but I would still think its a "nice to have".

For those who might stumble across this issue while searching for something similar - here's the approach to implement it yourself:

Host Object (C#)

[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class EventForwarder
{
    public const int WM_NCLBUTTONDOWN = 0xA1;
    public const int HT_CAPTION = 0x2;

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
    [DllImport("user32.dll")]
    public static extern bool ReleaseCapture();

    readonly IntPtr target;

    public EventForwarder(IntPtr target)
    {
        this.target = target;
    }

    public void MouseDownDrag()
    {
        ReleaseCapture();
        SendMessage(target, WM_NCLBUTTONDOWN, HT_CAPTION, 0);
    }
}

Host Hookup (C#)

private void WebView_CoreWebView2Ready(object sender, EventArgs e)
{
    var eventForwarder = new EventForwarder(this.Handle);

    webView.CoreWebView2.AddHostObjectToScript("eventForwarder", eventForwarder);
    webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(File.ReadAllText("preload.js"));

}

Preload (js)

window.addEventListener('DOMContentLoaded', () => {
    document.body.addEventListener('mousedown', evt => {
        const { target } = evt;
        const appRegion = getComputedStyle(target)['-webkit-app-region'];

        if (appRegion === 'drag') {
            chrome.webview.hostObjects.sync.eventForwarder.MouseDownDrag();
            evt.preventDefault();
            evt.stopPropagation();
        }
    });
});

Maybe I'll put this up in a demo repo somewhere 😄 ...

drag-regions

Thanks Nicholas!!

@JoshuaWeber JoshuaWeber self-assigned this Aug 13, 2020
@JoshuaWeber
Copy link

Thanks for this issue and especially for the solution, @nicholasdgoodman!

I'm going to close out this issue. But we are still keeping track of this and your suggestion of it being "nice to have". We have been looking into some design changes that would possibly incorporate this into WebView2. But don't have any concrete proposal to share out yet.

@SamukaDEV
Copy link

Turns out it is fairly easy for implementors to fill in this behavior with a registered host object and an injected script. I suppose that raises the question of whether or not the WebView2 control needs to have this behavior built-in, but I would still think its a "nice to have".

For those who might stumble across this issue while searching for something similar - here's the approach to implement it yourself:

Host Object (C#)

[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class EventForwarder
{
    public const int WM_NCLBUTTONDOWN = 0xA1;
    public const int HT_CAPTION = 0x2;

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
    [DllImport("user32.dll")]
    public static extern bool ReleaseCapture();

    readonly IntPtr target;

    public EventForwarder(IntPtr target)
    {
        this.target = target;
    }

    public void MouseDownDrag()
    {
        ReleaseCapture();
        SendMessage(target, WM_NCLBUTTONDOWN, HT_CAPTION, 0);
    }
}

Host Hookup (C#)

private void WebView_CoreWebView2Ready(object sender, EventArgs e)
{
    var eventForwarder = new EventForwarder(this.Handle);

    webView.CoreWebView2.AddHostObjectToScript("eventForwarder", eventForwarder);
    webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(File.ReadAllText("preload.js"));

}

Preload (js)

window.addEventListener('DOMContentLoaded', () => {
    document.body.addEventListener('mousedown', evt => {
        const { target } = evt;
        const appRegion = getComputedStyle(target)['-webkit-app-region'];

        if (appRegion === 'drag') {
            chrome.webview.hostObjects.sync.eventForwarder.MouseDownDrag();
            evt.preventDefault();
            evt.stopPropagation();
        }
    });
});

Maybe I'll put this up in a demo repo somewhere 😄 ...

drag-regions

Very interesting, very cool solution, but what if the window is dragged to the edge of the screen? will the maximize option appear as in the native function?
if so then this could be a very good solution, otherwise this would be another reason to have a native solution, where it would certainly come built in and with the best possible performance!

I still couldn't test this proposed solution, but I thought it was really cool!

@ukandrewc
Copy link

Very interesting, very cool solution, but what if the window is dragged to the edge of the screen?

Easy enough to add edge detection and show another form as preview frame.

@Coder666
Copy link

I've implemented the suggested workaround in C++ using an ATL IDispatchImpl, moving the window works as described.

Any suggestion on how I might raise the window to the foreground when it is clicked?

I have tried:

SetWindowPos( ..., HWND_TOP, ... , SWP_NOMOVE| SWP_NOSIZE)
SetForegroundWindow(...)
BringWindowToTop(...)

Not really acceptable to make the window WM_EX_TOPMOST

This has turned out to be a bit of a time sink, perhaps enabling WM_NCHITTEST pass through can be reconsidered?

Any suggestions welcome.

@Coder666
Copy link

Intercepting the WM_NCLBUTTONDOWN in the window message loop and then calling SwitchToThisWindow from that thread seems to raise the window topmost:

SwitchToThisWindow(hWnd, FALSE);

However, MSDN hints that it may be removed:

"[This function is not intended for general use. It may be altered or unavailable in subsequent versions of Windows.] "

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-switchtothiswindow

Are there any safe alternatives to this?

@radekvit
Copy link

radekvit commented Sep 9, 2021

I've implemented the suggested workaround in C++ using an ATL IDispatchImpl, moving the window works as described.

Any suggestion on how I might raise the window to the foreground when it is clicked?

I have tried:

SetWindowPos( ..., HWND_TOP, ... , SWP_NOMOVE| SWP_NOSIZE)
SetForegroundWindow(...)
BringWindowToTop(...)

Not really acceptable to make the window WM_EX_TOPMOST

This has turned out to be a bit of a time sink, perhaps enabling WM_NCHITTEST pass through can be reconsidered?

Any suggestions welcome.

@Coder666 Would you please consider sharing your C++ solution here?

@Coder666
Copy link

Coder666 commented Sep 9, 2021

I've implemented the suggested workaround in C++ using an ATL IDispatchImpl, moving the window works as described.
Any suggestion on how I might raise the window to the foreground when it is clicked?
I have tried:
SetWindowPos( ..., HWND_TOP, ... , SWP_NOMOVE| SWP_NOSIZE)
SetForegroundWindow(...)
BringWindowToTop(...)
Not really acceptable to make the window WM_EX_TOPMOST
This has turned out to be a bit of a time sink, perhaps enabling WM_NCHITTEST pass through can be reconsidered?
Any suggestions welcome.

@Coder666 Would you please consider sharing your C++ solution here?

@radekvit

More than happy to discuss contract work to accomplish this, please get in touch via the website if you are interested.

@pushkin-
Copy link

@nicholasdgoodman Thanks for the workaround. However, I'm having trouble getting it to work. I'm adding the script to a window that I'm opening (intercepting via the NewWindowRequested event), but it seems like the preload script doesn't even execute since the top-level console.log statement don't get hit.

I'm using WPF.

I'm doing something roughly like this:

public static async void CoreWebView2_NewWindowRequested(object sender, CoreWebView2NewWindowRequestedEventArgs e) {
     var obj = e.GetDeferral();
     var win = new Window();
     win.Show();
     var w = new WebView2();
     win.Content = w;
     await w.EnsureCoreWebView2Async(MainWindow.Webview.CoreWebView2.Environment);
     e.NewWindow = w.CoreWebView2;
     win.WindowStyle = WindowStyle.None;
     var handle = new WindowInteropHelper(win).Handle;
     var eventFor = new EventForwarder(handle);
     await webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(File.ReadAllText("preload.js"));
     webview.CoreWebView2.AddHostObjectToScript("eventForwarder", eventFor);
     obj.Complete();
}

Is there something obvious that I'm doing wrong? I get the same issue when I pass in the script contents directly into AddScriptToExecuteOnDocumentCreatedAsync

@leaanthony
Copy link

A side-effect to this approach is that click and doubleclick events on the element aren't fired because (it would seem that) ReleaseCapture prevents them from being fired. This can be shown by adding those event handlers that console.log. Then add a 100ms delay before calling ReleaseCapture and see that they are fired. Anyone had this issue?

@cn-9826191865
Copy link

using this method, in version 109, repeatedly refreshing the page and dragging the form crashes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request feature request
Projects
None yet
Development

No branches or pull requests