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

add initial wxpython window support #133

Merged
merged 5 commits into from
Mar 26, 2021
Merged

add initial wxpython window support #133

merged 5 commits into from
Mar 26, 2021

Conversation

Correct-Syntax
Copy link
Contributor

Obviously this is not fully implemented, but It's a start.

I had some questions about the usage of the qt timer and how that should be translated into wxpython. I am also having a hard time figuring out how get a relevant value for get_display_id. Not sure if other methods are needed as well.

@Korijn
Copy link
Collaborator

Korijn commented Feb 23, 2021

Disclaimer: I'm not a wxPython expert, I just searched for some code and documentation that may be of use to you. Here's what I found:

timer

The timer in the Qt backend is a feature that allows users to limit the FPS independently from the Qt window's redraw rate. I don't think this is technically required to support wxPython but I can see the value of the feature: if you leave the FPS uncapped, your CPU will constantly be pushing 100% utilization to render as many frames as it possibly can. :)

Looks like there is a similar construct in wxPython that you could use: https://docs.wxpython.org/wx.Timer.html

get_display_id

This is used only on Linux, and currently we (try to) support X11 and Wayland as window managers. Wgpu requires us to pass a display ID in order to make it work.

Maybe you can use the Display class: https://docs.wxpython.org/wx.Display.html
I think the display index might be the value you are looking for. You could give that a try.

Also, I found this in the wxPython Phoenix changelog for version 2.6.0.1: Added wx.GetXDisplay that returns a raw swigified pointer for the X11 Display, or None for the non-X11 platforms. but that function doesn't appear to be a part of the library anymore. Actually that function appears to be commented out in their python wrapper generator scripts: https://github.com/wxWidgets/Phoenix/blob/master/etg/gdicmn.py#L395

Maybe you can open an issue and see if the wxPython maintainers can point you in the right direction?

example

Would you mind also adding a small examples/triangle_wx.py module to showcase your GUI backend? You can refer to examples/triangle_qt.py to see how minimal it can be.

@almarklein
Copy link
Collaborator

Some extra notes:

You can omit the timer in the first attempt, and add fps limiting later, or in a second PR.

It's not actually necessary to implement get_display_id because the base class has an implementation to obtain this value (see base.py).

def close() is needed to be able to programmatically close an application. If close means hide, that's fine. The is_closed method is used in the example to determine if the application should stop running. It should return True when the user clicks the cross, or when close() has been called.

@Correct-Syntax
Copy link
Contributor Author

@Korijn, thanks for the notes.

I don't think this is technically required to support wxPython but I can see the value of the feature: if you leave the FPS uncapped, your CPU will constantly be pushing 100% utilization to render as many frames as it possibly can. :)

Yes, this is why I was wondering if it would be necessary to add the wx.Timer or not. :)

Maybe you can use the Display class: https://docs.wxpython.org/wx.Display.html
I think the display index might be the value you are looking for. You could give that a try.

I did take a look into that, but I wasn't sure if that would be the correct id. The QT function seemed to be getting the display id, so the terminology, etc kinda made me wonder if it was the right one in wxpython.

Maybe you can open an issue and see if the wxPython maintainers can point you in the right direction?

I did do this, but I suspect your probably correct about the id.

Would you mind also adding a small examples/triangle_wx.py module to showcase your GUI backend? You can refer to examples/triangle_qt.py to see how minimal it can be.

I think I could do that. Just follow https://github.com/pygfx/wgpu-py#developers to build from source, correct?

@almarklein,

You can omit the timer in the first attempt, and add fps limiting later, or in a second PR.

o.k.

It's not actually necessary to implement get_display_id because the base class has an implementation to obtain this value (see base.py).

Yes, I seen that too. ;)

def close() is needed to be able to programmatically close an application. If close means hide, that's fine. The is_closed method is used in the example to determine if the application should stop running. It should return True when the user clicks the cross, or when close() has been called.

Oh, do you mean it needs to close the whole application window or just the widget? Since I've implemented a widget (strangely called wx.Window) and not wx.Frame (wxpython's application window), this would only hide/close the widget. Could you please clarify?

Thanks!

@almarklein
Copy link
Collaborator

do you mean it needs to close the whole application window or just the widget? S

Good question. The close mechanism may need some thought / documentation. I think the close and is_closed methods should only apply when the widget is toplevel. They are used in the examples and in the tests. And people could potentially use them in small apps. But for anything bigger, people won't use these methods anyway.

@Correct-Syntax
Copy link
Contributor Author

So, then hiding them like this should be o.k?

@Korijn
Copy link
Collaborator

Korijn commented Feb 23, 2021

So, then hiding them like this should be o.k?

Yes, this is fine!

I think that in order to merge your PR all that is needed is for you to remove the overloaded get_display_id method. A timer and an example can be added in subsequent PRs. Nice job!

@Correct-Syntax
Copy link
Contributor Author

I think that in order to merge your PR all that is needed is for you to remove the overloaded get_display_id method. A timer and an example can be added in subsequent PRs. Nice job!

Thanks! Will do.

@Correct-Syntax
Copy link
Contributor Author

Should be fixed. Let me know if anything else is needed for now.

@Korijn
Copy link
Collaborator

Korijn commented Feb 24, 2021

I just tried running it with wxPython 4.1.1, and run into the following:

λ poetry run python examples\triangle_wxpython.py
Traceback (most recent call last):
  File "examples\triangle_wxpython.py", line 16, in <module>
    main(frm)
  File "C:\Users\korij_000\dev\wgpu-py\examples\triangle.py", line 52, in main
    return _main(canvas, device)
  File "C:\Users\korij_000\dev\wgpu-py\examples\triangle.py", line 112, in _main
    wgpu.TextureUsage.OUTPUT_ATTACHMENT,
  File "C:\Users\korij_000\dev\wgpu-py\wgpu\backends\rs.py", line 1062, in configure_swap_chain
    return GPUSwapChain(self, canvas, format, usage)
  File "C:\Users\korij_000\dev\wgpu-py\wgpu\backends\rs.py", line 1806, in __init__
    self._create_native_swap_chain_if_needed()
  File "C:\Users\korij_000\dev\wgpu-py\wgpu\backends\rs.py", line 1810, in _create_native_swap_chain_if_needed
    psize = canvas.get_physical_size()
  File "C:\Users\korij_000\dev\wgpu-py\wgpu\gui\wxpython.py", line 37, in get_physical_size
    lsize = self.GetWidth(), self.GetHeight()
AttributeError: 'WxWgpuCanvas' object has no attribute 'GetWidth'

Here's the triangle_wxpython.py script I'm running to give this a go:

"""
Import the viz from triangle.py and run it in a wxPython window.
"""

import wx
from wgpu.gui.wxpython import WxWgpuCanvas
import wgpu.backends.rs  # noqa: F401, Select Rust backend

# Import the (async) function that we must call to run the visualization
from triangle import main


app = wx.App()
frm = WxWgpuCanvas()
frm.Show()
main(frm)
app.MainLoop()

Any idea what might be wrong? I looked at the docs for wx.Window and couldn't find any mention of the GetWidth method.

I'd like to see this in a working state/know how to use it at least once before we merge. :)

@Correct-Syntax
Copy link
Contributor Author

Hi @Korijn,

I looked at the docs for wx.Window and couldn't find any mention of the GetWidth method.

Yes, you're right. It should be self.Size[0], etc. Evidently, in my mind I was thinking about wx.Bitmap which does have GetWidth, etc. ;)

Maybe we should change the wx.Window to wx.Panel? I guess it depends on how this will be used, but wx.Window will give us a widget, whereas wx.Panel is probably more along the lines of what is needed here(?)

wxpython.py

class WxWgpuCanvas(WgpuCanvasBase, wx.Window):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def get_window_id(self):
        return int(self.GetId())

    def get_pixel_ratio(self):
        return self.GetDPIScaleFactor()

    def get_logical_size(self):
        lsize = self.Size[0], self.Size[1]
        return float(lsize[0]), float(lsize[1])

    def get_physical_size(self):
        lsize = self.Size[0], self.Size[1]
        lsize = float(lsize[0]), float(lsize[1])
        ratio = self.GetDPIScaleFactor()
        return round(lsize[0] * ratio), round(lsize[1] * ratio)

    def set_logical_size(self, width, height):
        if width < 0 or height < 0:
            raise ValueError("Window width and height must not be negative")
        self.SetSize(width, height)

    def _request_draw(self):
        pass

    def close(self):
        self.Hide()

    def is_closed(self):
        return not self.IsShown()

Also, I think the demo script is going to need a wx.Frame. Currently, the WxWgpuCanvas is not a wx.Frame itself, so I am thinking it will have to be placed into a wx.Frame for it to work. Something like:

"""
Import the viz from triangle.py and run it in a wxPython window.
"""

import wx
from wgpu.gui.wxpython import WxWgpuCanvas
import wgpu.backends.rs  # noqa: F401, Select Rust backend

# Import the (async) function that we must call to run the visualization
from triangle import main

class AppFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="WxWgpuCanvas Demo"):
        wx.Frame.__init__(self, parent, id, title, pos, size, style, name)

        canvas= WxWgpuCanvas(self)

        self.Bind(wx.EVT_CLOSE, self.OnDestroy)

    def OnDestroy(self, event):
        self.Destroy()

app = wx.App()
frm = AppFrame()
frm.Show()
main(frm)
app.MainLoop()

Thanks!

@Korijn
Copy link
Collaborator

Korijn commented Feb 25, 2021

After making that change we get further:

λ poetry run python examples\triangle_wxpython.py
thread '<unnamed>' panicked at 'Unable to query surface capabilities: ERROR_SURFACE_LOST_KHR', C:\Users\runneradmin\.cargo\registry\src\github.com-1ecc6299db9ec823\gfx-backend-vulkan-0.5.2\src\window.rs:346:20
stack backtrace:
   0:     0x7ff96203f98f - wgpu_render_pass_finish
   1:     0x7ff962053b6b - rust_eh_personality
   2:     0x7ff96203d49c - wgpu_render_pass_finish
   3:     0x7ff96204269c - wgpu_render_pass_finish
   4:     0x7ff9620422df - wgpu_render_pass_finish
   5:     0x7ff962042dd7 - wgpu_render_pass_finish
   6:     0x7ff96204295f - wgpu_render_pass_finish
   7:     0x7ff962051810 - rust_eh_personality
   8:     0x7ff962051643 - rust_eh_personality
   9:     0x7ff961fc591f - wgpu_render_pass_finish
  10:     0x7ff961e128ff - wgpu_compute_pass_insert_debug_marker
  11:     0x7ff9868210f3 - <unknown>
  12:     0x7ff98683a3c0 - PyInit__cffi_backend
  13:     0x7ff986832e86 - PyInit__cffi_backend
  14:         0x73c1846a - PyObject_GetAttr
  15:         0x73c18b4e - PyEval_EvalFrameDefault
  16:         0x73c182e0 - PyObject_GetAttr
  17:         0x73c18b4e - PyEval_EvalFrameDefault
  18:         0x73c15728 - PyObject_Free
  19:         0x73c13c23 - PyFunction_FastCallDict
  20:         0x73c13a06 - PyObject_ClearWeakRefs
  21:         0x73c11e8d - PyNumber_InPlaceAdd
  22:         0x73c16c37 - PyUnicode_InternInPlace
  23:         0x73c1846a - PyObject_GetAttr
  24:         0x73c18b4e - PyEval_EvalFrameDefault
  25:         0x73c15728 - PyObject_Free
  26:         0x73c18587 - PyObject_GetAttr
  27:         0x73c18b4e - PyEval_EvalFrameDefault
  28:         0x73c15728 - PyObject_Free
  29:         0x73c18587 - PyObject_GetAttr
  30:         0x73c18b4e - PyEval_EvalFrameDefault
  31:         0x73c182e0 - PyObject_GetAttr
  32:         0x73c18b4e - PyEval_EvalFrameDefault
  33:         0x73c15728 - PyObject_Free
  34:         0x73beb45f - PyEval_EvalCodeEx
  35:         0x73beb3bd - PyEval_EvalCode
  36:         0x73beb367 - PyArena_Free
  37:         0x73d6d181 - PyRun_FileExFlags
  38:         0x73d6d9ac - PyRun_SimpleFileExFlags
  39:         0x73d6d04f - PyRun_AnyFileExFlags
  40:         0x73cbc173 - Py_hashtable_size
  41:         0x73c5ec8d - PyThreadState_UncheckedGet
  42:         0x1c561258 - <unknown>
  43:     0x7ff9cc937034 - BaseThreadInitThunk
  44:     0x7ff9cdfdd241 - RtlUserThreadStart

@almarklein Any ideas?

@Correct-Syntax
Copy link
Contributor Author

Correct-Syntax commented Feb 25, 2021

Not sure if it's the problem, but I just realized it should use args and kwargs, like:

"""
Import the viz from triangle.py and run it in a wxPython window.
"""

import wx
from wgpu.gui.wxpython import WxWgpuCanvas
import wgpu.backends.rs  # noqa: F401, Select Rust backend

# Import the (async) function that we must call to run the visualization
from triangle import main

class AppFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)

        canvas= WxWgpuCanvas(self)

        self.Bind(wx.EVT_CLOSE, self.OnDestroy)

    def OnDestroy(self, event):
        self.Destroy()

app = wx.App()
frm = AppFrame()
frm.Show()
main(frm)
app.MainLoop()

The window itself should now be working correctly, at least.

@almarklein
Copy link
Collaborator

Maybe we should change the wx.Window to wx.Panel? I guess it depends on how this will be used, but wx.Window will give us a widget, whereas wx.Panel is probably more along the lines of what is needed here(?)

I think some users will want to use it as a widget, but making it easy to do a quick example is also nice. Maybe we should include both?

Unable to query surface capabilities

Mmm, that smells like the surface id not being matched or something.

@Correct-Syntax
Copy link
Contributor Author

Correct-Syntax commented Feb 25, 2021

If we still need to implement the get_display_id method, this might be of some help: https://discuss.wxpython.org/t/how-to-get-the-display-id-for-a-wx-window/35221. Not exactly sure if this helps...

@Korijn
Copy link
Collaborator

Korijn commented Feb 28, 2021

I've looked into this a bit and I don't see any custom draw event code here (in this PR). For the Qt canvas widget we had to disable the standard draw event, so that wgpu-py could draw instead. Is there a similar thing in wxPython? I noticed they have a GLCanvas subclass of Window for example?

@Correct-Syntax
Copy link
Contributor Author

@Korijn, thanks for looking into this.

Yes, that is probably what needs to be done. I wasn't sure about the drawing part, but wxpython does have ways to draw in a similar fashion, etc.

However, I can't really tell what is needed from the qt.py as I've never used qt before and not sure I understand what is being done with the drawing in the qt example. Is the qt paintEvent the method we're looking at?

I am familiar with drawing in wxpython so maybe if you could tell me what is needed (tell me what is going on as far as the drawing in the qt demo), I could try to get this implemented. The main thing I don't understand is how the wgpu can draw to the qt window via self.parent()._draw_frame_and_present(). Is that just calling the draw for wgpu from WgpuCanvasBase?

If so, it may be something like:

class WxWgpuCanvas(WgpuCanvasBase, wx.Window):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)

    def on_paint(self, event):
        self._draw_frame_and_present()

        self.Refresh()
        self.Update()

but, as I said, I'm not sure what exactly is supposed to happen in order for wgpu to draw, etc. Thanks.

@Korijn
Copy link
Collaborator

Korijn commented Mar 1, 2021

Well, if you look at WgpuSubWidget in the Qt module you'll see that it does four things:

  • Return None as the paint engine (overriding any default)
  • implement a custom paintEvent handler (where wgpu actually draw)
  • Disable autoFillBackground
  • set paintOnScreen (ensuring a native window is created by Qt)

Together this ensures Qt leaves the drawing entirely up to wgpu, if I understand correctly.

@Korijn Korijn closed this Mar 1, 2021
@Korijn Korijn reopened this Mar 1, 2021
@almarklein
Copy link
Collaborator

You can also have a look at the wx backend for Vispy: https://github.com/vispy/vispy/blob/master/vispy/app/backends/_wx.py#L288-L297

@Correct-Syntax
Copy link
Contributor Author

Ah, okay. :) Thanks, I will look into this.

@Correct-Syntax
Copy link
Contributor Author

Hmmm....I am not sure. Is the problem the lack of a timer to control the frame-rate?

thread '<unnamed>' panicked at 'Unable to query surface capabilities: ERROR_OUT_OF_HOST_MEMORY', C:\Users\runneradmin\.cargo\registry\src\github.com-1ecc6299db9ec823\gfx-backend-vulkan-0.5.2\src\window.rs:346:20
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

triangle_wxpython.py

class AppFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)

        self.canvas = WxWgpuCanvas(self)

        self.Bind(wx.EVT_CLOSE, self.OnDestroy)

    def OnDestroy(self, event):
        self.Destroy()

app = wx.App()
frm = AppFrame()
frm.Show()
main(frm.canvas) # this was needed to access the canvas instead of the wx.frame
app.MainLoop()

wxpython.py

class WxWgpuCanvas(wx.Window, WgpuCanvasBase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)

    def on_paint(self, event):
        dc = wx.PaintDC(self)  # needed for wx
        self._draw_frame_and_present()
        del dc
        event.Skip()

    def get_window_id(self):
        return int(self.GetId())

    def get_pixel_ratio(self):
        return 1 #self.GetDPIScaleFactor()

    def get_logical_size(self):
        lsize = self.Size[0], self.Size[1]
        return float(lsize[0]), float(lsize[1])

    def get_physical_size(self):
        lsize = self.Size[0], self.Size[1]
        lsize = float(lsize[0]), float(lsize[1])
        ratio = 1 #self.GetDPIScaleFactor()
        return round(lsize[0] * ratio), round(lsize[1] * ratio)

    def set_logical_size(self, width, height):
        if width < 0 or height < 0:
            raise ValueError("Window width and height must not be negative")
        self.SetSize(width, height)

    def _request_draw(self):
        pass
        # dc = wx.PaintDC(self)  # needed for wx
        # self._draw_frame_and_present()
        # del dc

    def close(self):
        self.Hide()

    def is_closed(self):
        return not self.IsShown()

Note: I just set the dpi factor to 1 because it seemed to not be able to access the method, strangely.

Whether I used _request_draw or not, it still came up with the same error. Not sure if the PaintDC is needed. From what I can tell, the only difference between this and the qt version (for reference) is that the qt implementation uses a subwidget and a timer. Is that needed here, you think? Any ideas on what I could try next?

@almarklein
Copy link
Collaborator

Unable to query surface capabilities: ERROR_OUT_OF_HOST_MEMORY

It somewhat looks like it cannot get the server because it broke earlier due to running out of memory. Can you see whether there are any draws before the error occurs? Perhaps there is a memory leak somehow. I remember such leaks occurring when resizing a window a lot (not sure if this was an error in pygfx/wgpu-py/wgpu-native), so it looked like framebuffers not being cleaned up.

I don't think the timer thing would help prevent this error.

@Correct-Syntax
Copy link
Contributor Author

Correct-Syntax commented Mar 22, 2021

Sorry for my tardy reply. :)

So, I re-installed everything according to the README (into a virtualenv) as I suspect I had installed things improperly the first time and ran the triangle_wxpython.py file:

class AppFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)

        self.canvas = WxWgpuCanvas(self)

        self.Bind(wx.EVT_CLOSE, self.OnDestroy)

    def OnDestroy(self, event):
        self.Destroy()

app = wx.App()
frm = AppFrame(parent=None, title="wgpu triangle with wxPython")
frm.Show()
main(frm.canvas)
app.MainLoop()

wxpython.py:

class WxWgpuCanvas(wx.Window, WgpuCanvasBase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)

    def on_paint(self, event):
        #print("paint")
        dc = wx.PaintDC(self)  # needed for wx
        self._draw_frame_and_present()
        del dc
        #event.Skip()

    def get_window_id(self):
        return int(self.GetId())

    def get_pixel_ratio(self):
        return 1 #self.GetDPIScaleFactor()

    def get_logical_size(self):
        lsize = self.Size[0], self.Size[1]
        return float(lsize[0]), float(lsize[1])

    def get_physical_size(self):
        lsize = self.Size[0], self.Size[1]
        lsize = float(lsize[0]), float(lsize[1])
        ratio = 1 #self.GetDPIScaleFactor()
        return round(lsize[0] * ratio), round(lsize[1] * ratio)

    def set_logical_size(self, width, height):
        if width < 0 or height < 0:
            raise ValueError("Window width and height must not be negative")
        self.SetSize(width, height)

    def _request_draw(self):
        pass

    def close(self):
        self.Hide()

    def is_closed(self):
        return not self.IsShown()

The widow comes up with a white square in the corner, then it crashes with this error (I guess this is progress!):

thread '<unnamed>' panicked at 'Unable to query surface capabilities: ERROR_SURFACE_LOST_KHR', C:\Users\runneradmin\.cargo\registry\src\github.com-1ecc6299db9ec823\gfx-backend-vulkan-0.5.2\src\window.rs:346:20 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

It seems to be similar to the one @Korijn had.

Is the RUST_BACKTRACE a python environment variable? Maybe I should enable that? Any other ideas why it's "losing the surface"?

EDIT: it doesn't seem that the on_paint or _request_draw are getting called before it crashes.

@almarklein
Copy link
Collaborator

Is the RUST_BACKTRACE a python environment variable?

It's an environment variable that can be set to let Rust (wgpu-native is written in Rust) produce more info. But that maybe only when you're using the debug version of the lib.

Any other ideas why it's "losing the surface"?

I think that it lost the surface because its 'Unable to query surface capabilities', which might imply that the window id is not valid, perhaps?

@almarklein
Copy link
Collaborator

I gave it a try, and I managed to get it working by replacing getId() with GetHandle(). I think getID() is an id specific to wx. GetHandle() should be the native object: https://wxpython.org/Phoenix/docs/html/wx.Window.html#wx.Window.GetHandle

With that, it works on Windows. Not sure if any casting is required on Mac and Linux.

@Correct-Syntax
Copy link
Contributor Author

I confirmed that it works on Windows as well. I will update the PR then, @almarklein?

@almarklein
Copy link
Collaborator

Yes, please!

@Correct-Syntax
Copy link
Contributor Author

So, this all seems to be working -which is great. However, it seems that when I change the dpi to self.GetDPIScaleFactor() from my hard-coded 1, I get:

Requested size 478x254 is outside of the supported range: Extent2D { width: 382, height: 203 }..=Extent2D { width: 382, height: 203 }
paint
present failed: OutOfDate

paint is a print statement in my code, btw....

@almarklein
Copy link
Collaborator

Did you also update it in get_physical_size? It seems to work for me

@Korijn
Copy link
Collaborator

Korijn commented Mar 25, 2021

Can you add triangle_wxpython.py to the PR as well?

@Correct-Syntax
Copy link
Contributor Author

Did you also update it in get_physical_size? It seems to work for me

Nope. Same error each time. For me, the self.GetDPIScaleFactor() is returning 1.25.

Can you add triangle_wxpython.py to the PR as well?

Sure. Haven't gotten there yet. :)

@Correct-Syntax
Copy link
Contributor Author

This is the complete source I have currently:

"""
Support for rendering in a wxPython window. Provides a widget that
can be used as a standalone window or in a larger GUI.
"""

import sys
import time
import ctypes
import importlib

from .base import WgpuCanvasBase

import wx

try:
    # fix blurry text on windows
    ctypes.windll.shcore.SetProcessDpiAwareness(True)
except Exception:
    pass  # fail on non-windows


class WxWgpuCanvas(wx.Window, WgpuCanvasBase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)

    def on_paint(self, event):
        dc = wx.PaintDC(self)  # needed for wx
        self._draw_frame_and_present()
        del dc
        event.Skip()

    def get_window_id(self):
        return int(self.GetHandle())

    def get_pixel_ratio(self):
        return self.GetDPIScaleFactor()

    def get_logical_size(self):
        lsize = self.Size[0], self.Size[1]
        return float(lsize[0]), float(lsize[1])

    def get_physical_size(self):
        lsize = self.Size[0], self.Size[1]
        lsize = float(lsize[0]), float(lsize[1])
        ratio = self.GetDPIScaleFactor()
        return round(lsize[0] * ratio), round(lsize[1] * ratio)

    def set_logical_size(self, width, height):
        if width < 0 or height < 0:
            raise ValueError("Window width and height must not be negative")
        self.SetSize(width, height)

    def _request_draw(self):
        pass

    def close(self):
        self.Hide()

    def is_closed(self):
        return not self.IsShown()
"""
Import the viz from triangle.py and run it in a wxPython window.
"""

import wx
from wgpu.gui.wxpython import WxWgpuCanvas
import wgpu.backends.rs  # noqa: F401, Select Rust backend

# Import the (async) function that we must call to run the visualization
from examples.triangle import main


class AppFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)

        self.canvas = WxWgpuCanvas(self)

        self.Bind(wx.EVT_CLOSE, self.OnDestroy)

    def OnDestroy(self, event):
        self.Destroy()

app = wx.App()
frm = AppFrame(parent=None, title="wgpu triangle with wxPython")
frm.Show()
main(frm.canvas)
app.MainLoop()

@Korijn
Copy link
Collaborator

Korijn commented Mar 25, 2021

Is it possible that wx's self.size property is already multiplied by the scale factor?

There is also GetContentScaleFactor:

Returns the factor mapping logical pixels of this window to physical pixels

There's also ToDIP and FromDIP : https://wxpython.org/Phoenix/docs/html/wx.Window.html#wx.Window.ToDIP

This method performs the conversion only if it is not already done by the lower level toolkit

Maybe they give different results?

@Correct-Syntax
Copy link
Contributor Author

Correct-Syntax commented Mar 25, 2021

Is it possible that wx's self.size property is already multiplied by the scale factor?

Maybe.

GetContentScaleFactor works, so maybe just change it to that and hope for the best... If we need to, I guess we always can edit that later if there is some error/undesired result.

Also, it should be noted that because we're accessing the canvas (WxWgpuCanvas) through main(frm.canvas), maybe I should add a comment about that?

Overall, this is probably ready to merge, correct? -since this is "add initial wxpython window support". (I mean, after I commit the changes, of course)

@Korijn
Copy link
Collaborator

Korijn commented Mar 25, 2021

I think so! 🎉 Great work!

@Korijn
Copy link
Collaborator

Korijn commented Mar 26, 2021

I pushed your changes to your branch ;) so it's working now:

image

Let's merge!

@Korijn Korijn merged commit 2123070 into pygfx:main Mar 26, 2021
@Korijn Korijn mentioned this pull request Mar 26, 2021
4 tasks
@almarklein
Copy link
Collaborator

Nice! Thanks @Correct-Syntax !

@Correct-Syntax
Copy link
Contributor Author

Correct-Syntax commented Mar 26, 2021

I pushed your changes to your branch ;)

Uh-oh, I guess I now realize I probably committed to the repository instead of my fork (between switching computers, I lost track!). Sorry. Thanks for taking care of that @Korijn!

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

Successfully merging this pull request may close these issues.

None yet

3 participants