mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
242 lines
8.1 KiB
Python
242 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
import moderngl_window as mglw
|
|
from moderngl_window.context.pyglet.window import Window as PygletWindow
|
|
from moderngl_window.timers.clock import Timer
|
|
from functools import wraps
|
|
import screeninfo
|
|
|
|
from manimlib.config import get_global_config
|
|
from manimlib.constants import ASPECT_RATIO
|
|
from manimlib.constants import FRAME_SHAPE
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Callable, TypeVar, Optional
|
|
from manimlib.scene.scene import Scene
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class Window(PygletWindow):
|
|
fullscreen: bool = False
|
|
resizable: bool = True
|
|
gl_version: tuple[int, int] = (3, 3)
|
|
vsync: bool = True
|
|
cursor: bool = True
|
|
|
|
def __init__(
|
|
self,
|
|
scene: Optional[Scene] = None,
|
|
position_string: str = "UR",
|
|
monitor_index: int = 1,
|
|
full_screen: bool = False,
|
|
size: Optional[tuple[int, int]] = None,
|
|
position: Optional[tuple[int, int]] = None,
|
|
samples: int = 0
|
|
):
|
|
self.scene = scene
|
|
self.monitor = self.get_monitor(monitor_index)
|
|
self.default_size = size or self.get_default_size(full_screen)
|
|
self.default_position = position or self.position_from_string(position_string)
|
|
|
|
super().__init__(samples=samples)
|
|
self.to_default_position()
|
|
|
|
self.pressed_keys = set()
|
|
|
|
|
|
if self.scene:
|
|
self.init_for_scene(scene)
|
|
|
|
def init_for_scene(self, scene: Scene):
|
|
"""
|
|
Resets the state and updates the scene associated to this window.
|
|
|
|
This is necessary when we want to reuse an *existing* window after a
|
|
`scene.reload()` was requested, which will create new scene instances.
|
|
"""
|
|
self.pressed_keys.clear()
|
|
self._has_undrawn_event = True
|
|
|
|
self.scene = scene
|
|
self.title = str(scene)
|
|
|
|
self.init_mgl_context()
|
|
|
|
self.timer = Timer()
|
|
self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer)
|
|
mglw.activate_context(window=self, ctx=self.ctx)
|
|
self.timer.start()
|
|
|
|
def get_monitor(self, index):
|
|
try:
|
|
monitors = screeninfo.get_monitors()
|
|
return monitors[min(index, len(monitors) - 1)]
|
|
except screeninfo.ScreenInfoError:
|
|
# Default fallback
|
|
return screeninfo.Monitor(width=1920, height=1080)
|
|
|
|
def get_default_size(self, full_screen=False):
|
|
width = self.monitor.width // (1 if full_screen else 2)
|
|
height = int(width // ASPECT_RATIO)
|
|
return (width, height)
|
|
|
|
def position_from_string(self, position_string):
|
|
# Alternatively, it might be specified with a string like
|
|
# UR, OO, DL, etc. specifying what corner it should go to
|
|
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
|
|
size = self.default_size
|
|
width_diff = self.monitor.width - size[0]
|
|
height_diff = self.monitor.height - size[1]
|
|
x_step = char_to_n[position_string[1]] * width_diff // 2
|
|
y_step = char_to_n[position_string[0]] * height_diff // 2
|
|
return (self.monitor.x + x_step, -self.monitor.y + y_step)
|
|
|
|
def focus(self):
|
|
"""
|
|
Puts focus on this window by hiding and showing it again.
|
|
|
|
Note that the pyglet `activate()` method didn't work as expected here,
|
|
so that's why we have to use this workaround. This will produce a small
|
|
flicker on the window but at least reliably focuses it. It may also
|
|
offset the window position slightly.
|
|
"""
|
|
self._window.set_visible(False)
|
|
self._window.set_visible(True)
|
|
|
|
def to_default_position(self):
|
|
self.position = self.default_position
|
|
# Hack. Sometimes, namely when configured to open in a separate window,
|
|
# the window needs to be resized to display correctly.
|
|
w, h = self.default_size
|
|
self.size = (w - 1, h - 1)
|
|
self.size = (w, h)
|
|
|
|
# Delegate event handling to scene
|
|
def pixel_coords_to_space_coords(
|
|
self,
|
|
px: int,
|
|
py: int,
|
|
relative: bool = False
|
|
) -> np.ndarray:
|
|
if self.scene is None or not hasattr(self.scene, "frame"):
|
|
return np.zeros(3)
|
|
|
|
pixel_shape = np.array(self.size)
|
|
fixed_frame_shape = np.array(FRAME_SHAPE)
|
|
frame = self.scene.frame
|
|
|
|
coords = np.zeros(3)
|
|
coords[:2] = (fixed_frame_shape / pixel_shape) * np.array([px, py])
|
|
if not relative:
|
|
coords[:2] -= 0.5 * fixed_frame_shape
|
|
return frame.from_fixed_frame_point(coords, relative)
|
|
|
|
def has_undrawn_event(self) -> bool:
|
|
return self._has_undrawn_event
|
|
|
|
def swap_buffers(self):
|
|
super().swap_buffers()
|
|
self._has_undrawn_event = False
|
|
|
|
@staticmethod
|
|
def note_undrawn_event(func: Callable[..., T]) -> Callable[..., T]:
|
|
@wraps(func)
|
|
def wrapper(self, *args, **kwargs):
|
|
func(self, *args, **kwargs)
|
|
self._has_undrawn_event = True
|
|
return wrapper
|
|
|
|
@note_undrawn_event
|
|
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
|
super().on_mouse_motion(x, y, dx, dy)
|
|
if not self.scene:
|
|
return
|
|
point = self.pixel_coords_to_space_coords(x, y)
|
|
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
|
self.scene.on_mouse_motion(point, d_point)
|
|
|
|
@note_undrawn_event
|
|
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
|
|
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
|
|
if not self.scene:
|
|
return
|
|
point = self.pixel_coords_to_space_coords(x, y)
|
|
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
|
self.scene.on_mouse_drag(point, d_point, buttons, modifiers)
|
|
|
|
@note_undrawn_event
|
|
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
|
|
super().on_mouse_press(x, y, button, mods)
|
|
if not self.scene:
|
|
return
|
|
point = self.pixel_coords_to_space_coords(x, y)
|
|
self.scene.on_mouse_press(point, button, mods)
|
|
|
|
@note_undrawn_event
|
|
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
|
|
super().on_mouse_release(x, y, button, mods)
|
|
if not self.scene:
|
|
return
|
|
point = self.pixel_coords_to_space_coords(x, y)
|
|
self.scene.on_mouse_release(point, button, mods)
|
|
|
|
@note_undrawn_event
|
|
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
|
|
super().on_mouse_scroll(x, y, x_offset, y_offset)
|
|
if not self.scene:
|
|
return
|
|
point = self.pixel_coords_to_space_coords(x, y)
|
|
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
|
|
self.scene.on_mouse_scroll(point, offset, x_offset, y_offset)
|
|
|
|
@note_undrawn_event
|
|
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
|
self.pressed_keys.add(symbol) # Modifiers?
|
|
super().on_key_press(symbol, modifiers)
|
|
if not self.scene:
|
|
return
|
|
self.scene.on_key_press(symbol, modifiers)
|
|
|
|
@note_undrawn_event
|
|
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
|
self.pressed_keys.difference_update({symbol}) # Modifiers?
|
|
super().on_key_release(symbol, modifiers)
|
|
if not self.scene:
|
|
return
|
|
self.scene.on_key_release(symbol, modifiers)
|
|
|
|
@note_undrawn_event
|
|
def on_resize(self, width: int, height: int) -> None:
|
|
super().on_resize(width, height)
|
|
if not self.scene:
|
|
return
|
|
self.scene.on_resize(width, height)
|
|
|
|
@note_undrawn_event
|
|
def on_show(self) -> None:
|
|
super().on_show()
|
|
if not self.scene:
|
|
return
|
|
self.scene.on_show()
|
|
|
|
@note_undrawn_event
|
|
def on_hide(self) -> None:
|
|
super().on_hide()
|
|
if not self.scene:
|
|
return
|
|
self.scene.on_hide()
|
|
|
|
@note_undrawn_event
|
|
def on_close(self) -> None:
|
|
super().on_close()
|
|
if not self.scene:
|
|
return
|
|
self.scene.on_close()
|
|
|
|
def is_key_pressed(self, symbol: int) -> bool:
|
|
return (symbol in self.pressed_keys)
|