Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions rendercanvas/_size.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
class SizeInfo(dict):
"""A dict with size information, for internal use.

Handy to have a separate dict, so that it can be passed to objects that need it.
Also allows canvases to create size callbacks without holding a ref to the canvas.
"""

def __init__(self):
super().__init__()
self["physical_size"] = 1, 1
self["native_pixel_ratio"] = 1.0
self["canvas_pixel_ratio"] = 1.0
self["total_pixel_ratio"] = 1.0
self["logical_size"] = 1.0, 1.0
self["need_size_event"] = False
self["need_context_resize"] = False

def set_physical_size(self, width: int, height: int, pixel_ratio: float):
"""Must be called by subclasses when their size changes.

The given pixel-ratio represents the 'native' pixel ratio. The canvas'
zoom factor is multiplied with it to obtain the final pixel-ratio for
this canvas.
"""
self["physical_size"] = int(width), int(height)
self["native_pixel_ratio"] = float(pixel_ratio)
self._resolve_total_pixel_ratio_and_logical_size()

def _resolve_total_pixel_ratio_and_logical_size(self):
physical_size = self["physical_size"]
native_pixel_ratio = self["native_pixel_ratio"]
canvas_pixel_ratio = self["canvas_pixel_ratio"]

total_pixel_ratio = native_pixel_ratio * canvas_pixel_ratio
logical_size = (
physical_size[0] / total_pixel_ratio,
physical_size[1] / total_pixel_ratio,
)

self["total_pixel_ratio"] = total_pixel_ratio
self["logical_size"] = logical_size

self["need_size_event"] = True
self["need_context_resize"] = True

def set_logical_size(self, width: float, height: float):
"""Called by the canvas when the logical size is set.

This calculates the expected physical size (with the current pixel ratio),
to get the corrected logical size. But this *only* sets the logical_size
and total_pixel_ratio. The backend will, likely before the next draw,
adjust the size and call set_physical_size(), which re-sets the logical size.
"""
# Calculate adjusted logical size
ratio = self["native_pixel_ratio"] * self["canvas_pixel_ratio"]
pwidth = max(1, round(float(width) * ratio + 0.01))
pheight = max(1, round(float(height) * ratio + 0.01))
lwidth, lheight = pwidth / ratio, pheight / ratio

# Update logical size and total ratio. You could see it as a temporary zoom factor being applied.
# Note that The backend will soon call set_physical_size().
self["logical_size"] = lwidth, lheight
self["total_pixel_ratio"] = self["physical_size"][0] / lwidth

self["need_size_event"] = True
self["need_context_resize"] = True

def set_zoom(self, zoom: float):
"""Set the zoom factor, i.e. the canvas pixel ratio."""
self["canvas_pixel_ratio"] = float(zoom)
self._resolve_total_pixel_ratio_and_logical_size()
73 changes: 17 additions & 56 deletions rendercanvas/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CursorShapeEnum,
)
from . import contexts
from ._size import SizeInfo
from ._events import EventEmitter
from ._loop import BaseLoop
from ._scheduler import Scheduler
Expand Down Expand Up @@ -153,15 +154,7 @@ def __init__(
if (self._rc_canvas_group and self._rc_canvas_group.get_loop())
else "no-loop",
}
self.__size_info = {
"physical_size": (0, 0),
"native_pixel_ratio": 1.0,
"canvas_pixel_ratio": 1.0,
"total_pixel_ratio": 1.0,
"logical_size": (0.0, 0.0),
}
self.__need_size_event = False
self.__need_context_resize = True # True bc context may be created later
self._size_info = SizeInfo()

# Events and scheduler
self._events = EventEmitter()
Expand Down Expand Up @@ -224,7 +217,7 @@ def __del__(self):

def get_physical_size(self) -> tuple[int, int]:
"""Get the physical size of the canvas in integer pixels."""
return self.__size_info["physical_size"]
return self._size_info["physical_size"]

def get_bitmap_context(self) -> contexts.BitmapContext:
"""Get the ``BitmapContext`` to render to this canvas."""
Expand Down Expand Up @@ -323,41 +316,6 @@ def get_context(

# %% Events

def _set_size_info(
self, physical_width: int, physical_height: int, pixel_ratio: float
):
"""Must be called by subclasses when their size changes.

Backends must *not* submit a "resize" event; the base class takes care of that, because
it requires some more attention than the other events.

The subclass must call this when the actual viewport has changed. So not in ``_rc_set_logical_size()``,
but e.g. when the underlying GUI layer fires a resize event, and maybe on init.

The given pixel-ratio represents the 'native' pixel ratio. The canvas'
zoom factor is multiplied with it to obtain the final pixel-ratio for
this canvas.
"""
self.__size_info["physical_size"] = int(physical_width), int(physical_height)
self.__size_info["native_pixel_ratio"] = float(pixel_ratio)
self.__resolve_total_pixel_ratio_and_logical_size()

def __resolve_total_pixel_ratio_and_logical_size(self):
physical_size = self.__size_info["physical_size"]
native_pixel_ratio = self.__size_info["native_pixel_ratio"]
canvas_pixel_ratio = self.__size_info["canvas_pixel_ratio"]

total_pixel_ratio = native_pixel_ratio * canvas_pixel_ratio
logical_size = (
physical_size[0] / total_pixel_ratio,
physical_size[1] / total_pixel_ratio,
)

self.__size_info["total_pixel_ratio"] = total_pixel_ratio
self.__size_info["logical_size"] = logical_size
self.__need_size_event = True
self.__need_context_resize = True

def add_event_handler(
self, *args: EventTypeEnum | EventHandlerFunction, order: float = 0
) -> Callable:
Expand All @@ -380,23 +338,23 @@ def submit_event(self, event: dict) -> None:

def __maybe_emit_resize_event(self):
# Keep context up-to-date
if self.__need_context_resize and self._canvas_context is not None:
self.__need_context_resize = False
self._canvas_context._rc_set_size_dict(self.__size_info)
if self._size_info["need_context_resize"] and self._canvas_context is not None:
self._size_info["need_context_resize"] = False
self._canvas_context._rc_set_size_dict(self._size_info)

# Keep event listeners up-to-date
if self.__need_size_event:
self.__need_size_event = False
lsize = self.__size_info["logical_size"]
if self._size_info["need_size_event"]:
self._size_info["need_size_event"] = False
lsize = self._size_info["logical_size"]
self._events.emit(
{
"event_type": "resize",
"width": lsize[0],
"height": lsize[1],
"pixel_ratio": self.__size_info["total_pixel_ratio"],
"pixel_ratio": self._size_info["total_pixel_ratio"],
# Would be nice to have more details. But as it is now, PyGfx errors if we add fields it does not know, so let's do later.
# "logical_size": self.__size_info["logical_size"],
# "physical_size": self.__size_info["physical_size"],
# "logical_size": self._size_info["logical_size"],
# "physical_size": self._size_info["physical_size"],
}
)

Expand Down Expand Up @@ -575,7 +533,7 @@ def get_logical_size(self) -> tuple[float, float]:
The logical size can be smaller than the physical size, e.g. on HiDPI
monitors or when the user's system has the display-scale set to e.g. 125%.
"""
return self.__size_info["logical_size"]
return self._size_info["logical_size"]

def get_pixel_ratio(self) -> float:
"""Get the float ratio between logical and physical pixels.
Expand All @@ -585,7 +543,7 @@ def get_pixel_ratio(self) -> float:
display-scale is set to e.g. 125%. An HiDPI screen can be assumed if the
pixel ratio >= 2.0.
"""
return self.__size_info["total_pixel_ratio"]
return self._size_info["total_pixel_ratio"]

def close(self) -> None:
"""Close the canvas."""
Expand Down Expand Up @@ -628,6 +586,9 @@ def set_logical_size(self, width: float, height: float) -> None:
width, height = float(width), float(height)
if width < 0 or height < 0:
raise ValueError("Canvas width and height must not be negative")
# Already adjust our logical size, so e.g. layout engines can get to work
self._size_info.set_logical_size(width, height)
# Tell the backend to adjust the size. It will likely set the new physical size before the next draw.
self._rc_set_logical_size(width, height)

def set_title(self, title: str) -> None:
Expand Down
2 changes: 1 addition & 1 deletion rendercanvas/glfw.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def _determine_size(self):
pwidth, pheight = get_physical_size(self._window)

self._pixel_ratio = pixel_ratio # store
self._set_size_info(pwidth, pheight, pixel_ratio)
self._size_info.set_physical_size(pwidth, pheight, pixel_ratio)

def _on_want_close(self, *args):
# Called when the user attempts to close the window, for example by clicking the close widget in the title bar.
Expand Down
2 changes: 1 addition & 1 deletion rendercanvas/jupyter.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def handle_event(self, event):
pixel_ratio = event["pixel_ratio"]
pwidth = int(logical_size[0] * pixel_ratio)
pheight = int(logical_size[1] * pixel_ratio)
self._set_size_info(pwidth, pheight, pixel_ratio)
self._size_info.set_physical_size(pwidth, pheight, pixel_ratio)
self.request_draw()
return

Expand Down
10 changes: 5 additions & 5 deletions rendercanvas/offscreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ def _rc_present_bitmap(self, *, data, format, **kwargs):
def _rc_set_logical_size(self, width, height):
logical_size = float(width), float(height)
pixel_ratio = self._pixel_ratio
pwidth = int(logical_size[0] * pixel_ratio)
pheight = int(logical_size[1] * pixel_ratio)
self._set_size_info(pwidth, pheight, pixel_ratio)
pwidth = max(1, round(logical_size[0] * pixel_ratio + 0.01))
pheight = max(1, round(logical_size[1] * pixel_ratio + 0.01))
self._size_info.set_physical_size(pwidth, pheight, pixel_ratio)

def _rc_close(self):
self._closed = True
Expand All @@ -100,7 +100,7 @@ def set_physical_size(self, width: int, height: int):
"""
pwidth = int(width)
pheight = int(height)
self._set_size_info(pwidth, pheight, self._pixel_ratio)
self._size_info.set_physical_size(pwidth, pheight, self._pixel_ratio)

def set_pixel_ratio(self, pixel_ratio: float):
"""Set the pixel ratio, changing the logical size of the canvas.
Expand All @@ -110,7 +110,7 @@ def set_pixel_ratio(self, pixel_ratio: float):
"""
self._pixel_ratio = float(pixel_ratio)
pwidth, pheight = self.get_physical_size()
self._set_size_info(pwidth, pheight, self._pixel_ratio)
self._size_info.set_physical_size(pwidth, pheight, self._pixel_ratio)

def draw(self):
"""Perform a draw and get the resulting image.
Expand Down
2 changes: 1 addition & 1 deletion rendercanvas/pyodide.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def _resize_callback(entries, _=None):

# Notify the base class, so it knows our new size
pwidth, pheight = psize
self._set_size_info(pwidth, pheight, window.devicePixelRatio)
self._size_info.set_physical_size(pwidth, pheight, window.devicePixelRatio)

self._resize_callback_proxy = create_proxy(_resize_callback)
self._resize_observer = ResizeObserver.new(self._resize_callback_proxy)
Expand Down
2 changes: 1 addition & 1 deletion rendercanvas/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def resizeEvent(self, event): # noqa: N802
# we can't. Not an issue on qt5, because ratio is always integer then.
pwidth = round(lsize[0] * ratio + 0.01)
pheight = round(lsize[1] * ratio + 0.01)
self._set_size_info(pwidth, pheight, ratio)
self._size_info.set_physical_size(pwidth, pheight, ratio)
# self.update() / self.request_draw() is implicit

def closeEvent(self, event): # noqa: N802
Expand Down
31 changes: 15 additions & 16 deletions rendercanvas/stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,26 @@ class StubRenderCanvas(BaseRenderCanvas):
"""
The ``RenderCanvas`` represents the canvas to render to.

Backends must subclass ``BaseRenderCanvas`` and implement a set of methods prefixed with ``_rc_``.
This class also shows a few other private methods of the base canvas class, that a backend must be aware of.
"""

# Note that the methods below don't have docstrings, but Sphinx recovers the docstrings from the base class.
Backends must subclass ``BaseRenderCanvas`` and implement a set of methods
prefixed with ``_rc_``.

# Just listed here so they end up in the docs
Backends must call ``self._final_canvas_init()`` at the end of its
``__init__()``. This will set the canvas' logical size and title.

def _final_canvas_init(self):
return super()._final_canvas_init()
Backends must call ``self._draw_frame_and_present()`` to make the actual
draw. This should typically be done inside the backend's native draw event.

def _set_size_info(self, physical_width, physical_height, pixel_ratio):
return super()._set_size_info(physical_width, physical_height, pixel_ratio)
Backends must call ``self._size_info.set_physical_size(width, height, native_pixel_ratio)``,
whenever the size or pixel ratio changes. It must be called when the actual
viewport has changed, so typically not in ``_rc_set_logical_size()``, but
e.g. when the underlying GUI layer fires a resize event.

def _process_events(self):
return super()._process_events()

def _draw_frame_and_present(self):
return super()._draw_frame_and_present()
Backends must also call ``self.submit_event()``, if applicable, to produce
events for mouse and keyboard. Backends must *not* submit a "resize" event;
the base class takes care of that. See the event spec for details.
"""

# Must be implemented by subclasses.
# Note that the methods below don't have docstrings, but Sphinx recovers the docstrings from the base class.

_rc_canvas_group = StubCanvasGroup(loop)

Expand Down
2 changes: 1 addition & 1 deletion rendercanvas/wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ def _on_resize(self, event: wx.SizeEvent):
# We use the same logic as for Qt to derive the physical size.
pwidth = round(lsize[0] * ratio + 0.01)
pheight = round(lsize[1] * ratio + 0.01)
self._set_size_info(pwidth, pheight, ratio)
self._size_info.set_physical_size(pwidth, pheight, ratio)

def _on_key_down(self, event: wx.KeyEvent):
char_str = self._get_char_from_event(event)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class MyOffscreenCanvas(rendercanvas.BaseRenderCanvas):
def __init__(self):
super().__init__()
self.frame_count = 0
self._set_size_info(100, 100, 1)
self._size_info.set_physical_size(100, 100, 1)

def _rc_get_present_methods(self):
return {
Expand Down Expand Up @@ -153,7 +153,7 @@ def draw_frame():
assert np.all(canvas.array[:, :, 1] == 255)

# Change resolution
canvas._set_size_info(120, 100, 1)
canvas._size_info.set_physical_size(120, 100, 1)

# Draw 3
canvas.force_draw()
Expand All @@ -162,7 +162,7 @@ def draw_frame():
assert np.all(canvas.array[:, :, 1] == 255)

# Change resolution
canvas._set_size_info(120, 140, 1)
canvas._size_info.set_physical_size(120, 140, 1)

# Draw 4
canvas.force_draw()
Expand Down
28 changes: 28 additions & 0 deletions tests/test_offscreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,33 @@ def test_offscreen_canvas_del():
assert ref() is None


def test_offscreen_extra_size_methods():
from rendercanvas.offscreen import RenderCanvas

c = RenderCanvas()

assert c.get_physical_size() == (640, 480)
assert c.get_logical_size() == (640.0, 480.0)
assert c.get_pixel_ratio() == 1.0

c.set_physical_size(100, 100)

assert c.get_physical_size() == (100, 100)
assert c.get_logical_size() == (100.0, 100.0)
assert c.get_pixel_ratio() == 1.0

c.set_pixel_ratio(2)

assert c.get_physical_size() == (100, 100)
assert c.get_logical_size() == (50.0, 50.0)
assert c.get_pixel_ratio() == 2.0

c.set_logical_size(100, 100)

assert c.get_physical_size() == (200, 200)
assert c.get_logical_size() == (100.0, 100.0)
assert c.get_pixel_ratio() == 2.0


if __name__ == "__main__":
run_tests(globals())
Loading