Skip to content

Commit b00ab0e

Browse files
committed
Refactor drawing to use pyglet.shapes and improve macOS support
Replaces low-level pyglet.graphics drawing calls with pyglet.shapes primitives for lines, circles, and rectangles in graphics and widgets modules. Adds platform-specific handling for PyObjC dependencies and pyglet options for better compatibility on macOS, including PyPy support. Updates setup.py to install PyObjC only on CPython/macOS, and improves documentation for macOS and PyPy users.
1 parent 29c9f8b commit b00ab0e

File tree

5 files changed

+93
-49
lines changed

5 files changed

+93
-49
lines changed

README.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,20 @@ Pyperclip requires clipboard tools that might not come pre-installed.
2121
2222
sudo apt-get install xclip
2323
24-
Without them the app still works but pasting won’t.
24+
Without them the app still works but pasting won't.
25+
26+
Note for macOS
27+
~~~~~~~~~~~~~~
28+
29+
PyObjC is automatically installed on macOS systems for proper Cocoa/OpenGL integration with pyglet 2.0+. If you encounter OpenGL surface errors, ensure PyObjC is installed:
30+
31+
.. code:: sh
32+
33+
pip install pyobjc-core pyobjc-framework-Cocoa
34+
35+
This is handled automatically during normal installation.
36+
37+
**PyPy Users**: PyObjC doesn't support PyPy. TilingsGUI automatically configures pyglet to use shadow context disabled mode for PyPy compatibility on macOS.
2538

2639
Known issues
2740
------------

setup.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python
22
import os
3+
import sys
34

45
from setuptools import find_packages, setup
56

@@ -16,6 +17,18 @@ def get_version():
1617
raise ValueError("Version not found in tilingsgui/__init__.py")
1718

1819

20+
def get_install_requires():
21+
"""Get install requirements, including platform-specific dependencies."""
22+
base_requires = ["pyperclip>=1.9.0", "pyglet>=2.0.0", "tilings>=2.5.0"]
23+
24+
# Add macOS-specific dependencies for pyglet Cocoa integration
25+
# Only on CPython, not PyPy (PyObjC doesn't support PyPy)
26+
if sys.platform == "darwin" and sys.implementation.name == "cpython":
27+
base_requires.extend(["pyobjc-core", "pyobjc-framework-Cocoa"])
28+
29+
return base_requires
30+
31+
1932
setup(
2033
name="tilingsgui",
2134
version=get_version(),
@@ -31,7 +44,7 @@ def get_version():
3144
},
3245
packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
3346
long_description=read("README.rst"),
34-
install_requires=["pyperclip>=1.9.0", "pyglet>=1.5.15,<2.0", "tilings>=2.5.0"],
47+
install_requires=get_install_requires(),
3548
python_requires=">=3.8",
3649
include_package_data=True,
3750
classifiers=[

tilingsgui/app.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@
55

66
# pylint: disable=abstract-method
77

8+
import sys
89
from typing import ClassVar, Tuple
910

1011
import pyglet
1112

13+
# Configure pyglet for PyPy compatibility on macOS
14+
# PyPy doesn't support PyObjC, so we disable shadow context for high-res displays
15+
if sys.platform == "darwin" and sys.implementation.name == "pypy":
16+
pyglet.options["shadow_window"] = False
17+
18+
# pylint: disable=wrong-import-position
1219
from .files import History, PathManager
1320
from .graphics import Color
1421
from .menu import RightMenu, TopMenu
@@ -90,7 +97,7 @@ def _initial_config(self) -> None:
9097
"""Configuration done before starting."""
9198

9299
# Center the window within the os.
93-
screen = pyglet.canvas.Display().get_default_screen()
100+
screen = pyglet.display.Display().get_default_screen() # type: ignore
94101
self.set_location(
95102
(screen.width - self.width) // 2, (screen.height - self.height) // 2
96103
)
@@ -100,7 +107,7 @@ def _initial_config(self) -> None:
100107

101108
# Handle clearing the canvas on each draw.
102109
pyglet.gl.glClearColor(*TilingGui._CLEAR_COLOR)
103-
self.push_handlers(on_draw=self.clear)
110+
self.push_handlers(on_draw=self.clear) # pylint: disable=unreachable
104111

105112
def on_resize(self, width: int, height: int) -> bool:
106113
"""Event handler for the window resize event.

tilingsgui/graphics.py

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Drawable objects"""
22

3-
from math import cos, pi, sin
43
from typing import ClassVar, List, Tuple
54

65
import pyglet
6+
import pyglet.shapes
77

88
from .geometry import Point
99

@@ -32,12 +32,10 @@ def draw_line_segment(
3232
y2 (float): End y coordinate.
3333
color (Tuple[float, float, float]): RGB valued color.
3434
"""
35-
pyglet.graphics.draw(
36-
2,
37-
pyglet.gl.GL_LINE_STRIP,
38-
(GeoDrawer._VERTEX_MODE, [x1, y1, x2, y2]),
39-
(GeoDrawer._COLOR_MODE, color * 2),
40-
)
35+
# Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes
36+
color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255)
37+
line = pyglet.shapes.Line(x1, y1, x2, y2, color=color_255)
38+
line.draw()
4139

4240
@staticmethod
4341
def draw_circle(x: float, y: float, r: float, color: C3F, splits: int = 30) -> None:
@@ -51,17 +49,11 @@ def draw_circle(x: float, y: float, r: float, color: C3F, splits: int = 30) -> N
5149
splits (int, optional): How detailed the polygon emulating a circle should
5250
be. Higher values increase detail.
5351
"""
54-
vertices = [x, y]
55-
for i in range(splits + 1):
56-
ang = 2 * pi * i / splits
57-
vertices.append(x + cos(ang) * r)
58-
vertices.append(y + sin(ang) * r)
59-
pyglet.graphics.draw(
60-
splits + 2,
61-
pyglet.gl.GL_TRIANGLE_FAN,
62-
(GeoDrawer._VERTEX_MODE, vertices),
63-
(GeoDrawer._COLOR_MODE, color * (splits + 2)),
64-
)
52+
# Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes
53+
# Note: pyglet.shapes.Circle uses segments parameter for detail level
54+
color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255)
55+
circle = pyglet.shapes.Circle(x, y, r, color=color_255, segments=splits)
56+
circle.draw()
6557

6658
@staticmethod
6759
def draw_point(point: Point, size: float, color: C3F) -> None:
@@ -85,12 +77,10 @@ def draw_rectangle(x: float, y: float, w: float, h: float, color: C3F) -> None:
8577
h (float): Vertical length.
8678
color (Tuple[float, float, float]): Fill color.
8779
"""
88-
pyglet.graphics.draw(
89-
4,
90-
pyglet.gl.GL_TRIANGLE_STRIP,
91-
(GeoDrawer._VERTEX_MODE, [x, y, x, y + h, x + w, y, x + w, y + h]),
92-
(GeoDrawer._COLOR_MODE, color * 4),
93-
)
80+
# Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes
81+
color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255)
82+
rectangle = pyglet.shapes.Rectangle(x, y, w, h, color=color_255)
83+
rectangle.draw()
9484

9585
@staticmethod
9686
def draw_point_path(pnt_path: List[Point], color: C3F, point_size: float) -> None:
@@ -104,13 +94,18 @@ def draw_point_path(pnt_path: List[Point], color: C3F, point_size: float) -> Non
10494
n = len(pnt_path)
10595
if n > 0:
10696
if n > 1:
107-
vertices = [coord for pnt in pnt_path for coord in pnt.coords()]
108-
pyglet.graphics.draw(
109-
n,
110-
pyglet.gl.GL_LINE_STRIP,
111-
(GeoDrawer._VERTEX_MODE, vertices),
112-
(GeoDrawer._COLOR_MODE, color * n),
97+
# Draw line segments between adjacent points
98+
color_255 = (
99+
int(color[0] * 255),
100+
int(color[1] * 255),
101+
int(color[2] * 255),
102+
255,
113103
)
104+
for i in range(n - 1):
105+
p1, p2 = pnt_path[i], pnt_path[i + 1]
106+
line = pyglet.shapes.Line(p1.x, p1.y, p2.x, p2.y, color=color_255)
107+
line.draw()
108+
# Draw points
114109
for pnt in pnt_path:
115110
GeoDrawer.draw_point(pnt, point_size, color)
116111

tilingsgui/widgets.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ def __init__(self, init_text: str, font_size: int, color: RGBA) -> None:
3232
self._document.set_style(0, 0, {"font_size": font_size, "color": color})
3333
self._layout: pyglet.text.layout.IncrementalTextLayout = (
3434
pyglet.text.layout.IncrementalTextLayout(
35-
self._document, 0, 0, multiline=False, batch=self._batch
35+
self._document,
36+
x=0,
37+
y=0,
38+
width=100,
39+
height=20, # Will be updated in position()
40+
multiline=False,
41+
batch=self._batch,
3642
)
3743
)
3844
self._caret: pyglet.text.caret.Caret = pyglet.text.caret.Caret(self._layout)
@@ -47,10 +53,10 @@ def position(self, x: float, y: float, w: float, h: float) -> None:
4753
w (float): The horizontal length of the component.
4854
h (float): The vertical length of the component.
4955
"""
50-
self._layout.x = x + Text._LEFT_PAD
51-
self._layout.y = y
52-
self._layout.width = w - Text._LEFT_PAD
53-
self._layout.height = h
56+
self._layout.x = int(x + Text._LEFT_PAD)
57+
self._layout.y = int(y)
58+
self._layout.width = int(w - Text._LEFT_PAD)
59+
self._layout.height = int(h)
5460

5561
def set_focus(self) -> None:
5662
"""Set focus on the input text. This is needed to write to it."""
@@ -118,12 +124,20 @@ def __init__(
118124
box_color (Tuple[float, float, float]): The rgb color of the box.
119125
"""
120126
super().__init__(init_text, font_size, text_color)
121-
self._vertex_list: pyglet.graphics.vertexdomain.VertexList = self._batch.add(
122-
4,
123-
pyglet.gl.GL_QUADS,
124-
None,
125-
("v2f", [0] * 8),
126-
("c3B", box_color * 4),
127+
# Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes
128+
box_color_255 = (
129+
int(box_color[0] * 255),
130+
int(box_color[1] * 255),
131+
int(box_color[2] * 255),
132+
255,
133+
)
134+
self._rectangle = pyglet.shapes.Rectangle(
135+
x=0,
136+
y=0,
137+
width=100,
138+
height=20, # Will be updated in position()
139+
color=box_color_255,
140+
batch=self._batch,
127141
)
128142

129143
def position(self, x: float, y: float, w: float, h: float) -> None:
@@ -136,8 +150,10 @@ def position(self, x: float, y: float, w: float, h: float) -> None:
136150
h (float): The vertical length of the component.
137151
"""
138152
super().position(x, y, w, h)
139-
for i, vertex in enumerate((x, y, x + w, y, x + w, y + h, x, y + h)):
140-
self._vertex_list.vertices[i] = vertex
153+
self._rectangle.x = x
154+
self._rectangle.y = y
155+
self._rectangle.width = w
156+
self._rectangle.height = h
141157

142158
def hit_test(self, x: float, y: float) -> bool:
143159
"""Is the point (x,y) inside the rectangle that the text box forms.
@@ -150,8 +166,8 @@ def hit_test(self, x: float, y: float) -> bool:
150166
[type]: True iff inside.
151167
"""
152168
return (
153-
self._vertex_list.vertices[0] < x < self._vertex_list.vertices[2]
154-
and self._vertex_list.vertices[1] < y < self._vertex_list.vertices[5]
169+
self._rectangle.x < x < self._rectangle.x + self._rectangle.width
170+
and self._rectangle.y < y < self._rectangle.y + self._rectangle.height
155171
)
156172

157173

0 commit comments

Comments
 (0)