minilibx-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: minilibx-python
3
+ Version: 0.1.0
4
+ Summary: Python wrapper for MiniLibX — the 42 school graphics library
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/yourname/pymlx
7
+ Project-URL: Repository, https://github.com/yourname/pymlx
8
+ Keywords: minilibx,42school,graphics,x11
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Topic :: Multimedia :: Graphics
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+
16
+ # pymlx
17
+
18
+ A Python wrapper for [MiniLibX](https://github.com/42Paris/minilibx-linux) — the simple X11 graphics library used at 42 school.
19
+
20
+ Lets you open windows, draw shapes, and handle keyboard/mouse events in pure Python.
21
+
22
+ ```python
23
+ from pymlx import Mlx, Color
24
+
25
+ with Mlx() as mlx:
26
+ win = mlx.new_window(800, 600, "Hello pymlx")
27
+ img = mlx.new_image(800, 600)
28
+
29
+ img.draw_circle(400, 300, 100, Color.RED)
30
+ img.draw_rect(50, 50, 200, 100, Color.BLUE)
31
+ img.draw_line(0, 0, 800, 600, Color.WHITE)
32
+
33
+ win.put_image(img)
34
+ win.on_close(mlx.loop_end)
35
+ win.on_key(lambda key: mlx.loop_end() if key == 65307 else None)
36
+
37
+ mlx.loop()
38
+ ```
39
+
40
+ ## Requirements
41
+
42
+ - Linux with X11
43
+ - Python 3.10+
44
+ - `gcc`, `make`
45
+ - `libX11-dev`, `libXext-dev`
46
+
47
+ ```bash
48
+ sudo apt install gcc make libx11-dev libxext-dev
49
+ ```
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ git clone https://github.com/yourname/pymlx.git
55
+ cd pymlx
56
+
57
+ # Build the C wrapper
58
+ make -C src/pymlx MLX_DIR=src/pymlx/lib/minilibx-linux
59
+
60
+ # Install the package
61
+ pip install -e .
62
+ ```
63
+
64
+ ## API
65
+
66
+ ### `Mlx`
67
+
68
+ Main context. Use as a context manager.
69
+
70
+ ```python
71
+ mlx = Mlx()
72
+ mlx.new_window(width, height, title) -> Window
73
+ mlx.new_image(width, height) -> Image
74
+ mlx.destroy_image(image)
75
+ mlx.on_loop(callback) # callback() called every frame
76
+ mlx.loop() # start event loop (blocking)
77
+ mlx.loop_end() # stop event loop
78
+ mlx.destroy()
79
+ ```
80
+
81
+ ### `Window`
82
+
83
+ ```python
84
+ win.put_image(image, x=0, y=0) # blit image onto window
85
+ win.pixel_put(x, y, color) # direct pixel (slow)
86
+ win.string_put(x, y, color, text) # draw text
87
+
88
+ win.on_key(callback) # callback(keycode: int)
89
+ win.on_mouse(callback) # callback(button, x, y)
90
+ win.on_close(callback) # called on window close
91
+ win.on_event(event, mask, callback) # raw X11 event
92
+
93
+ win.mouse_hide()
94
+ win.mouse_show()
95
+ win.mouse_pos() -> (x, y)
96
+ win.mouse_move(x, y)
97
+ win.destroy()
98
+ ```
99
+
100
+ ### `Image`
101
+
102
+ All drawing methods write directly into the pixel buffer — fast enough for real-time rendering.
103
+
104
+ ```python
105
+ # Pixels
106
+ img.pixel_put(x, y, color)
107
+ img.pixel_get(x, y) -> int
108
+ img.clear(color)
109
+
110
+ # Rectangles
111
+ img.draw_rect(x, y, w, h, color)
112
+ img.draw_rect_outline(x, y, w, h, color, thickness=1)
113
+
114
+ # Lines
115
+ img.draw_line(x0, y0, x1, y1, color)
116
+ img.draw_line_thick(x0, y0, x1, y1, color, thickness=2)
117
+
118
+ # Circles
119
+ img.draw_circle(cx, cy, radius, color)
120
+ img.draw_circle_outline(cx, cy, radius, color, thickness=1)
121
+
122
+ # Triangles
123
+ img.draw_triangle(x0, y0, x1, y1, x2, y2, color)
124
+ img.draw_triangle_outline(x0, y0, x1, y1, x2, y2, color)
125
+
126
+ # Gradients
127
+ img.draw_gradient_h(x, y, w, h, color_left, color_right)
128
+ img.draw_gradient_v(x, y, w, h, color_top, color_bottom)
129
+
130
+ # Polygon outline
131
+ img.draw_polygon([(x, y), ...], color)
132
+
133
+ # Flood fill
134
+ img.flood_fill(x, y, color)
135
+ ```
136
+
137
+ ### `Color`
138
+
139
+ ```python
140
+ Color.BLACK, Color.WHITE, Color.RED, Color.GREEN
141
+ Color.BLUE, Color.YELLOW, Color.CYAN, Color.MAGENTA
142
+ Color.GRAY, Color.ORANGE, Color.PINK
143
+
144
+ Color.rgb(r, g, b) -> int # 0–255 each
145
+ Color.to_rgb(color) -> (r, g, b)
146
+ Color.lerp(c1, c2, t) -> int # t in [0.0, 1.0]
147
+ ```
148
+
149
+ ### `X11Event`
150
+
151
+ ```python
152
+ X11Event.KEY_PRESS # 2
153
+ X11Event.KEY_RELEASE # 3
154
+ X11Event.BUTTON_PRESS # 4
155
+ X11Event.BUTTON_RELEASE # 5
156
+ X11Event.MOTION_NOTIFY # 6
157
+ X11Event.DESTROY_NOTIFY # 17
158
+ ```
159
+
160
+ ## Example — animated gradient
161
+
162
+ ```python
163
+ from pymlx import Mlx, Color
164
+
165
+ W, H = 800, 600
166
+ frame = 0
167
+
168
+ with Mlx() as mlx:
169
+ win = mlx.new_window(W, H, "Gradient")
170
+ img = mlx.new_image(W, H)
171
+
172
+ def render():
173
+ global frame
174
+ t = (frame % 256) / 255
175
+ img.draw_gradient_h(0, 0, W, H,
176
+ Color.lerp(Color.BLUE, Color.RED, t),
177
+ Color.lerp(Color.RED, Color.BLUE, t))
178
+ win.put_image(img)
179
+ frame += 1
180
+
181
+ win.on_close(mlx.loop_end)
182
+ win.on_key(lambda k: mlx.loop_end() if k == 65307 else None)
183
+ mlx.on_loop(render)
184
+ mlx.loop()
185
+ ```
186
+
187
+ ## License
188
+
189
+ MIT
@@ -0,0 +1,7 @@
1
+ pymlx/__init__.py,sha256=fMafE7KmlVWkiPB3ngoh_G-P_pW6sOFEWQrkep2yCSs,113
2
+ pymlx/core.py,sha256=HTylN7jOviBCWM-Zi32thuBuvXnYWEcDWaz19dliqbw,22942
3
+ pymlx/lib/libmlx_wrapper.so,sha256=zTvHOhV_6Ay9pCU4L2_s9QkLLGxUzNi7J5AgR4uCa9w,39008
4
+ minilibx_python-0.1.0.dist-info/METADATA,sha256=dCBOmT3Mod1jLIBOoplwSZTWeUgjQ4OVP4aU37MWtTk,4479
5
+ minilibx_python-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ minilibx_python-0.1.0.dist-info/top_level.txt,sha256=q73JJdYIdIW82JnQHmMqAq3oZ2gy8VDsSjGoCpG_S0A,6
7
+ minilibx_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pymlx
pymlx/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .core import Mlx, Window, Image, Color, X11Event
2
+
3
+ __all__ = ["Mlx", "Window", "Image", "Color", "X11Event"]
pymlx/core.py ADDED
@@ -0,0 +1,502 @@
1
+ """
2
+ core.py — Python ctypes wrapper around libmlx_wrapper.so
3
+ """
4
+
5
+ import ctypes
6
+ import math
7
+ from pathlib import Path
8
+
9
+ # ─── Load shared library ──────────────────────────────────────────────────────
10
+ _LIB_PATH = Path(__file__).parent / "lib" / "libmlx_wrapper.so"
11
+ try:
12
+ _lib = ctypes.CDLL(str(_LIB_PATH))
13
+ except OSError as e:
14
+ raise RuntimeError(
15
+ f"Cannot load libmlx_wrapper.so: {e}\n"
16
+ "Run `make` in the wrapper directory first."
17
+ )
18
+
19
+ # ─── C struct: t_img_info ─────────────────────────────────────────────────────
20
+ class _ImgInfo(ctypes.Structure):
21
+ _fields_ = [
22
+ ("img", ctypes.c_void_p),
23
+ ("addr", ctypes.c_char_p),
24
+ ("bits_per_pixel", ctypes.c_int),
25
+ ("line_length", ctypes.c_int),
26
+ ("endian", ctypes.c_int),
27
+ ]
28
+
29
+ # ─── Callback types ───────────────────────────────────────────────────────────
30
+ KeyCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_void_p)
31
+ MouseCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int,
32
+ ctypes.c_int, ctypes.c_int, ctypes.c_void_p)
33
+ LoopCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)
34
+ GenericCb = ctypes.CFUNCTYPE(ctypes.c_int)
35
+
36
+ # ─── Prototypes ───────────────────────────────────────────────────────────────
37
+ def _proto(name, restype, *argtypes):
38
+ fn = getattr(_lib, name)
39
+ fn.restype = restype
40
+ fn.argtypes = list(argtypes)
41
+ return fn
42
+
43
+ _mlx_init = _proto("wrap_mlx_init", ctypes.c_void_p)
44
+ _mlx_new_window = _proto("wrap_mlx_new_window", ctypes.c_void_p,
45
+ ctypes.c_void_p, ctypes.c_int, ctypes.c_int,
46
+ ctypes.c_char_p)
47
+ _mlx_destroy_window = _proto("wrap_mlx_destroy_window", None,
48
+ ctypes.c_void_p, ctypes.c_void_p)
49
+ _mlx_destroy_display = _proto("wrap_mlx_destroy_display", None,
50
+ ctypes.c_void_p)
51
+ _mlx_new_image = _proto("wrap_mlx_new_image", _ImgInfo,
52
+ ctypes.c_void_p, ctypes.c_int, ctypes.c_int)
53
+ _mlx_destroy_image = _proto("wrap_mlx_destroy_image", None,
54
+ ctypes.c_void_p, ctypes.c_void_p)
55
+ _mlx_put_image_to_win = _proto("wrap_mlx_put_image_to_window", None,
56
+ ctypes.c_void_p, ctypes.c_void_p,
57
+ ctypes.c_void_p, ctypes.c_int, ctypes.c_int)
58
+ _img_pixel_put = _proto("wrap_img_pixel_put", None,
59
+ ctypes.POINTER(_ImgInfo),
60
+ ctypes.c_int, ctypes.c_int, ctypes.c_int)
61
+ _img_pixel_get = _proto("wrap_img_pixel_get", ctypes.c_int,
62
+ ctypes.POINTER(_ImgInfo),
63
+ ctypes.c_int, ctypes.c_int)
64
+ _img_clear = _proto("wrap_img_clear", None,
65
+ ctypes.POINTER(_ImgInfo), ctypes.c_int)
66
+ _mlx_pixel_put = _proto("wrap_mlx_pixel_put", None,
67
+ ctypes.c_void_p, ctypes.c_void_p,
68
+ ctypes.c_int, ctypes.c_int, ctypes.c_int)
69
+ _mlx_string_put = _proto("wrap_mlx_string_put", None,
70
+ ctypes.c_void_p, ctypes.c_void_p,
71
+ ctypes.c_int, ctypes.c_int, ctypes.c_int,
72
+ ctypes.c_char_p)
73
+ _mlx_key_hook = _proto("wrap_mlx_key_hook", None,
74
+ ctypes.c_void_p, KeyCb, ctypes.c_void_p)
75
+ _mlx_mouse_hook = _proto("wrap_mlx_mouse_hook", None,
76
+ ctypes.c_void_p, MouseCb, ctypes.c_void_p)
77
+ _mlx_hook = _proto("wrap_mlx_hook", None,
78
+ ctypes.c_void_p, ctypes.c_int, ctypes.c_int,
79
+ GenericCb, ctypes.c_void_p)
80
+ _mlx_loop_hook = _proto("wrap_mlx_loop_hook", None,
81
+ ctypes.c_void_p, LoopCb, ctypes.c_void_p)
82
+ _mlx_loop = _proto("wrap_mlx_loop", None,
83
+ ctypes.c_void_p)
84
+ _mlx_loop_end = _proto("wrap_mlx_loop_end", None,
85
+ ctypes.c_void_p)
86
+ _mlx_mouse_hide = _proto("wrap_mlx_mouse_hide", ctypes.c_int,
87
+ ctypes.c_void_p, ctypes.c_void_p)
88
+ _mlx_mouse_show = _proto("wrap_mlx_mouse_show", ctypes.c_int,
89
+ ctypes.c_void_p, ctypes.c_void_p)
90
+ _mlx_mouse_get_pos = _proto("wrap_mlx_mouse_get_pos", ctypes.c_int,
91
+ ctypes.c_void_p, ctypes.c_void_p,
92
+ ctypes.POINTER(ctypes.c_int),
93
+ ctypes.POINTER(ctypes.c_int))
94
+ _mlx_mouse_move = _proto("wrap_mlx_mouse_move", ctypes.c_int,
95
+ ctypes.c_void_p, ctypes.c_void_p,
96
+ ctypes.c_int, ctypes.c_int)
97
+
98
+
99
+ # ─── Color ────────────────────────────────────────────────────────────────────
100
+ class Color:
101
+ """Color constants and helpers (0x00RRGGBB)."""
102
+ BLACK = 0x000000
103
+ WHITE = 0xFFFFFF
104
+ RED = 0xFF0000
105
+ GREEN = 0x00FF00
106
+ BLUE = 0x0000FF
107
+ YELLOW = 0xFFFF00
108
+ CYAN = 0x00FFFF
109
+ MAGENTA = 0xFF00FF
110
+ GRAY = 0x808080
111
+ ORANGE = 0xFF8000
112
+ PINK = 0xFF69B4
113
+
114
+ @staticmethod
115
+ def rgb(r: int, g: int, b: int) -> int:
116
+ """Create color from R, G, B components (0–255 each)."""
117
+ return ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)
118
+
119
+ @staticmethod
120
+ def to_rgb(color: int) -> tuple[int, int, int]:
121
+ """Split color int into (r, g, b) tuple."""
122
+ return ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF)
123
+
124
+ @staticmethod
125
+ def lerp(c1: int, c2: int, t: float) -> int:
126
+ """Linear interpolation between two colors. t in [0.0, 1.0]."""
127
+ r1, g1, b1 = Color.to_rgb(c1)
128
+ r2, g2, b2 = Color.to_rgb(c2)
129
+ return Color.rgb(
130
+ int(r1 + (r2 - r1) * t),
131
+ int(g1 + (g2 - g1) * t),
132
+ int(b1 + (b2 - b1) * t),
133
+ )
134
+
135
+
136
+ # ─── X11 Event codes ──────────────────────────────────────────────────────────
137
+ class X11Event:
138
+ """Common X11 event codes for mlx_hook."""
139
+ KEY_PRESS = 2
140
+ KEY_RELEASE = 3
141
+ BUTTON_PRESS = 4
142
+ BUTTON_RELEASE = 5
143
+ MOTION_NOTIFY = 6
144
+ DESTROY_NOTIFY = 17
145
+
146
+
147
+ # ─── Image ────────────────────────────────────────────────────────────────────
148
+ class Image:
149
+ """
150
+ MLX image with direct pixel buffer access and drawing primitives.
151
+
152
+ All drawing methods write directly into the pixel buffer —
153
+ no X11 roundtrip, so they are fast enough for real-time rendering.
154
+ Call window.put_image(img) to display the result.
155
+ """
156
+
157
+ def __init__(self, info: _ImgInfo, width: int, height: int):
158
+ self._info = info
159
+ self._pinfo = ctypes.pointer(info)
160
+ self.width = width
161
+ self.height = height
162
+
163
+ @property
164
+ def handle(self) -> ctypes.c_void_p:
165
+ return self._info.img
166
+
167
+ # ── Low-level ─────────────────────────────────────────────────────────────
168
+
169
+ def pixel_put(self, x: int, y: int, color: int) -> None:
170
+ """Write a single pixel. Out-of-bounds coords are silently ignored."""
171
+ if 0 <= x < self.width and 0 <= y < self.height:
172
+ _img_pixel_put(self._pinfo, x, y, color)
173
+
174
+ def pixel_get(self, x: int, y: int) -> int:
175
+ """Read a single pixel color."""
176
+ if 0 <= x < self.width and 0 <= y < self.height:
177
+ return _img_pixel_get(self._pinfo, x, y)
178
+ return 0
179
+
180
+ def clear(self, color: int = Color.BLACK) -> None:
181
+ """Fill the entire image with one color."""
182
+ _img_clear(self._pinfo, color)
183
+
184
+ # ── Rectangles ────────────────────────────────────────────────────────────
185
+
186
+ def draw_rect(self, x: int, y: int, w: int, h: int, color: int) -> None:
187
+ """Draw a filled rectangle."""
188
+ x0 = max(x, 0); y0 = max(y, 0)
189
+ x1 = min(x + w, self.width)
190
+ y1 = min(y + h, self.height)
191
+ for py in range(y0, y1):
192
+ for px in range(x0, x1):
193
+ _img_pixel_put(self._pinfo, px, py, color)
194
+
195
+ def draw_rect_outline(self, x: int, y: int, w: int, h: int,
196
+ color: int, thickness: int = 1) -> None:
197
+ """Draw a rectangle outline (not filled)."""
198
+ for t in range(thickness):
199
+ self.draw_line(x + t, y + t, x + w - t, y + t, color)
200
+ self.draw_line(x + w - t, y + t, x + w - t, y + h - t, color)
201
+ self.draw_line(x + w - t, y + h - t, x + t, y + h - t, color)
202
+ self.draw_line(x + t, y + h - t, x + t, y + t, color)
203
+
204
+ # ── Lines ─────────────────────────────────────────────────────────────────
205
+
206
+ def draw_line(self, x0: int, y0: int, x1: int, y1: int,
207
+ color: int) -> None:
208
+ """Draw a line using Bresenham's algorithm."""
209
+ dx = abs(x1 - x0); sx = 1 if x0 < x1 else -1
210
+ dy = -abs(y1 - y0); sy = 1 if y0 < y1 else -1
211
+ err = dx + dy
212
+ while True:
213
+ self.pixel_put(x0, y0, color)
214
+ if x0 == x1 and y0 == y1:
215
+ break
216
+ e2 = 2 * err
217
+ if e2 >= dy:
218
+ if x0 == x1:
219
+ break
220
+ err += dy
221
+ x0 += sx
222
+ if e2 <= dx:
223
+ if y0 == y1:
224
+ break
225
+ err += dx
226
+ y0 += sy
227
+
228
+ def draw_line_thick(self, x0: int, y0: int, x1: int, y1: int,
229
+ color: int, thickness: int = 2) -> None:
230
+ """Draw a thick line by drawing multiple offset lines."""
231
+ half = thickness // 2
232
+ dx = x1 - x0; dy = y1 - y0
233
+ length = math.hypot(dx, dy)
234
+ if length == 0:
235
+ self.draw_rect(x0 - half, y0 - half, thickness, thickness, color)
236
+ return
237
+ nx = -dy / length # normal vector
238
+ ny = dx / length
239
+ for i in range(-half, half + 1):
240
+ ox = round(nx * i); oy = round(ny * i)
241
+ self.draw_line(x0 + ox, y0 + oy, x1 + ox, y1 + oy, color)
242
+
243
+ # ── Circles ───────────────────────────────────────────────────────────────
244
+
245
+ def draw_circle(self, cx: int, cy: int, radius: int,
246
+ color: int) -> None:
247
+ """Draw a filled circle using midpoint algorithm."""
248
+ for y in range(-radius, radius + 1):
249
+ x_max = int(math.sqrt(radius * radius - y * y))
250
+ for x in range(-x_max, x_max + 1):
251
+ self.pixel_put(cx + x, cy + y, color)
252
+
253
+ def draw_circle_outline(self, cx: int, cy: int, radius: int,
254
+ color: int, thickness: int = 1) -> None:
255
+ """Draw a circle outline."""
256
+ for r in range(radius, radius - thickness, -1):
257
+ if r < 0:
258
+ break
259
+ x = 0; y = r; d = 1 - r
260
+ while x <= y:
261
+ for px, py in [
262
+ (cx+x, cy+y), (cx-x, cy+y), (cx+x, cy-y), (cx-x, cy-y),
263
+ (cx+y, cy+x), (cx-y, cy+x), (cx+y, cy-x), (cx-y, cy-x),
264
+ ]:
265
+ self.pixel_put(px, py, color)
266
+ x += 1
267
+ if d < 0:
268
+ d += 2 * x + 1
269
+ else:
270
+ y -= 1; d += 2 * (x - y) + 1
271
+
272
+ # ── Triangles ─────────────────────────────────────────────────────────────
273
+
274
+ def draw_triangle(self, x0: int, y0: int, x1: int, y1: int,
275
+ x2: int, y2: int, color: int) -> None:
276
+ """Draw a filled triangle."""
277
+ # Sort by y
278
+ pts = sorted([(x0, y0), (x1, y1), (x2, y2)], key=lambda p: p[1])
279
+ (ax, ay), (bx, by), (cx, cy) = pts
280
+
281
+ def scanline(y_start, y_end, xa, xb, x_long_start, dx_long, dx_short):
282
+ xl = x_long_start
283
+ xs = xa
284
+ for y in range(y_start, y_end):
285
+ left = min(round(xl), round(xs))
286
+ right = max(round(xl), round(xs))
287
+ for x in range(left, right + 1):
288
+ self.pixel_put(x, y, color)
289
+ xl += dx_long
290
+ xs += dx_short
291
+
292
+ total_h = cy - ay or 1
293
+ # Top half
294
+ if by != ay:
295
+ dx_long = (cx - ax) / total_h
296
+ dx_short = (bx - ax) / (by - ay)
297
+ scanline(ay, by, ax, ax, ax, dx_long, dx_short)
298
+ # Bottom half
299
+ if cy != by:
300
+ dx_long = (cx - ax) / total_h
301
+ dx_short = (cx - bx) / (cy - by)
302
+ x_long_at_b = ax + (cx - ax) * (by - ay) / total_h
303
+ scanline(by, cy, bx, x_long_at_b, x_long_at_b, dx_long, dx_short)
304
+
305
+ def draw_triangle_outline(self, x0: int, y0: int, x1: int, y1: int,
306
+ x2: int, y2: int, color: int) -> None:
307
+ """Draw a triangle outline."""
308
+ self.draw_line(x0, y0, x1, y1, color)
309
+ self.draw_line(x1, y1, x2, y2, color)
310
+ self.draw_line(x2, y2, x0, y0, color)
311
+
312
+ # ── Gradients ─────────────────────────────────────────────────────────────
313
+
314
+ def draw_gradient_h(self, x: int, y: int, w: int, h: int,
315
+ color_left: int, color_right: int) -> None:
316
+ """Horizontal gradient rectangle."""
317
+ for px in range(w):
318
+ t = px / (w - 1) if w > 1 else 0
319
+ c = Color.lerp(color_left, color_right, t)
320
+ for py in range(h):
321
+ self.pixel_put(x + px, y + py, c)
322
+
323
+ def draw_gradient_v(self, x: int, y: int, w: int, h: int,
324
+ color_top: int, color_bottom: int) -> None:
325
+ """Vertical gradient rectangle."""
326
+ for py in range(h):
327
+ t = py / (h - 1) if h > 1 else 0
328
+ c = Color.lerp(color_top, color_bottom, t)
329
+ for px in range(w):
330
+ self.pixel_put(x + px, y + py, c)
331
+
332
+ # ── Polygon ───────────────────────────────────────────────────────────────
333
+
334
+ def draw_polygon(self, points: list[tuple[int, int]],
335
+ color: int) -> None:
336
+ """Draw a polygon outline from a list of (x, y) points."""
337
+ n = len(points)
338
+ if n < 2:
339
+ return
340
+ for i in range(n):
341
+ x0, y0 = points[i]
342
+ x1, y1 = points[(i + 1) % n]
343
+ self.draw_line(x0, y0, x1, y1, color)
344
+
345
+ # ── Flood fill ────────────────────────────────────────────────────────────
346
+
347
+ def flood_fill(self, x: int, y: int, color: int) -> None:
348
+ """
349
+ Flood fill starting at (x, y).
350
+ Replaces all connected pixels of the same color with the new color.
351
+ Uses an iterative stack to avoid Python recursion limit.
352
+ """
353
+ if not (0 <= x < self.width and 0 <= y < self.height):
354
+ return
355
+ target = self.pixel_get(x, y)
356
+ if target == color:
357
+ return
358
+ stack = [(x, y)]
359
+ while stack:
360
+ px, py = stack.pop()
361
+ if not (0 <= px < self.width and 0 <= py < self.height):
362
+ continue
363
+ if self.pixel_get(px, py) != target:
364
+ continue
365
+ self.pixel_put(px, py, color)
366
+ stack.extend([(px+1, py), (px-1, py), (px, py+1), (px, py-1)])
367
+
368
+
369
+ # ─── Window ───────────────────────────────────────────────────────────────────
370
+ class Window:
371
+ """An MLX window."""
372
+
373
+ def __init__(self, mlx_ptr, win_ptr, width: int, height: int):
374
+ self._mlx = mlx_ptr
375
+ self._win = win_ptr
376
+ self.width = width
377
+ self.height = height
378
+ self._callbacks: list = []
379
+
380
+ def pixel_put(self, x: int, y: int, color: int) -> None:
381
+ """Direct pixel write. Slow — prefer Image + put_image for animation."""
382
+ _mlx_pixel_put(self._mlx, self._win, x, y, color)
383
+
384
+ def string_put(self, x: int, y: int, color: int, text: str) -> None:
385
+ """Draw text directly on the window using the built-in mlx font."""
386
+ _mlx_string_put(self._mlx, self._win, x, y, color, text.encode())
387
+
388
+ def put_image(self, image: Image, x: int = 0, y: int = 0) -> None:
389
+ """Blit an Image onto the window at position (x, y)."""
390
+ _mlx_put_image_to_win(self._mlx, self._win, image.handle, x, y)
391
+
392
+ # ── Hooks ─────────────────────────────────────────────────────────────────
393
+
394
+ def on_key(self, callback) -> None:
395
+ """Register key press handler: callback(keycode: int) -> None."""
396
+ def _cb(keycode, _param):
397
+ callback(keycode)
398
+ return 0
399
+ c_cb = KeyCb(_cb)
400
+ self._callbacks.append(c_cb)
401
+ _mlx_key_hook(self._win, c_cb, None)
402
+
403
+ def on_mouse(self, callback) -> None:
404
+ """Register mouse button handler: callback(button, x, y) -> None."""
405
+ def _cb(button, x, y, _param):
406
+ callback(button, x, y)
407
+ return 0
408
+ c_cb = MouseCb(_cb)
409
+ self._callbacks.append(c_cb)
410
+ _mlx_mouse_hook(self._win, c_cb, None)
411
+
412
+ def on_event(self, event: int, mask: int, callback) -> None:
413
+ """Register a generic X11 event hook: callback() -> None."""
414
+ def _cb():
415
+ callback()
416
+ return 0
417
+ c_cb = GenericCb(_cb)
418
+ self._callbacks.append(c_cb)
419
+ _mlx_hook(self._win, event, mask, c_cb, None)
420
+
421
+ def on_close(self, callback) -> None:
422
+ """Called when the window close button is pressed."""
423
+ self.on_event(X11Event.DESTROY_NOTIFY, 0, callback)
424
+
425
+ # ── Mouse control ─────────────────────────────────────────────────────────
426
+
427
+ def mouse_hide(self) -> None:
428
+ _mlx_mouse_hide(self._mlx, self._win)
429
+
430
+ def mouse_show(self) -> None:
431
+ _mlx_mouse_show(self._mlx, self._win)
432
+
433
+ def mouse_pos(self) -> tuple[int, int]:
434
+ x, y = ctypes.c_int(), ctypes.c_int()
435
+ _mlx_mouse_get_pos(self._mlx, self._win,
436
+ ctypes.byref(x), ctypes.byref(y))
437
+ return x.value, y.value
438
+
439
+ def mouse_move(self, x: int, y: int) -> None:
440
+ _mlx_mouse_move(self._mlx, self._win, x, y)
441
+
442
+ def destroy(self) -> None:
443
+ _mlx_destroy_window(self._mlx, self._win)
444
+
445
+
446
+ # ─── Mlx ──────────────────────────────────────────────────────────────────────
447
+ class Mlx:
448
+ """
449
+ Main MiniLibX context. Use as a context manager for automatic cleanup:
450
+
451
+ with Mlx() as mlx:
452
+ win = mlx.new_window(800, 600, "Title")
453
+ ...
454
+ mlx.loop()
455
+ """
456
+
457
+ def __init__(self):
458
+ self._ptr = _mlx_init()
459
+ if not self._ptr:
460
+ raise RuntimeError("mlx_init() failed — is DISPLAY set?")
461
+ self._callbacks: list = []
462
+
463
+ def new_window(self, width: int, height: int, title: str) -> Window:
464
+ ptr = _mlx_new_window(self._ptr, width, height, title.encode())
465
+ if not ptr:
466
+ raise RuntimeError(f"mlx_new_window() failed for '{title}'")
467
+ return Window(self._ptr, ptr, width, height)
468
+
469
+ def new_image(self, width: int, height: int) -> Image:
470
+ info = _mlx_new_image(self._ptr, width, height)
471
+ if not info.img:
472
+ raise RuntimeError("mlx_new_image() failed")
473
+ return Image(info, width, height)
474
+
475
+ def destroy_image(self, image: Image) -> None:
476
+ _mlx_destroy_image(self._ptr, image.handle)
477
+
478
+ def on_loop(self, callback) -> None:
479
+ """Register a per-frame callback: callback() -> None."""
480
+ def _cb(_param):
481
+ callback()
482
+ return 0
483
+ c_cb = LoopCb(_cb)
484
+ self._callbacks.append(c_cb)
485
+ _mlx_loop_hook(self._ptr, c_cb, None)
486
+
487
+ def loop(self) -> None:
488
+ """Start the event loop (blocking)."""
489
+ _mlx_loop(self._ptr)
490
+
491
+ def loop_end(self) -> None:
492
+ """Stop the event loop."""
493
+ _mlx_loop_end(self._ptr)
494
+
495
+ def destroy(self) -> None:
496
+ _mlx_destroy_display(self._ptr)
497
+
498
+ def __enter__(self):
499
+ return self
500
+
501
+ def __exit__(self, *_):
502
+ self.destroy()
Binary file