doodle-ui 1.0.9__tar.gz

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,2 @@
1
+ include src/*.h
2
+ include src/*.c
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: doodle-ui
3
+ Version: 1.0.9
4
+ Summary: Hardware-accelerated DOM-based UI & 2D Game Engine
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Doodle Engine 🎨🚀
9
+ ### Hardware-Accelerated Hybrid C-Python UI & 2D Game Engine
10
+
11
+ Doodle is a high-performance, lightweight UI and 2D game engine combining a **native C rendering core (powered by Raylib)** with a **highly reactive Python scripting layer**.
12
+
13
+ By bypassing heavy browser runtimes (like Chromium/Electron), Doodle parses dynamic HTML-like templates and CSS stylesheets in a single pass directly to GPU-accelerated graphics, procedural audio synths, and collision meshes.
14
+
15
+ ---
16
+
17
+ ## 📂 Repository Organization
18
+
19
+ ```text
20
+ Doodle/
21
+ ├── docs/ # Full reference specifications & requirements
22
+ │ ├── cheatsheet.md # API & layout syntax quick reference
23
+ │ ├── requirements.md # Engine specifications and architecture blueprint
24
+ │ └── unused_reference.md # Unused HTML/CSS/C engine features reference
25
+ ├── doodle/ # Python package wrapper
26
+ │ ├── __init__.py # Tween animations, reactive state binding, event emitter
27
+ │ └── cli.py # PyInstaller packager cli
28
+ ├── examples/ # Game demonstrations
29
+ │ └── breakout/ # Breakout game demo
30
+ │ ├── layout.html # Game view DOM nodes & inline handlers
31
+ │ ├── styles.css # Retro arcade UI styles, flexbox, and crt shaders
32
+ │ ├── shaders/ # Custom GLSL Fragment shaders
33
+ │ └── main.py # Collision, input, and game state script
34
+ ├── src/ # C-Extension Core (complies to _doodle.pyd)
35
+ │ ├── setup.py # Setuptools compilation script
36
+ │ ├── expose_raylib.c # Python-to-C bindings, Raylib window, and particle pool
37
+ │ ├── mparser.h / .c # n-ary DOM parser, CSS registry handler, and layout box solver
38
+ │ ├── dutils.h / .c # Custom color parser, fast unit converter, and math helpers
39
+ │ └── daudio.h / .c # Polyphonic synth, multi-voice ADSR envelope generator
40
+ ├── third_party/ # Static Raylib binaries & dependencies
41
+ └── README.md # You are here!
42
+ ```
43
+
44
+ ---
45
+
46
+ ## 🛠️ Getting Started & Compilation
47
+
48
+ ### 1. Requirements
49
+ * **OS**: Windows (x64) / linux
50
+ * **Python**: Python 3.11+ (with virtual environment capability)
51
+ * **C Toolchain**: MSYS2/MinGW-w64 (`C:\msys64\ucrt64\bin` for `gcc` and compilation libraries) / linux cc compiler
52
+
53
+ ### 2. Quick Setup & Build
54
+ From the repository root directory, run the setuptools compilation with your virtual environment's python. Specify the compiler:
55
+
56
+ ```bash
57
+ # Add GCC to PATH (if not global)
58
+ $env:PATH = "C:\msys64\ucrt64\bin;" + $env:PATH
59
+
60
+ # Compile the native C extension
61
+ cd src
62
+ ..\.venv\Scripts\python.exe setup.py build_ext --inplace -c mingw32
63
+
64
+ # Copy the compiled pyd module to the doodle library
65
+ cd ..
66
+ Copy-Item -Path "src\_doodle.cp311-win_amd64.pyd" -Destination "doodle\_doodle.pyd" -Force
67
+ ```
68
+
69
+ ### 3. Run the Breakout Demo
70
+ Launch the breakout game:
71
+ ```bash
72
+ cd examples/breakout
73
+ ..\..\.venv\Scripts\python.exe main.py
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 🏗️ XML Markup (`layout.html`) & CSS (`styles.css`)
79
+
80
+ Doodle supports layout-driven layouts with traditional tag structures and inline event bindings.
81
+
82
+ ### Core Elements
83
+ * `<view>`: Structural container. Standard flex container or a 2D camera boundary using `<view camera="true">`.
84
+ * `<text>`: Text layout with reactive state-bound variables like `SCORE: {{ score }}`.
85
+ * `<image>`: Renders cached bitmaps using `src="..."`.
86
+ * `<button>`: Clickable target with mouse state callbacks.
87
+ * `<circle>`: Renders shapes using `radius` and `color` parameters.
88
+ * `<line>`: Renders vectors using `x2`, `y2`, `thickness`, and `color`.
89
+
90
+ ### Event Hooks
91
+ Directly attach Python functions inline:
92
+ * `onclick="python_function_name"`
93
+ * `onhover="python_function_name"`
94
+
95
+ ### Layout Styles
96
+ * **Sizing Rules**: `width` / `height` support pixels (`100px`), percentages (`50%`), growth parameters (`grow`), and content sizing (`fit`).
97
+ * **Flexbox Attributes**: `display: flex`, `flex-direction` (`row` | `column`), `justify-content` (`center` | `space-between` | `space-around`), `align-items`.
98
+ * **Cosmetics**: `background-color`, `border-radius`, `border-color`, `border-width`, `opacity`, `font-family`, `font-size`.
99
+ * **Juice Shaders**: Add custom GLSL fragment shaders directly to views using `shader-path: "shaders/crt.fs"`.
100
+
101
+ ---
102
+
103
+ ## 🐍 Python OOP APIs
104
+
105
+ ### Initialization
106
+ ```python
107
+ import doodle
108
+
109
+ # Game state passed for template rendering
110
+ state = {"score": 0, "lives": 3}
111
+
112
+ doodle.run(
113
+ layout="layout.html",
114
+ style="styles.css",
115
+ width=800,
116
+ height=600,
117
+ title="Game Window",
118
+ state=state
119
+ )
120
+ ```
121
+
122
+ ### Node Manipulation
123
+ ```python
124
+ # Fetch reference
125
+ paddle = doodle.get_node("paddle")
126
+
127
+ # Read/Write positions
128
+ paddle.position = (350, 500)
129
+ paddle.x += 10.0
130
+
131
+ # Easing animations
132
+ doodle.animate("paddle", target_x=350, target_y=500, duration=0.4, ease="quad_out")
133
+ ```
134
+
135
+ ### Input Polling
136
+ ```python
137
+ # Keys
138
+ doodle.is_key_down(263) # Left arrow key code
139
+ doodle.is_key_pressed(82) # R key code
140
+
141
+ # Mouse
142
+ mx = doodle.get_mouse_x()
143
+ doodle.set_mouse_cursor(4) # Changes mouse pointer style
144
+ ```
145
+
146
+ ---
147
+
148
+ ## 🔊 Polyphonic Procedural Sound Synthesizer
149
+
150
+ Doodle includes an integrated multi-voice synthesizer utilizing **ADSR Envelopes** to play retro sound waves without lagging the update tick loop.
151
+
152
+ ```python
153
+ # Play procedural tone
154
+ doodle.play_synth(
155
+ freq=440.0,
156
+ duration=0.15,
157
+ wave_type=doodle.WAVE_TRIANGLE,
158
+ attack=0.01,
159
+ decay=0.05,
160
+ sustain=0.3,
161
+ release=0.05
162
+ )
163
+ ```
164
+
165
+ **Wave Types**: `WAVE_SINE` (`0`), `WAVE_SQUARE` (`1`), `WAVE_TRIANGLE` (`2`), `WAVE_SAWTOOTH` (`3`), `WAVE_NOISE` (`4`)
166
+
167
+ ---
168
+
169
+ ## ⚡ Performance Optimizations
170
+
171
+ Doodle is engineered for maximum performance, featuring multiple custom optimizations:
172
+ 1. **Single-Item DOM Lookup Cache**: Node lookup (`FindNodeById`) caches the pointer of the last requested node. Repetitive state checks (like checking click and hover events in the same frame) resolve in $O(1)$ without scanning the DOM tree.
173
+ 2. **Precompiled Format Templating**: Template variables `{{ score }}` are compiled once into native Python `{score}` string format structures, converting reactive updates from regex replacements to C-speed string builders.
174
+ 3. **Loop Division Elimination**: Particle shaders and polyphonic audio generators use cached precalculated inverse values (`inv_max_lifetime` and `phase_increment`) to replace expensive division operations with single-clock multiplication instructions.
175
+ 4. **Square Wave Optimization**: Square waves skip standard trignometric `sinf` calculations, computing high-low status directly from phase boundaries.
@@ -0,0 +1,168 @@
1
+ # Doodle Engine 🎨🚀
2
+ ### Hardware-Accelerated Hybrid C-Python UI & 2D Game Engine
3
+
4
+ Doodle is a high-performance, lightweight UI and 2D game engine combining a **native C rendering core (powered by Raylib)** with a **highly reactive Python scripting layer**.
5
+
6
+ By bypassing heavy browser runtimes (like Chromium/Electron), Doodle parses dynamic HTML-like templates and CSS stylesheets in a single pass directly to GPU-accelerated graphics, procedural audio synths, and collision meshes.
7
+
8
+ ---
9
+
10
+ ## 📂 Repository Organization
11
+
12
+ ```text
13
+ Doodle/
14
+ ├── docs/ # Full reference specifications & requirements
15
+ │ ├── cheatsheet.md # API & layout syntax quick reference
16
+ │ ├── requirements.md # Engine specifications and architecture blueprint
17
+ │ └── unused_reference.md # Unused HTML/CSS/C engine features reference
18
+ ├── doodle/ # Python package wrapper
19
+ │ ├── __init__.py # Tween animations, reactive state binding, event emitter
20
+ │ └── cli.py # PyInstaller packager cli
21
+ ├── examples/ # Game demonstrations
22
+ │ └── breakout/ # Breakout game demo
23
+ │ ├── layout.html # Game view DOM nodes & inline handlers
24
+ │ ├── styles.css # Retro arcade UI styles, flexbox, and crt shaders
25
+ │ ├── shaders/ # Custom GLSL Fragment shaders
26
+ │ └── main.py # Collision, input, and game state script
27
+ ├── src/ # C-Extension Core (complies to _doodle.pyd)
28
+ │ ├── setup.py # Setuptools compilation script
29
+ │ ├── expose_raylib.c # Python-to-C bindings, Raylib window, and particle pool
30
+ │ ├── mparser.h / .c # n-ary DOM parser, CSS registry handler, and layout box solver
31
+ │ ├── dutils.h / .c # Custom color parser, fast unit converter, and math helpers
32
+ │ └── daudio.h / .c # Polyphonic synth, multi-voice ADSR envelope generator
33
+ ├── third_party/ # Static Raylib binaries & dependencies
34
+ └── README.md # You are here!
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 🛠️ Getting Started & Compilation
40
+
41
+ ### 1. Requirements
42
+ * **OS**: Windows (x64) / linux
43
+ * **Python**: Python 3.11+ (with virtual environment capability)
44
+ * **C Toolchain**: MSYS2/MinGW-w64 (`C:\msys64\ucrt64\bin` for `gcc` and compilation libraries) / linux cc compiler
45
+
46
+ ### 2. Quick Setup & Build
47
+ From the repository root directory, run the setuptools compilation with your virtual environment's python. Specify the compiler:
48
+
49
+ ```bash
50
+ # Add GCC to PATH (if not global)
51
+ $env:PATH = "C:\msys64\ucrt64\bin;" + $env:PATH
52
+
53
+ # Compile the native C extension
54
+ cd src
55
+ ..\.venv\Scripts\python.exe setup.py build_ext --inplace -c mingw32
56
+
57
+ # Copy the compiled pyd module to the doodle library
58
+ cd ..
59
+ Copy-Item -Path "src\_doodle.cp311-win_amd64.pyd" -Destination "doodle\_doodle.pyd" -Force
60
+ ```
61
+
62
+ ### 3. Run the Breakout Demo
63
+ Launch the breakout game:
64
+ ```bash
65
+ cd examples/breakout
66
+ ..\..\.venv\Scripts\python.exe main.py
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 🏗️ XML Markup (`layout.html`) & CSS (`styles.css`)
72
+
73
+ Doodle supports layout-driven layouts with traditional tag structures and inline event bindings.
74
+
75
+ ### Core Elements
76
+ * `<view>`: Structural container. Standard flex container or a 2D camera boundary using `<view camera="true">`.
77
+ * `<text>`: Text layout with reactive state-bound variables like `SCORE: {{ score }}`.
78
+ * `<image>`: Renders cached bitmaps using `src="..."`.
79
+ * `<button>`: Clickable target with mouse state callbacks.
80
+ * `<circle>`: Renders shapes using `radius` and `color` parameters.
81
+ * `<line>`: Renders vectors using `x2`, `y2`, `thickness`, and `color`.
82
+
83
+ ### Event Hooks
84
+ Directly attach Python functions inline:
85
+ * `onclick="python_function_name"`
86
+ * `onhover="python_function_name"`
87
+
88
+ ### Layout Styles
89
+ * **Sizing Rules**: `width` / `height` support pixels (`100px`), percentages (`50%`), growth parameters (`grow`), and content sizing (`fit`).
90
+ * **Flexbox Attributes**: `display: flex`, `flex-direction` (`row` | `column`), `justify-content` (`center` | `space-between` | `space-around`), `align-items`.
91
+ * **Cosmetics**: `background-color`, `border-radius`, `border-color`, `border-width`, `opacity`, `font-family`, `font-size`.
92
+ * **Juice Shaders**: Add custom GLSL fragment shaders directly to views using `shader-path: "shaders/crt.fs"`.
93
+
94
+ ---
95
+
96
+ ## 🐍 Python OOP APIs
97
+
98
+ ### Initialization
99
+ ```python
100
+ import doodle
101
+
102
+ # Game state passed for template rendering
103
+ state = {"score": 0, "lives": 3}
104
+
105
+ doodle.run(
106
+ layout="layout.html",
107
+ style="styles.css",
108
+ width=800,
109
+ height=600,
110
+ title="Game Window",
111
+ state=state
112
+ )
113
+ ```
114
+
115
+ ### Node Manipulation
116
+ ```python
117
+ # Fetch reference
118
+ paddle = doodle.get_node("paddle")
119
+
120
+ # Read/Write positions
121
+ paddle.position = (350, 500)
122
+ paddle.x += 10.0
123
+
124
+ # Easing animations
125
+ doodle.animate("paddle", target_x=350, target_y=500, duration=0.4, ease="quad_out")
126
+ ```
127
+
128
+ ### Input Polling
129
+ ```python
130
+ # Keys
131
+ doodle.is_key_down(263) # Left arrow key code
132
+ doodle.is_key_pressed(82) # R key code
133
+
134
+ # Mouse
135
+ mx = doodle.get_mouse_x()
136
+ doodle.set_mouse_cursor(4) # Changes mouse pointer style
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 🔊 Polyphonic Procedural Sound Synthesizer
142
+
143
+ Doodle includes an integrated multi-voice synthesizer utilizing **ADSR Envelopes** to play retro sound waves without lagging the update tick loop.
144
+
145
+ ```python
146
+ # Play procedural tone
147
+ doodle.play_synth(
148
+ freq=440.0,
149
+ duration=0.15,
150
+ wave_type=doodle.WAVE_TRIANGLE,
151
+ attack=0.01,
152
+ decay=0.05,
153
+ sustain=0.3,
154
+ release=0.05
155
+ )
156
+ ```
157
+
158
+ **Wave Types**: `WAVE_SINE` (`0`), `WAVE_SQUARE` (`1`), `WAVE_TRIANGLE` (`2`), `WAVE_SAWTOOTH` (`3`), `WAVE_NOISE` (`4`)
159
+
160
+ ---
161
+
162
+ ## ⚡ Performance Optimizations
163
+
164
+ Doodle is engineered for maximum performance, featuring multiple custom optimizations:
165
+ 1. **Single-Item DOM Lookup Cache**: Node lookup (`FindNodeById`) caches the pointer of the last requested node. Repetitive state checks (like checking click and hover events in the same frame) resolve in $O(1)$ without scanning the DOM tree.
166
+ 2. **Precompiled Format Templating**: Template variables `{{ score }}` are compiled once into native Python `{score}` string format structures, converting reactive updates from regex replacements to C-speed string builders.
167
+ 3. **Loop Division Elimination**: Particle shaders and polyphonic audio generators use cached precalculated inverse values (`inv_max_lifetime` and `phase_increment`) to replace expensive division operations with single-clock multiplication instructions.
168
+ 4. **Square Wave Optimization**: Square waves skip standard trignometric `sinf` calculations, computing high-low status directly from phase boundaries.
@@ -0,0 +1,260 @@
1
+ try:
2
+ from . import _doodle
3
+ except ImportError:
4
+ import _doodle
5
+ import sys
6
+ import os
7
+ import re
8
+ import time
9
+ import math
10
+
11
+ # Re-export all flat APIs from _doodle
12
+ for name in dir(_doodle):
13
+ if not name.startswith('_'):
14
+ globals()[name] = getattr(_doodle, name)
15
+
16
+ # Event listener registry
17
+ # node_id -> event_type -> list of callables
18
+ _event_listeners = {}
19
+ _event_context = {}
20
+ _inline_listeners = {}
21
+ _all_event_nodes = []
22
+
23
+ def _rebuild_event_nodes():
24
+ global _all_event_nodes
25
+ _all_event_nodes = list(set(list(_event_listeners.keys()) + [k[0] for k in _inline_listeners.keys()]))
26
+
27
+ def add_event_listener(node_id, event_type, callback):
28
+ if node_id not in _event_listeners:
29
+ _event_listeners[node_id] = {}
30
+ if event_type not in _event_listeners[node_id]:
31
+ _event_listeners[node_id][event_type] = []
32
+ _event_listeners[node_id][event_type].append(callback)
33
+ _rebuild_event_nodes()
34
+
35
+ def set_event_context(context_dict):
36
+ global _event_context
37
+ _event_context.update(context_dict)
38
+
39
+ # OOP Wrapper Node Class
40
+ class NodeStyleProxy:
41
+ def __init__(self, node_id):
42
+ self._id = node_id
43
+ def __setattr__(self, name, value):
44
+ if name.startswith('_'):
45
+ super().__setattr__(name, value)
46
+ else:
47
+ # Convert python_name to css-property-name (e.g. background_color -> background-color)
48
+ css_name = name.replace('_', '-')
49
+ _doodle.set_style(self._id, css_name, str(value))
50
+ def __getattr__(self, name):
51
+ return ""
52
+
53
+ class Node:
54
+ def __init__(self, node_id):
55
+ self.id = node_id
56
+ self.style = NodeStyleProxy(node_id)
57
+
58
+ @property
59
+ def position(self):
60
+ return _doodle.get_position(self.id)
61
+
62
+ @position.setter
63
+ def position(self, pos):
64
+ _doodle.set_position(self.id, float(pos[0]), float(pos[1]))
65
+
66
+ @property
67
+ def x(self):
68
+ return _doodle.get_position(self.id)[0]
69
+
70
+ @x.setter
71
+ def x(self, val):
72
+ y = _doodle.get_position(self.id)[1]
73
+ _doodle.set_position(self.id, float(val), y)
74
+
75
+ @property
76
+ def y(self):
77
+ return _doodle.get_position(self.id)[1]
78
+
79
+ @y.setter
80
+ def y(self, val):
81
+ x = _doodle.get_position(self.id)[0]
82
+ _doodle.set_position(self.id, x, float(val))
83
+
84
+ @property
85
+ def text(self):
86
+ return ""
87
+
88
+ @text.setter
89
+ def text(self, value):
90
+ _doodle.update_text(self.id, str(value))
91
+
92
+ def show(self):
93
+ _doodle.show_node(self.id)
94
+
95
+ def hide(self):
96
+ _doodle.hide_node(self.id)
97
+
98
+ def get_node(node_id):
99
+ return Node(node_id)
100
+
101
+ # Tweens Animation Engine
102
+ _active_tweens = []
103
+
104
+ def animate(node_id, target_x=None, target_y=None, duration=0.5, ease="quad_out"):
105
+ node = Node(node_id)
106
+ start_x, start_y = node.position
107
+
108
+ if target_x is not None:
109
+ _active_tweens.append({
110
+ "node": node,
111
+ "prop": "x",
112
+ "start": start_x,
113
+ "end": target_x,
114
+ "duration": duration,
115
+ "elapsed": 0.0,
116
+ "ease": ease
117
+ })
118
+ if target_y is not None:
119
+ _active_tweens.append({
120
+ "node": node,
121
+ "prop": "y",
122
+ "start": start_y,
123
+ "end": target_y,
124
+ "duration": duration,
125
+ "elapsed": 0.0,
126
+ "ease": ease
127
+ })
128
+
129
+ def _update_tweens(dt):
130
+ global _active_tweens
131
+ still_active = []
132
+ for t in _active_tweens:
133
+ t["elapsed"] += dt
134
+ progress = min(t["elapsed"] / t["duration"], 1.0)
135
+
136
+ if t["ease"] == "linear":
137
+ val = progress
138
+ elif t["ease"] == "quad_out":
139
+ val = progress * (2 - progress)
140
+ elif t["ease"] == "quad_in":
141
+ val = progress * progress
142
+ else:
143
+ val = progress
144
+
145
+ current_val = t["start"] + val * (t["end"] - t["start"])
146
+ setattr(t["node"], t["prop"], current_val)
147
+
148
+ if t["elapsed"] < t["duration"]:
149
+ still_active.append(t)
150
+ _active_tweens = still_active
151
+
152
+ # Waveform Constants
153
+ WAVE_SINE = 0
154
+ WAVE_SQUARE = 1
155
+ WAVE_TRIANGLE = 2
156
+ WAVE_SAWTOOTH = 3
157
+ WAVE_NOISE = 4
158
+
159
+ # Template Data Binding
160
+ _templates = {}
161
+
162
+ def _parse_layout_templates(layout_path):
163
+ global _templates
164
+ _templates = {}
165
+ try:
166
+ with open(layout_path, "r", encoding="utf-8") as f:
167
+ content = f.read()
168
+
169
+ # Match elements containing {{ variable }} with template group markers
170
+ pattern = r'<([a-zA-Z0-9]+)\s+[^>]*id="([^"]+)"[^>]*>([^<]*\{\{.*?\}\}[^<]*)<\/\1>'
171
+ matches = re.findall(pattern, content, re.DOTALL)
172
+ for tag, node_id, template_str in matches:
173
+ # Convert '{{ var }}' to '{var}' for built-in fast formatting
174
+ _templates[node_id] = re.sub(r'\{\{\s*([a-zA-Z0-9_]+)\s*\}\}', r'{\1}', template_str)
175
+ except Exception as e:
176
+ print(f"Template parsing warning: {e}")
177
+
178
+ _tick_callback = None
179
+
180
+ def register_tick_callback(callback):
181
+ global _tick_callback
182
+ _tick_callback = callback
183
+
184
+ def _update_templates(state):
185
+ for node_id, format_str in _templates.items():
186
+ try:
187
+ _doodle.update_text(node_id, format_str.format(**state))
188
+ except Exception:
189
+ pass
190
+
191
+ # Extended main run loop
192
+ def run(layout="layout.html", style="styles.css", width=800, height=600, title="Doodle Engine", state=None):
193
+ if hasattr(sys, "_MEIPASS"):
194
+ try:
195
+ os.chdir(sys._MEIPASS)
196
+ except Exception:
197
+ pass
198
+ _parse_layout_templates(layout)
199
+
200
+ # Extract inline event callbacks from layout.html
201
+ global _inline_listeners
202
+ _inline_listeners = {}
203
+ try:
204
+ with open(layout, "r", encoding="utf-8") as f:
205
+ content = f.read()
206
+ tag_pattern = r'<[a-zA-Z0-9]+\s+([^>]*id="([^"]+)"[^>]*)>'
207
+ tags = re.findall(tag_pattern, content)
208
+ for attr_str, node_id in tags:
209
+ click_match = re.search(r'onclick="([^"]+)"', attr_str)
210
+ if click_match:
211
+ _inline_listeners[(node_id, "click")] = click_match.group(1)
212
+ hover_match = re.search(r'onhover="([^"]+)"', attr_str)
213
+ if hover_match:
214
+ _inline_listeners[(node_id, "hover")] = hover_match.group(1)
215
+ except Exception as e:
216
+ print(f"Event parsing warning: {e}")
217
+
218
+ _rebuild_event_nodes()
219
+
220
+ last_time = time.perf_counter()
221
+
222
+ def wrapper_tick():
223
+ nonlocal last_time
224
+ now = time.perf_counter()
225
+ dt = now - last_time
226
+ last_time = now
227
+
228
+ _update_tweens(dt)
229
+
230
+ if state is not None:
231
+ _update_templates(state)
232
+
233
+ for node_id in _all_event_nodes:
234
+ if _doodle.is_node_clicked(node_id):
235
+ if node_id in _event_listeners and "click" in _event_listeners[node_id]:
236
+ for cb in _event_listeners[node_id]["click"]:
237
+ cb()
238
+ if (node_id, "click") in _inline_listeners:
239
+ cb_name = _inline_listeners[(node_id, "click")]
240
+ cb = _event_context.get(cb_name) or globals().get(cb_name) or sys.modules['__main__'].__dict__.get(cb_name)
241
+ if cb:
242
+ cb()
243
+
244
+ if _doodle.is_node_hovered(node_id):
245
+ if node_id in _event_listeners and "hover" in _event_listeners[node_id]:
246
+ for cb in _event_listeners[node_id]["hover"]:
247
+ cb()
248
+ if (node_id, "hover") in _inline_listeners:
249
+ cb_name = _inline_listeners[(node_id, "hover")]
250
+ cb = _event_context.get(cb_name) or globals().get(cb_name) or sys.modules['__main__'].__dict__.get(cb_name)
251
+ if cb:
252
+ cb()
253
+
254
+ if _tick_callback:
255
+ _tick_callback()
256
+
257
+ # Setup Python wrapper's registration hook
258
+ _doodle.register_tick_callback(wrapper_tick)
259
+
260
+ return _doodle.run(layout=layout, style=style, width=width, height=height, title=title)
@@ -0,0 +1,78 @@
1
+ import sys
2
+ import os
3
+ import subprocess
4
+
5
+ def main():
6
+ if len(sys.argv) < 2:
7
+ print("Usage: python -m doodle.cli package <entrypoint.py>")
8
+ sys.exit(1)
9
+
10
+ cmd = sys.argv[1]
11
+ if cmd != "package":
12
+ print(f"Unknown command: {cmd}. Available commands: package")
13
+ sys.exit(1)
14
+
15
+ if len(sys.argv) < 3:
16
+ print("Please specify the entrypoint script (e.g. main.py)")
17
+ sys.exit(1)
18
+
19
+ entrypoint = sys.argv[2]
20
+ if not os.path.exists(entrypoint):
21
+ print(f"Error: Entrypoint file '{entrypoint}' not found.")
22
+ sys.exit(1)
23
+
24
+ print(f"=== Doodle Standalone Packager ===")
25
+ print(f"Packing '{entrypoint}' into a standalone executable...")
26
+
27
+ # Check if PyInstaller is installed
28
+ try:
29
+ import PyInstaller
30
+ except ImportError:
31
+ print("PyInstaller is not installed. Installing it via pip...")
32
+ subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"], check=True)
33
+
34
+ # Build command line arguments
35
+ # On Windows, path separator is ;. On Unix it is :.
36
+ sep = ";" if os.name == "nt" else ":"
37
+
38
+ import doodle
39
+ doodle_dir = os.path.dirname(os.path.abspath(doodle.__file__))
40
+ doodle_pyd = os.path.join(doodle_dir, "_doodle.pyd")
41
+ entrypoint_dir = os.path.dirname(os.path.abspath(entrypoint))
42
+
43
+ layout_path = os.path.join(entrypoint_dir, "layout.html")
44
+ styles_path = os.path.join(entrypoint_dir, "styles.css")
45
+ shaders_path = os.path.join(entrypoint_dir, "shaders")
46
+ assets_path = os.path.join(entrypoint_dir, "assets")
47
+
48
+ pyinstaller_args = [
49
+ sys.executable,
50
+ "-m",
51
+ "PyInstaller",
52
+ "--onefile",
53
+ "--noconsole",
54
+ f"--add-binary={doodle_pyd}{sep}doodle",
55
+ f"--add-data={layout_path}{sep}.",
56
+ f"--add-data={styles_path}{sep}.",
57
+ ]
58
+
59
+ # Add folders if they exist
60
+ if os.path.exists(shaders_path):
61
+ pyinstaller_args.append(f"--add-data={shaders_path}{sep}shaders")
62
+ if os.path.exists(assets_path):
63
+ pyinstaller_args.append(f"--add-data={assets_path}{sep}assets")
64
+
65
+ # Link against raylib dll if found locally
66
+ if os.path.exists("raylib.dll"):
67
+ pyinstaller_args.append(f"--add-binary=raylib.dll{sep}.")
68
+ elif os.path.exists("src/raylib.dll"):
69
+ pyinstaller_args.append(f"--add-binary=src/raylib.dll{sep}.")
70
+
71
+ pyinstaller_args.append(entrypoint)
72
+
73
+ print(f"Running: {' '.join(pyinstaller_args)}")
74
+ subprocess.run(pyinstaller_args)
75
+ print(f"Packager completed. Check the 'dist/' folder for your executable.")
76
+
77
+ if __name__ == "__main__":
78
+ main()