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.
- doodle_ui-1.0.9/MANIFEST.in +2 -0
- doodle_ui-1.0.9/PKG-INFO +175 -0
- doodle_ui-1.0.9/README.md +168 -0
- doodle_ui-1.0.9/doodle/__init__.py +260 -0
- doodle_ui-1.0.9/doodle/cli.py +78 -0
- doodle_ui-1.0.9/doodle_ui.egg-info/PKG-INFO +175 -0
- doodle_ui-1.0.9/doodle_ui.egg-info/SOURCES.txt +18 -0
- doodle_ui-1.0.9/doodle_ui.egg-info/dependency_links.txt +1 -0
- doodle_ui-1.0.9/doodle_ui.egg-info/top_level.txt +2 -0
- doodle_ui-1.0.9/pyproject.toml +32 -0
- doodle_ui-1.0.9/setup.cfg +4 -0
- doodle_ui-1.0.9/setup.py +81 -0
- doodle_ui-1.0.9/src/daudio.c +141 -0
- doodle_ui-1.0.9/src/daudio.h +24 -0
- doodle_ui-1.0.9/src/dutils.c +127 -0
- doodle_ui-1.0.9/src/dutils.h +30 -0
- doodle_ui-1.0.9/src/expose_raylib.c +1005 -0
- doodle_ui-1.0.9/src/mparser.c +869 -0
- doodle_ui-1.0.9/src/mparser.h +104 -0
- doodle_ui-1.0.9/src/raylib.h +1752 -0
doodle_ui-1.0.9/PKG-INFO
ADDED
|
@@ -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()
|