opacity + animation fixes
This commit is contained in:
parent
2fb29a102e
commit
045b625655
9 changed files with 845 additions and 151 deletions
|
|
@ -284,6 +284,11 @@ bool UIArc::setProperty(const std::string& name, float value) {
|
||||||
markDirty();
|
markDirty();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "opacity") {
|
||||||
|
opacity = std::clamp(value, 0.0f, 1.0f);
|
||||||
|
markDirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,6 +347,10 @@ bool UIArc::getProperty(const std::string& name, float& value) const {
|
||||||
value = origin.y;
|
value = origin.y;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "opacity") {
|
||||||
|
value = opacity;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,7 +373,7 @@ bool UIArc::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
bool UIArc::hasProperty(const std::string& name) const {
|
bool UIArc::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "radius" || name == "start_angle" || name == "end_angle" ||
|
if (name == "radius" || name == "start_angle" || name == "end_angle" ||
|
||||||
name == "thickness" || name == "x" || name == "y" ||
|
name == "thickness" || name == "x" || name == "y" || name == "opacity" ||
|
||||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,10 @@ void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// Check visibility
|
// Check visibility
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
|
|
||||||
// Apply opacity
|
// Apply opacity (multiply with fill_color alpha)
|
||||||
auto color = text.getFillColor();
|
auto color = text.getFillColor();
|
||||||
color.a = static_cast<sf::Uint8>(255 * opacity);
|
sf::Uint8 original_alpha = color.a;
|
||||||
|
color.a = static_cast<sf::Uint8>(original_alpha * opacity);
|
||||||
text.setFillColor(color);
|
text.setFillColor(color);
|
||||||
|
|
||||||
// Apply rotation and origin
|
// Apply rotation and origin
|
||||||
|
|
@ -114,7 +115,7 @@ void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore original alpha
|
// Restore original alpha
|
||||||
color.a = 255;
|
color.a = original_alpha;
|
||||||
text.setFillColor(color);
|
text.setFillColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -604,6 +605,11 @@ bool UICaption::setProperty(const std::string& name, float value) {
|
||||||
markDirty(); // #144 - Content change
|
markDirty(); // #144 - Content change
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "opacity") {
|
||||||
|
opacity = std::clamp(value, 0.0f, 1.0f);
|
||||||
|
markDirty(); // #144 - Visual change
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if (name == "fill_color.r") {
|
else if (name == "fill_color.r") {
|
||||||
auto color = text.getFillColor();
|
auto color = text.getFillColor();
|
||||||
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||||
|
|
@ -730,6 +736,10 @@ bool UICaption::getProperty(const std::string& name, float& value) const {
|
||||||
value = text.getOutlineThickness();
|
value = text.getOutlineThickness();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "opacity") {
|
||||||
|
value = opacity;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if (name == "fill_color.r") {
|
else if (name == "fill_color.r") {
|
||||||
value = text.getFillColor().r;
|
value = text.getFillColor().r;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -808,7 +818,7 @@ bool UICaption::getProperty(const std::string& name, std::string& value) const {
|
||||||
bool UICaption::hasProperty(const std::string& name) const {
|
bool UICaption::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "x" || name == "y" ||
|
if (name == "x" || name == "y" ||
|
||||||
name == "font_size" || name == "size" || name == "outline" ||
|
name == "font_size" || name == "size" || name == "outline" || name == "opacity" ||
|
||||||
name == "fill_color.r" || name == "fill_color.g" ||
|
name == "fill_color.r" || name == "fill_color.g" ||
|
||||||
name == "fill_color.b" || name == "fill_color.a" ||
|
name == "fill_color.b" || name == "fill_color.a" ||
|
||||||
name == "outline_color.r" || name == "outline_color.g" ||
|
name == "outline_color.r" || name == "outline_color.g" ||
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,10 @@ bool UICircle::setProperty(const std::string& name, float value) {
|
||||||
shape.setOrigin(radius + origin.x, radius + origin.y);
|
shape.setOrigin(radius + origin.x, radius + origin.y);
|
||||||
markDirty();
|
markDirty();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (name == "opacity") {
|
||||||
|
opacity = std::clamp(value, 0.0f, 1.0f);
|
||||||
|
markDirty();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -278,6 +282,9 @@ bool UICircle::getProperty(const std::string& name, float& value) const {
|
||||||
} else if (name == "origin_y") {
|
} else if (name == "origin_y") {
|
||||||
value = origin.y;
|
value = origin.y;
|
||||||
return true;
|
return true;
|
||||||
|
} else if (name == "opacity") {
|
||||||
|
value = opacity;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +310,7 @@ bool UICircle::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
|
|
||||||
bool UICircle::hasProperty(const std::string& name) const {
|
bool UICircle::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "radius" || name == "outline" ||
|
if (name == "radius" || name == "outline" || name == "opacity" ||
|
||||||
name == "x" || name == "y" ||
|
name == "x" || name == "y" ||
|
||||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,15 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// Check visibility
|
// Check visibility
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
|
|
||||||
// TODO: Apply opacity when SFML supports it on shapes
|
// Apply opacity (multiply with fill/outline color alpha)
|
||||||
|
auto fill_color = box.getFillColor();
|
||||||
|
auto outline_color = box.getOutlineColor();
|
||||||
|
sf::Uint8 original_fill_alpha = fill_color.a;
|
||||||
|
sf::Uint8 original_outline_alpha = outline_color.a;
|
||||||
|
fill_color.a = static_cast<sf::Uint8>(original_fill_alpha * opacity);
|
||||||
|
outline_color.a = static_cast<sf::Uint8>(original_outline_alpha * opacity);
|
||||||
|
box.setFillColor(fill_color);
|
||||||
|
box.setOutlineColor(outline_color);
|
||||||
|
|
||||||
// #144: Use RenderTexture for clipping OR texture caching OR shaders (#106)
|
// #144: Use RenderTexture for clipping OR texture caching OR shaders (#106)
|
||||||
// clip_children: requires texture for clipping effect (only when has children)
|
// clip_children: requires texture for clipping effect (only when has children)
|
||||||
|
|
@ -194,6 +202,11 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
render_sprite.setOrigin(origin);
|
render_sprite.setOrigin(origin);
|
||||||
render_sprite.setRotation(rotation);
|
render_sprite.setRotation(rotation);
|
||||||
|
|
||||||
|
// Apply opacity to render_sprite
|
||||||
|
auto sprite_color = render_sprite.getColor();
|
||||||
|
sprite_color.a = static_cast<sf::Uint8>(255 * opacity);
|
||||||
|
render_sprite.setColor(sprite_color);
|
||||||
|
|
||||||
// #106: Apply shader if set
|
// #106: Apply shader if set
|
||||||
if (shader && shader->shader) {
|
if (shader && shader->shader) {
|
||||||
// Apply engine uniforms (time, resolution, mouse, texture)
|
// Apply engine uniforms (time, resolution, mouse, texture)
|
||||||
|
|
@ -236,6 +249,12 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
drawable->render(offset + position, target); // Use `position` as source of truth
|
drawable->render(offset + position, target); // Use `position` as source of truth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore original colors
|
||||||
|
fill_color.a = original_fill_alpha;
|
||||||
|
outline_color.a = original_outline_alpha;
|
||||||
|
box.setFillColor(fill_color);
|
||||||
|
box.setOutlineColor(outline_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIFrame::get_children(PyUIFrameObject* self, void* closure)
|
PyObject* UIFrame::get_children(PyUIFrameObject* self, void* closure)
|
||||||
|
|
@ -836,6 +855,10 @@ bool UIFrame::setProperty(const std::string& name, float value) {
|
||||||
box.setOutlineThickness(value);
|
box.setOutlineThickness(value);
|
||||||
markDirty();
|
markDirty();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (name == "opacity") {
|
||||||
|
opacity = std::clamp(value, 0.0f, 1.0f);
|
||||||
|
markDirty();
|
||||||
|
return true;
|
||||||
} else if (name == "fill_color.r") {
|
} else if (name == "fill_color.r") {
|
||||||
auto color = box.getFillColor();
|
auto color = box.getFillColor();
|
||||||
color.r = std::clamp(static_cast<int>(value), 0, 255);
|
color.r = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
|
|
@ -960,6 +983,9 @@ bool UIFrame::getProperty(const std::string& name, float& value) const {
|
||||||
} else if (name == "outline") {
|
} else if (name == "outline") {
|
||||||
value = box.getOutlineThickness();
|
value = box.getOutlineThickness();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (name == "opacity") {
|
||||||
|
value = opacity;
|
||||||
|
return true;
|
||||||
} else if (name == "fill_color.r") {
|
} else if (name == "fill_color.r") {
|
||||||
value = box.getFillColor().r;
|
value = box.getFillColor().r;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1029,7 +1055,7 @@ bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
bool UIFrame::hasProperty(const std::string& name) const {
|
bool UIFrame::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "x" || name == "y" || name == "w" || name == "h" ||
|
if (name == "x" || name == "y" || name == "w" || name == "h" ||
|
||||||
name == "outline" ||
|
name == "outline" || name == "opacity" ||
|
||||||
name == "fill_color.r" || name == "fill_color.g" ||
|
name == "fill_color.r" || name == "fill_color.g" ||
|
||||||
name == "fill_color.b" || name == "fill_color.a" ||
|
name == "fill_color.b" || name == "fill_color.a" ||
|
||||||
name == "outline_color.r" || name == "outline_color.g" ||
|
name == "outline_color.r" || name == "outline_color.g" ||
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,11 @@ bool UILine::setProperty(const std::string& name, float value) {
|
||||||
markDirty();
|
markDirty();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "opacity") {
|
||||||
|
opacity = std::clamp(value, 0.0f, 1.0f);
|
||||||
|
markDirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,6 +359,10 @@ bool UILine::getProperty(const std::string& name, float& value) const {
|
||||||
value = origin.y;
|
value = origin.y;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "opacity") {
|
||||||
|
value = opacity;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,7 +388,7 @@ bool UILine::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
|
|
||||||
bool UILine::hasProperty(const std::string& name) const {
|
bool UILine::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "thickness" || name == "x" || name == "y" ||
|
if (name == "thickness" || name == "x" || name == "y" || name == "opacity" ||
|
||||||
name == "start_x" || name == "start_y" ||
|
name == "start_x" || name == "start_y" ||
|
||||||
name == "end_x" || name == "end_y" ||
|
name == "end_x" || name == "end_y" ||
|
||||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,10 @@ void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// Check visibility
|
// Check visibility
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
|
|
||||||
// Apply opacity
|
// Apply opacity (multiply with sprite color alpha)
|
||||||
auto color = sprite.getColor();
|
auto color = sprite.getColor();
|
||||||
color.a = static_cast<sf::Uint8>(255 * opacity);
|
sf::Uint8 original_alpha = color.a;
|
||||||
|
color.a = static_cast<sf::Uint8>(original_alpha * opacity);
|
||||||
sprite.setColor(color);
|
sprite.setColor(color);
|
||||||
|
|
||||||
// Apply rotation and origin
|
// Apply rotation and origin
|
||||||
|
|
@ -157,7 +158,7 @@ void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore original alpha
|
// Restore original alpha
|
||||||
color.a = 255;
|
color.a = original_alpha;
|
||||||
sprite.setColor(color);
|
sprite.setColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -653,6 +654,11 @@ bool UISprite::setProperty(const std::string& name, float value) {
|
||||||
markDirty(); // #144 - Content change
|
markDirty(); // #144 - Content change
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "opacity") {
|
||||||
|
opacity = std::clamp(value, 0.0f, 1.0f);
|
||||||
|
markDirty(); // #144 - Visual change
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if (name == "z_index") {
|
else if (name == "z_index") {
|
||||||
z_index = static_cast<int>(value);
|
z_index = static_cast<int>(value);
|
||||||
markDirty(); // #144 - Z-order change affects parent
|
markDirty(); // #144 - Z-order change affects parent
|
||||||
|
|
@ -718,6 +724,10 @@ bool UISprite::getProperty(const std::string& name, float& value) const {
|
||||||
value = sprite.getScale().y;
|
value = sprite.getScale().y;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "opacity") {
|
||||||
|
value = opacity;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if (name == "z_index") {
|
else if (name == "z_index") {
|
||||||
value = static_cast<float>(z_index);
|
value = static_cast<float>(z_index);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -757,7 +767,7 @@ bool UISprite::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "x" || name == "y" ||
|
if (name == "x" || name == "y" ||
|
||||||
name == "scale" || name == "scale_x" || name == "scale_y" ||
|
name == "scale" || name == "scale_x" || name == "scale_y" ||
|
||||||
name == "z_index" ||
|
name == "opacity" || name == "z_index" ||
|
||||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,16 @@ int run_python_string(const char* code) {
|
||||||
// Returns a pointer to a static buffer (caller should copy immediately)
|
// Returns a pointer to a static buffer (caller should copy immediately)
|
||||||
static std::string python_output_buffer;
|
static std::string python_output_buffer;
|
||||||
|
|
||||||
|
// Helper to safely delete a key from a dict without leaving error state
|
||||||
|
static void safe_dict_del(PyObject* dict, const char* key) {
|
||||||
|
PyObject* py_key = PyUnicode_FromString(key);
|
||||||
|
if (py_key && PyDict_Contains(dict, py_key)) {
|
||||||
|
PyDict_DelItem(dict, py_key);
|
||||||
|
}
|
||||||
|
Py_XDECREF(py_key);
|
||||||
|
PyErr_Clear(); // Clear any error from Contains or Del
|
||||||
|
}
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
EMSCRIPTEN_KEEPALIVE
|
||||||
const char* run_python_string_with_output(const char* code) {
|
const char* run_python_string_with_output(const char* code) {
|
||||||
if (!Py_IsInitialized()) {
|
if (!Py_IsInitialized()) {
|
||||||
|
|
@ -60,6 +70,12 @@ const char* run_python_string_with_output(const char* code) {
|
||||||
return python_output_buffer.c_str();
|
return python_output_buffer.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Clear any lingering error state before execution
|
||||||
|
// This prevents "clogged" interpreter state from previous errors
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect stdout and stderr to a StringIO, run code, capture output
|
// Redirect stdout and stderr to a StringIO, run code, capture output
|
||||||
// Also capture repr of last expression (like Python REPL)
|
// Also capture repr of last expression (like Python REPL)
|
||||||
const char* capture_code = R"(
|
const char* capture_code = R"(
|
||||||
|
|
@ -113,7 +129,37 @@ elif _mcrf_last_repr:
|
||||||
Py_DECREF(py_code);
|
Py_DECREF(py_code);
|
||||||
|
|
||||||
// Run the capture code
|
// Run the capture code
|
||||||
PyRun_SimpleString(capture_code);
|
int result = PyRun_SimpleString(capture_code);
|
||||||
|
|
||||||
|
// Check if capture code itself failed (e.g., internal error in REPL wrapper)
|
||||||
|
if (result != 0 || PyErr_Occurred()) {
|
||||||
|
// Capture the Python error before clearing
|
||||||
|
PyObject *ptype, *pvalue, *ptraceback;
|
||||||
|
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||||
|
|
||||||
|
python_output_buffer = "Internal REPL Error: ";
|
||||||
|
if (pvalue) {
|
||||||
|
PyObject* str = PyObject_Str(pvalue);
|
||||||
|
if (str) {
|
||||||
|
const char* err_str = PyUnicode_AsUTF8(str);
|
||||||
|
if (err_str) {
|
||||||
|
python_output_buffer += err_str;
|
||||||
|
}
|
||||||
|
Py_DECREF(str);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
python_output_buffer += "Unknown error in capture code";
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_XDECREF(ptype);
|
||||||
|
Py_XDECREF(pvalue);
|
||||||
|
Py_XDECREF(ptraceback);
|
||||||
|
PyErr_Clear(); // Ensure clean state for next execution
|
||||||
|
|
||||||
|
// Still clean up what we can
|
||||||
|
safe_dict_del(main_dict, "_mcrf_user_code");
|
||||||
|
return python_output_buffer.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
// Get the captured output
|
// Get the captured output
|
||||||
PyObject* output = PyDict_GetItemString(main_dict, "_mcrf_captured_output");
|
PyObject* output = PyDict_GetItemString(main_dict, "_mcrf_captured_output");
|
||||||
|
|
@ -124,14 +170,22 @@ elif _mcrf_last_repr:
|
||||||
python_output_buffer = "";
|
python_output_buffer = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up temporary variables
|
// Clean up ALL temporary variables (including previously missed ones)
|
||||||
PyDict_DelItemString(main_dict, "_mcrf_user_code");
|
safe_dict_del(main_dict, "_mcrf_user_code");
|
||||||
PyDict_DelItemString(main_dict, "_mcrf_stdout_capture");
|
safe_dict_del(main_dict, "_mcrf_stdout_capture");
|
||||||
PyDict_DelItemString(main_dict, "_mcrf_stderr_capture");
|
safe_dict_del(main_dict, "_mcrf_stderr_capture");
|
||||||
PyDict_DelItemString(main_dict, "_mcrf_old_stdout");
|
safe_dict_del(main_dict, "_mcrf_old_stdout");
|
||||||
PyDict_DelItemString(main_dict, "_mcrf_old_stderr");
|
safe_dict_del(main_dict, "_mcrf_old_stderr");
|
||||||
PyDict_DelItemString(main_dict, "_mcrf_exec_error");
|
safe_dict_del(main_dict, "_mcrf_exec_error");
|
||||||
PyDict_DelItemString(main_dict, "_mcrf_captured_output");
|
safe_dict_del(main_dict, "_mcrf_captured_output");
|
||||||
|
safe_dict_del(main_dict, "_mcrf_last_repr"); // Previously leaked
|
||||||
|
safe_dict_del(main_dict, "_mcrf_result"); // Previously leaked
|
||||||
|
safe_dict_del(main_dict, "_mcrf_code_obj"); // Previously leaked
|
||||||
|
|
||||||
|
// Final safety check - clear any residual error state
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
|
||||||
return python_output_buffer.c_str();
|
return python_output_buffer.c_str();
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +197,11 @@ int reset_python_environment() {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any lingering error state first
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Clear all scenes and reload game.py
|
// Clear all scenes and reload game.py
|
||||||
const char* reset_code = R"(
|
const char* reset_code = R"(
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
|
|
@ -161,7 +220,74 @@ except Exception as e:
|
||||||
print(f"Reset error: {e}")
|
print(f"Reset error: {e}")
|
||||||
)";
|
)";
|
||||||
|
|
||||||
return PyRun_SimpleString(reset_code);
|
int result = PyRun_SimpleString(reset_code);
|
||||||
|
|
||||||
|
// Clear any error state from reset
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Interpreter health check functions (for debugging)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
static std::string python_state_buffer;
|
||||||
|
|
||||||
|
// Check the current state of the Python interpreter
|
||||||
|
// Returns: "OK", "NOT_INITIALIZED", or "ERROR_SET: <message>"
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
const char* get_python_state() {
|
||||||
|
if (!Py_IsInitialized()) {
|
||||||
|
return "NOT_INITIALIZED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
PyObject *ptype, *pvalue, *ptraceback;
|
||||||
|
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||||
|
|
||||||
|
python_state_buffer = "ERROR_SET: ";
|
||||||
|
if (pvalue) {
|
||||||
|
PyObject* str = PyObject_Str(pvalue);
|
||||||
|
if (str) {
|
||||||
|
const char* err_str = PyUnicode_AsUTF8(str);
|
||||||
|
if (err_str) {
|
||||||
|
python_state_buffer += err_str;
|
||||||
|
}
|
||||||
|
Py_DECREF(str);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
python_state_buffer += "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the error so caller can decide what to do
|
||||||
|
PyErr_Restore(ptype, pvalue, ptraceback);
|
||||||
|
return python_state_buffer.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any pending Python error state
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
void clear_python_error() {
|
||||||
|
if (Py_IsInitialized() && PyErr_Occurred()) {
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a count of items in Python's global namespace (for debugging memory leaks)
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
int get_python_globals_count() {
|
||||||
|
if (!Py_IsInitialized()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* main_module = PyImport_AddModule("__main__");
|
||||||
|
PyObject* main_dict = PyModule_GetDict(main_module);
|
||||||
|
return (int)PyDict_Size(main_dict);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@ class PlaygroundScene(mcrfpy.Scene):
|
||||||
for a in mcrfpy.animations:
|
for a in mcrfpy.animations:
|
||||||
a.stop()
|
a.stop()
|
||||||
for s in mcrfpy.scenes:
|
for s in mcrfpy.scenes:
|
||||||
|
if s is self: continue
|
||||||
s.unregister()
|
s.unregister()
|
||||||
|
self.activate()
|
||||||
while self.children:
|
while self.children:
|
||||||
self.children.pop()
|
self.children.pop()
|
||||||
self.activate()
|
|
||||||
|
|
||||||
scene = PlaygroundScene()
|
scene = PlaygroundScene()
|
||||||
scene.activate()
|
scene.activate()
|
||||||
|
|
|
||||||
732
src/shell.html
732
src/shell.html
|
|
@ -4,6 +4,13 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>McRogueFace - WebGL</title>
|
<title>McRogueFace - WebGL</title>
|
||||||
|
<!-- LZ-String for URL compression -->
|
||||||
|
<script src="https://unpkg.com/lz-string@1.5.0/libs/lz-string.min.js"></script>
|
||||||
|
<!-- CodeMirror from CDN -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/dracula.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -25,11 +32,26 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #e94560;
|
color: #e94560;
|
||||||
}
|
}
|
||||||
|
h1 a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.tagline {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
#status {
|
#status {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
@ -43,24 +65,21 @@
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
#canvas {
|
#canvas {
|
||||||
/* border: 2px solid #e94560; -- disabled per Emscripten docs, causes alignment issues */
|
|
||||||
background: #000;
|
background: #000;
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
/* Crisp pixel rendering - no smoothing/interpolation */
|
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
-ms-interpolation-mode: nearest-neighbor;
|
-ms-interpolation-mode: nearest-neighbor;
|
||||||
/* Ensure canvas can receive focus */
|
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
#canvas:focus {
|
#canvas:focus {
|
||||||
box-shadow: 0 0 10px rgba(78, 204, 163, 0.5);
|
box-shadow: 0 0 10px rgba(78, 204, 163, 0.5);
|
||||||
}
|
}
|
||||||
.repl-panel {
|
.repl-panel {
|
||||||
width: 400px;
|
width: 450px;
|
||||||
min-width: 300px;
|
min-width: 350px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
|
|
@ -74,15 +93,35 @@
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background: #0f3460;
|
background: #0f3460;
|
||||||
border-bottom: 1px solid #e94560;
|
border-bottom: 1px solid #e94560;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.repl-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.repl-header h3 {
|
.repl-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #e94560;
|
color: #e94560;
|
||||||
}
|
}
|
||||||
|
/* Interpreter status indicator */
|
||||||
|
.status-indicator {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
.status-ok { background-color: #4ecca3; }
|
||||||
|
.status-error { background-color: #e94560; }
|
||||||
|
.status-busy { background-color: #f39c12; }
|
||||||
.repl-buttons {
|
.repl-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.repl-buttons button {
|
.repl-buttons button {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
|
|
@ -100,6 +139,13 @@
|
||||||
#runBtn:hover {
|
#runBtn:hover {
|
||||||
background: #3db892;
|
background: #3db892;
|
||||||
}
|
}
|
||||||
|
#shareBtn {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#shareBtn:hover {
|
||||||
|
background: #5a6fd6;
|
||||||
|
}
|
||||||
#resetBtn {
|
#resetBtn {
|
||||||
background: #e94560;
|
background: #e94560;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
@ -118,28 +164,27 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
#codeEditor {
|
/* CodeMirror container */
|
||||||
width: 100%;
|
.editor-container {
|
||||||
height: 250px;
|
border-bottom: 1px solid #333;
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
background: #0f0f23;
|
|
||||||
color: #4ecca3;
|
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
resize: vertical;
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
#codeEditor::placeholder {
|
.CodeMirror {
|
||||||
color: #555;
|
height: 430px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
}
|
}
|
||||||
.repl-output-header {
|
.repl-output-header {
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
background: #0f3460;
|
background: #0f3460;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #888;
|
color: #888;
|
||||||
border-top: 1px solid #333;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.shortcut-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
}
|
}
|
||||||
#replOutput {
|
#replOutput {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -159,6 +204,12 @@
|
||||||
#replOutput .success {
|
#replOutput .success {
|
||||||
color: #4ecca3;
|
color: #4ecca3;
|
||||||
}
|
}
|
||||||
|
#replOutput .warning {
|
||||||
|
color: #f39c12;
|
||||||
|
}
|
||||||
|
#replOutput .input {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
#output {
|
#output {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -184,16 +235,6 @@
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
.toggle-repl {
|
|
||||||
display: none;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: #0f3460;
|
|
||||||
border: 1px solid #e94560;
|
|
||||||
color: #e94560;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -202,18 +243,96 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Keyboard shortcut hint */
|
/* Source indicator for gists */
|
||||||
.shortcut-hint {
|
.source-indicator {
|
||||||
font-size: 11px;
|
padding: 5px 10px;
|
||||||
color: #666;
|
background: #0f3460;
|
||||||
margin-left: 8px;
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4ecca3;
|
||||||
|
}
|
||||||
|
.source-indicator.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.source-indicator a {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
/* Share modal */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.modal-overlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background: #16213e;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
border: 2px solid #e94560;
|
||||||
|
}
|
||||||
|
.modal h2 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #e94560;
|
||||||
|
}
|
||||||
|
.modal p {
|
||||||
|
color: #888;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.modal input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #0f0f23;
|
||||||
|
border: 1px solid #333;
|
||||||
|
color: #4ecca3;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.modal-buttons button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.modal .copy-btn {
|
||||||
|
background: #4ecca3;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
.modal .close-btn {
|
||||||
|
background: #666;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1>McRogueFace</h1>
|
<div class="header-left">
|
||||||
|
<h1><a href="/">McRogueFace</a></h1>
|
||||||
|
<span class="tagline">Playground</span>
|
||||||
|
<span id="sourceIndicator" class="source-indicator hidden"></span>
|
||||||
|
</div>
|
||||||
<div id="status">Downloading...</div>
|
<div id="status">Downloading...</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -226,23 +345,24 @@
|
||||||
|
|
||||||
<div class="repl-panel" id="replPanel">
|
<div class="repl-panel" id="replPanel">
|
||||||
<div class="repl-header">
|
<div class="repl-header">
|
||||||
|
<div class="repl-title">
|
||||||
|
<span id="interpreterStatus" class="status-indicator status-busy" title="Initializing..."></span>
|
||||||
<h3>Python REPL</h3>
|
<h3>Python REPL</h3>
|
||||||
|
</div>
|
||||||
<div class="repl-buttons">
|
<div class="repl-buttons">
|
||||||
<button id="runBtn" disabled>Run</button>
|
<button id="runBtn" disabled>Run</button>
|
||||||
|
<button id="shareBtn">Share</button>
|
||||||
<button id="resetBtn" disabled>Reset</button>
|
<button id="resetBtn" disabled>Reset</button>
|
||||||
<button id="clearBtn">Clear</button>
|
<button id="clearBtn">Clear</button>
|
||||||
<span class="shortcut-hint">Ctrl+Enter to run</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="codeEditor" placeholder="Write Python code here...
|
<div class="editor-container">
|
||||||
|
<textarea id="codeEditor"></textarea>
|
||||||
import mcrfpy
|
</div>
|
||||||
scene = mcrfpy.Scene('test')
|
<div class="repl-output-header">
|
||||||
frame = mcrfpy.Frame(100, 100, 200, 150)
|
<span>Output</span>
|
||||||
scene.ui.append(frame)
|
<span class="shortcut-hint">Ctrl+Enter to run | Ctrl+Up/Down for history</span>
|
||||||
mcrfpy.setScene('test')
|
</div>
|
||||||
"></textarea>
|
|
||||||
<div class="repl-output-header">Output</div>
|
|
||||||
<div id="replOutput"></div>
|
<div id="replOutput"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -250,21 +370,312 @@ mcrfpy.setScene('test')
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Share Modal -->
|
||||||
|
<div id="shareModal" class="modal-overlay hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<h2>Share Your Code</h2>
|
||||||
|
<p>Copy this URL to share your playground:</p>
|
||||||
|
<input type="text" id="shareUrl" readonly onclick="this.select()">
|
||||||
|
<p id="shareSize" style="font-size: 12px;"></p>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button class="copy-btn" id="copyBtn">Copy to Clipboard</button>
|
||||||
|
<button class="close-btn" id="closeModalBtn">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type='text/javascript'>
|
<script type='text/javascript'>
|
||||||
|
// ===========================================
|
||||||
|
// CONFIGURATION
|
||||||
|
// ===========================================
|
||||||
|
const CONFIG = {
|
||||||
|
playgroundUrl: window.location.origin,
|
||||||
|
shortUrl: window.location.origin,
|
||||||
|
playgroundPath: window.location.pathname.replace(/\/[^\/]*$/, ''),
|
||||||
|
maxCodeSize: 16 * 1024,
|
||||||
|
githubApiBase: 'https://api.github.com'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// URL Fragment Handling (Share & Gist support)
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
async function parseUrlFragment() {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
if (!hash) return null;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(hash);
|
||||||
|
|
||||||
|
// Handle #src=<lz-compressed>
|
||||||
|
if (params.has('src')) {
|
||||||
|
try {
|
||||||
|
const compressed = params.get('src');
|
||||||
|
const code = LZString.decompressFromEncodedURIComponent(compressed);
|
||||||
|
if (code) {
|
||||||
|
return { type: 'inline', code: code };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to decompress code:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle #gist=user/id or #gist=id
|
||||||
|
if (params.has('gist')) {
|
||||||
|
const gistParam = params.get('gist');
|
||||||
|
const file = params.get('file');
|
||||||
|
return { type: 'gist', gistId: gistParam, file: file };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGist(gistId, preferredFile) {
|
||||||
|
const id = gistId.includes('/') ? gistId.split('/').pop() : gistId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${CONFIG.githubApiBase}/gists/${id}`);
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`Gist not found (${resp.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
const files = Object.values(data.files);
|
||||||
|
|
||||||
|
let targetFile = null;
|
||||||
|
if (preferredFile) {
|
||||||
|
targetFile = files.find(f => f.filename === preferredFile);
|
||||||
|
}
|
||||||
|
if (!targetFile) {
|
||||||
|
targetFile = files.find(f => f.filename.endsWith('.py'));
|
||||||
|
}
|
||||||
|
if (!targetFile) {
|
||||||
|
targetFile = files[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: targetFile.content,
|
||||||
|
filename: targetFile.filename,
|
||||||
|
gistUrl: data.html_url,
|
||||||
|
owner: data.owner?.login || 'anonymous',
|
||||||
|
description: data.description
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load gist:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateShareUrl(code) {
|
||||||
|
const compressed = LZString.compressToEncodedURIComponent(code);
|
||||||
|
const path = CONFIG.playgroundPath || '';
|
||||||
|
const url = `${CONFIG.shortUrl}${path}/#src=${compressed}`;
|
||||||
|
return {
|
||||||
|
url: url,
|
||||||
|
originalSize: code.length,
|
||||||
|
compressedSize: compressed.length,
|
||||||
|
ratio: ((compressed.length / code.length) * 100).toFixed(1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// DOM Elements
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
var statusElement = document.getElementById('status');
|
var statusElement = document.getElementById('status');
|
||||||
var spinnerElement = document.getElementById('spinner');
|
var spinnerElement = document.getElementById('spinner');
|
||||||
var outputElement = document.getElementById('output');
|
var outputElement = document.getElementById('output');
|
||||||
var canvasElement = document.getElementById('canvas');
|
var canvasElement = document.getElementById('canvas');
|
||||||
var codeEditor = document.getElementById('codeEditor');
|
|
||||||
var replOutput = document.getElementById('replOutput');
|
var replOutput = document.getElementById('replOutput');
|
||||||
var runBtn = document.getElementById('runBtn');
|
var runBtn = document.getElementById('runBtn');
|
||||||
|
var shareBtn = document.getElementById('shareBtn');
|
||||||
var resetBtn = document.getElementById('resetBtn');
|
var resetBtn = document.getElementById('resetBtn');
|
||||||
var clearBtn = document.getElementById('clearBtn');
|
var clearBtn = document.getElementById('clearBtn');
|
||||||
|
var interpreterStatus = document.getElementById('interpreterStatus');
|
||||||
|
var sourceIndicator = document.getElementById('sourceIndicator');
|
||||||
|
var shareModal = document.getElementById('shareModal');
|
||||||
|
var shareUrlInput = document.getElementById('shareUrl');
|
||||||
|
var shareSizeEl = document.getElementById('shareSize');
|
||||||
|
var copyBtn = document.getElementById('copyBtn');
|
||||||
|
var closeModalBtn = document.getElementById('closeModalBtn');
|
||||||
|
|
||||||
|
// Initialize CodeMirror
|
||||||
|
var editor = CodeMirror.fromTextArea(document.getElementById('codeEditor'), {
|
||||||
|
mode: 'python',
|
||||||
|
theme: 'dracula',
|
||||||
|
lineNumbers: true,
|
||||||
|
indentUnit: 4,
|
||||||
|
tabSize: 4,
|
||||||
|
indentWithTabs: false,
|
||||||
|
electricChars: true,
|
||||||
|
matchBrackets: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default content
|
||||||
|
var defaultCode = `import mcrfpy
|
||||||
|
|
||||||
|
# the default "playground" scene
|
||||||
|
scene = mcrfpy.current_scene
|
||||||
|
|
||||||
|
# Frame:
|
||||||
|
frame = mcrfpy.Frame((10, 10), (50, 50), fill_color=(30,30,80))
|
||||||
|
scene.children.append(frame)
|
||||||
|
|
||||||
|
# Caption:
|
||||||
|
caption = mcrfpy.Caption((10,60), text="Hello\nMcRogueFace!", font_size=32) # uses default font
|
||||||
|
scene.children.append(caption)
|
||||||
|
|
||||||
|
# Sprite:
|
||||||
|
sprite = mcrfpy.Sprite((10, 150), sprite_index=84, scale=4.0) # uses default sprite sheet
|
||||||
|
scene.children.append(sprite)
|
||||||
|
|
||||||
|
# Grid:
|
||||||
|
grid = mcrfpy.Grid((250,10), (320,320), grid_size=(10,10), zoom=2.0)
|
||||||
|
scene.children.append(grid)
|
||||||
|
# place entities on grid squares
|
||||||
|
mcrfpy.Entity((4,5), sprite_index=85, grid=grid) # uses default sprite sheet
|
||||||
|
mcrfpy.Entity((5,9), sprite_index=87, grid=grid)
|
||||||
|
mcrfpy.Entity((3,7), sprite_index=89, grid=grid)
|
||||||
|
# fill with some dirt
|
||||||
|
import random
|
||||||
|
for x in range(10):
|
||||||
|
for y in range(10):
|
||||||
|
# mostly 0 for plain dirt, with two variations
|
||||||
|
grid[x,y].tilesprite = random.choice([0, 0, 0, 0, 0, 0, 12, 24])
|
||||||
|
|
||||||
|
|
||||||
|
# make the wizard sprite clickable
|
||||||
|
def poke(position, mousebtn, inputstate):
|
||||||
|
if inputstate != mcrfpy.InputState.PRESSED:
|
||||||
|
return
|
||||||
|
say = random.choice(["oof", "ouch", "uff"])
|
||||||
|
new_txt = mcrfpy.Caption(position, text=say)
|
||||||
|
scene.children.append(new_txt)
|
||||||
|
done = lambda *args: scene.children.remove(new_txt)
|
||||||
|
new_txt.animate("y", # property
|
||||||
|
-100, # target value
|
||||||
|
10.0 + random.randint(-4, 2), # duration
|
||||||
|
callback=done # called on completion
|
||||||
|
)
|
||||||
|
new_txt.animate("x", position.x + random.randint(-70, +270), 5.5)
|
||||||
|
new_txt.animate("fill_color.a", 0, 5.5)
|
||||||
|
sprite.on_click = poke
|
||||||
|
|
||||||
|
# make the wizard sprite moveable
|
||||||
|
def keypress(key, inputstate):
|
||||||
|
actions = { mcrfpy.Key.W: mcrfpy.Vector(0, -10),
|
||||||
|
mcrfpy.Key.A: mcrfpy.Vector(-10, 0),
|
||||||
|
mcrfpy.Key.S: mcrfpy.Vector(0, 10),
|
||||||
|
mcrfpy.Key.D: mcrfpy.Vector(10, 0) }
|
||||||
|
if inputstate != mcrfpy.InputState.PRESSED:
|
||||||
|
return
|
||||||
|
if key in actions:
|
||||||
|
sprite.pos += actions[key]
|
||||||
|
|
||||||
|
scene.on_key = keypress
|
||||||
|
|
||||||
|
print(mcrfpy.current_scene.children)
|
||||||
|
|
||||||
|
# Press F3 for stats
|
||||||
|
|
||||||
|
# create a new scene and switch to it:
|
||||||
|
#new_scene = mcrfpy.Scene("test")
|
||||||
|
#new_scene.activate()
|
||||||
|
`;
|
||||||
|
editor.setValue(defaultCode);
|
||||||
|
|
||||||
|
// Stop keyboard events from bubbling to SDL (bubble phase)
|
||||||
|
var cmElement = editor.getWrapperElement();
|
||||||
|
cmElement.addEventListener('keydown', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Handle Ctrl+Enter for running code
|
||||||
|
if (e.ctrlKey && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!runBtn.disabled) {
|
||||||
|
runCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle Ctrl+Up/Down for history
|
||||||
|
if (e.ctrlKey && e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
navigateHistory(-1);
|
||||||
|
}
|
||||||
|
if (e.ctrlKey && e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
navigateHistory(1);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
cmElement.addEventListener('keyup', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}, false);
|
||||||
|
cmElement.addEventListener('keypress', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Execution history
|
||||||
|
var cmdHistory = [];
|
||||||
|
var historyIndex = -1;
|
||||||
|
var currentInput = '';
|
||||||
|
|
||||||
|
function navigateHistory(direction) {
|
||||||
|
if (cmdHistory.length === 0) return;
|
||||||
|
|
||||||
|
if (historyIndex === -1) {
|
||||||
|
currentInput = editor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
historyIndex += direction;
|
||||||
|
|
||||||
|
if (historyIndex < -1) {
|
||||||
|
historyIndex = -1;
|
||||||
|
} else if (historyIndex >= cmdHistory.length) {
|
||||||
|
historyIndex = cmdHistory.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyIndex === -1) {
|
||||||
|
editor.setValue(currentInput);
|
||||||
|
} else {
|
||||||
|
editor.setValue(cmdHistory[cmdHistory.length - 1 - historyIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.setCursor(editor.lineCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-set canvas size
|
// Pre-set canvas size
|
||||||
canvasElement.width = 1024;
|
canvasElement.width = 1024;
|
||||||
canvasElement.height = 768;
|
canvasElement.height = 768;
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Initialization (load from URL fragment)
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const source = await parseUrlFragment();
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
if (source.type === 'inline') {
|
||||||
|
editor.setValue(source.code);
|
||||||
|
sourceIndicator.textContent = 'Loaded from shared URL';
|
||||||
|
sourceIndicator.classList.remove('hidden');
|
||||||
|
} else if (source.type === 'gist') {
|
||||||
|
editor.setValue('# Loading gist...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gist = await loadGist(source.gistId, source.file);
|
||||||
|
editor.setValue(gist.code);
|
||||||
|
|
||||||
|
sourceIndicator.innerHTML = `From: <a href="${gist.gistUrl}" target="_blank">${gist.owner}/${gist.filename}</a>`;
|
||||||
|
sourceIndicator.classList.remove('hidden');
|
||||||
|
} catch (e) {
|
||||||
|
editor.setValue(`# Failed to load gist: ${e.message}\n# Check the console for details.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Emscripten Module
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
var Module = {
|
var Module = {
|
||||||
print: (function() {
|
print: (function() {
|
||||||
return function(text) {
|
return function(text) {
|
||||||
|
|
@ -310,7 +721,7 @@ mcrfpy.setScene('test')
|
||||||
// Make FS available globally for console access
|
// Make FS available globally for console access
|
||||||
window.FS = Module.FS;
|
window.FS = Module.FS;
|
||||||
|
|
||||||
// Create convenient Python execution functions
|
// Create Python execution functions
|
||||||
window.runPython = function(code) {
|
window.runPython = function(code) {
|
||||||
return Module.ccall('run_python_string_with_output', 'string', ['string'], [code]);
|
return Module.ccall('run_python_string_with_output', 'string', ['string'], [code]);
|
||||||
};
|
};
|
||||||
|
|
@ -318,12 +729,24 @@ mcrfpy.setScene('test')
|
||||||
return Module.ccall('reset_python_environment', 'number', [], []);
|
return Module.ccall('reset_python_environment', 'number', [], []);
|
||||||
};
|
};
|
||||||
|
|
||||||
replOutput.innerHTML = '<span class="success">Python REPL ready. Enter code and click Run (or Ctrl+Enter).</span>\n';
|
// Interpreter health check functions
|
||||||
|
window.getPythonState = function() {
|
||||||
|
return Module.ccall('get_python_state', 'string', [], []);
|
||||||
|
};
|
||||||
|
window.clearPythonError = function() {
|
||||||
|
Module.ccall('clear_python_error', null, [], []);
|
||||||
|
};
|
||||||
|
window.getPythonGlobalsCount = function() {
|
||||||
|
return Module.ccall('get_python_globals_count', 'number', [], []);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateInterpreterStatus();
|
||||||
|
|
||||||
|
appendToOutput('Python REPL ready. Enter code and click Run (or Ctrl+Enter).', 'success');
|
||||||
|
appendToOutput('Tip: Use Ctrl+Up/Down to navigate command history.', 'input');
|
||||||
|
|
||||||
// Focus canvas after a short delay to ensure SDL is ready
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
canvasElement.focus();
|
canvasElement.focus();
|
||||||
// Trigger a synthetic resize to ensure SDL is properly initialized
|
|
||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
@ -336,7 +759,6 @@ mcrfpy.setScene('test')
|
||||||
spinnerElement.style.display = 'none';
|
spinnerElement.style.display = 'none';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stub for resolveGlobalSymbol
|
|
||||||
if (typeof resolveGlobalSymbol === 'undefined') {
|
if (typeof resolveGlobalSymbol === 'undefined') {
|
||||||
window.resolveGlobalSymbol = function(name, direct) {
|
window.resolveGlobalSymbol = function(name, direct) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -346,114 +768,188 @@ mcrfpy.setScene('test')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent browser zoom on canvas
|
// Click on canvas to focus it
|
||||||
document.addEventListener('wheel', function(e) {
|
|
||||||
if (e.ctrlKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, { passive: false, capture: true });
|
|
||||||
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.ctrlKey && (e.key === '+' || e.key === '-' || e.key === '=' || e.key === '_')) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, { capture: true });
|
|
||||||
|
|
||||||
// Click on canvas to focus it (for keyboard input)
|
|
||||||
canvasElement.addEventListener('click', function() {
|
canvasElement.addEventListener('click', function() {
|
||||||
canvasElement.focus();
|
canvasElement.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also handle mousedown to ensure focus happens before SDL processes the click
|
|
||||||
canvasElement.addEventListener('mousedown', function() {
|
canvasElement.addEventListener('mousedown', function() {
|
||||||
if (document.activeElement !== canvasElement) {
|
if (document.activeElement !== canvasElement) {
|
||||||
canvasElement.focus();
|
canvasElement.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// REPL functionality
|
// ===========================================
|
||||||
|
// Interpreter Status
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
function updateInterpreterStatus() {
|
||||||
|
if (!window.getPythonState) {
|
||||||
|
interpreterStatus.className = 'status-indicator status-busy';
|
||||||
|
interpreterStatus.title = 'Initializing...';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = window.getPythonState();
|
||||||
|
if (state === 'OK') {
|
||||||
|
interpreterStatus.className = 'status-indicator status-ok';
|
||||||
|
interpreterStatus.title = 'Interpreter ready';
|
||||||
|
} else if (state === 'NOT_INITIALIZED') {
|
||||||
|
interpreterStatus.className = 'status-indicator status-busy';
|
||||||
|
interpreterStatus.title = 'Python not initialized';
|
||||||
|
} else {
|
||||||
|
interpreterStatus.className = 'status-indicator status-error';
|
||||||
|
interpreterStatus.title = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendToOutput(text, className) {
|
||||||
|
var span = document.createElement('span');
|
||||||
|
span.className = className || '';
|
||||||
|
span.textContent = text + '\n';
|
||||||
|
replOutput.appendChild(span);
|
||||||
|
replOutput.scrollTop = replOutput.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// REPL Functionality
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
function runCode() {
|
function runCode() {
|
||||||
var code = codeEditor.value;
|
var code = editor.getValue();
|
||||||
if (!code.trim()) return;
|
if (!code.trim()) return;
|
||||||
|
|
||||||
replOutput.innerHTML += '<span style="color:#666">>>> </span><span style="color:#888">' + escapeHtml(code.split('\n')[0]) + (code.includes('\n') ? '...' : '') + '</span>\n';
|
// Add to history
|
||||||
|
if (cmdHistory.length === 0 || cmdHistory[cmdHistory.length - 1] !== code) {
|
||||||
|
cmdHistory.push(code);
|
||||||
|
}
|
||||||
|
historyIndex = -1;
|
||||||
|
|
||||||
|
var inputDisplay = '>>> ' + code.split('\n')[0] + (code.includes('\n') ? '...' : '');
|
||||||
|
appendToOutput(inputDisplay, 'input');
|
||||||
|
|
||||||
|
// Check interpreter state and auto-recover if needed
|
||||||
|
var state = window.getPythonState();
|
||||||
|
if (state !== 'OK' && state !== 'NOT_INITIALIZED') {
|
||||||
|
appendToOutput('Warning: Clearing previous error state...', 'warning');
|
||||||
|
window.clearPythonError();
|
||||||
|
updateInterpreterStatus();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Reset environment before running for idempotent execution
|
||||||
|
window.runPython('_reset()');
|
||||||
|
|
||||||
var result = window.runPython(code);
|
var result = window.runPython(code);
|
||||||
|
updateInterpreterStatus();
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
// Check if result contains error indicators
|
if (result.includes('Traceback') ||
|
||||||
if (result.includes('Traceback') || result.includes('Error:')) {
|
result.includes('Error:') ||
|
||||||
replOutput.innerHTML += '<span class="error">' + escapeHtml(result) + '</span>\n';
|
result.includes('Error\n') ||
|
||||||
|
result.startsWith('Internal REPL Error:')) {
|
||||||
|
appendToOutput(result, 'error');
|
||||||
} else {
|
} else {
|
||||||
// Show result (could be print output or repr of expression)
|
appendToOutput(result, 'success');
|
||||||
replOutput.innerHTML += '<span class="success">' + escapeHtml(result) + '</span>\n';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
replOutput.innerHTML += '<span class="error">JavaScript Error: ' + escapeHtml(e.toString()) + '</span>\n';
|
appendToOutput('JavaScript Error: ' + e.toString(), 'error');
|
||||||
|
updateInterpreterStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
replOutput.scrollTop = replOutput.scrollHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetEnvironment() {
|
function resetEnvironment() {
|
||||||
replOutput.innerHTML += '<span style="color:#888">>>> Resetting environment...</span>\n';
|
appendToOutput('>>> Resetting environment...', 'input');
|
||||||
|
|
||||||
|
if (window.clearPythonError) {
|
||||||
|
window.clearPythonError();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.resetGame();
|
window.resetGame();
|
||||||
replOutput.innerHTML += '<span class="success">Environment reset.</span>\n';
|
updateInterpreterStatus();
|
||||||
|
appendToOutput('Environment reset.', 'success');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
replOutput.innerHTML += '<span class="error">Reset error: ' + escapeHtml(e.toString()) + '</span>\n';
|
appendToOutput('Reset error: ' + e.toString(), 'error');
|
||||||
|
updateInterpreterStatus();
|
||||||
}
|
}
|
||||||
replOutput.scrollTop = replOutput.scrollHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearOutput() {
|
function clearOutput() {
|
||||||
replOutput.innerHTML = '';
|
replOutput.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
// ===========================================
|
||||||
var div = document.createElement('div');
|
// Share Functionality
|
||||||
div.textContent = text;
|
// ===========================================
|
||||||
return div.innerHTML;
|
|
||||||
|
function showShareModal() {
|
||||||
|
const code = editor.getValue();
|
||||||
|
|
||||||
|
if (code.length > CONFIG.maxCodeSize) {
|
||||||
|
alert(`Code is too large (${(code.length / 1024).toFixed(1)} KB). Maximum is ${CONFIG.maxCodeSize / 1024} KB.\n\nConsider using a GitHub Gist for larger code.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button handlers
|
const result = generateShareUrl(code);
|
||||||
|
shareUrlInput.value = result.url;
|
||||||
|
shareSizeEl.textContent = `${result.originalSize} bytes -> ${result.compressedSize} chars (${result.ratio}% of original)`;
|
||||||
|
|
||||||
|
if (result.url.length > 2000) {
|
||||||
|
shareSizeEl.textContent += ' Warning: URL is long - may not work in all browsers';
|
||||||
|
}
|
||||||
|
|
||||||
|
shareModal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', function() {
|
||||||
|
shareUrlInput.select();
|
||||||
|
navigator.clipboard.writeText(shareUrlInput.value).then(function() {
|
||||||
|
copyBtn.textContent = 'Copied!';
|
||||||
|
setTimeout(function() {
|
||||||
|
copyBtn.textContent = 'Copy to Clipboard';
|
||||||
|
}, 2000);
|
||||||
|
}).catch(function() {
|
||||||
|
document.execCommand('copy');
|
||||||
|
copyBtn.textContent = 'Copied!';
|
||||||
|
setTimeout(function() {
|
||||||
|
copyBtn.textContent = 'Copy to Clipboard';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
closeModalBtn.addEventListener('click', function() {
|
||||||
|
shareModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
shareModal.addEventListener('click', function(e) {
|
||||||
|
if (e.target === shareModal) {
|
||||||
|
shareModal.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escape closes modal
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape' && !shareModal.classList.contains('hidden')) {
|
||||||
|
shareModal.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Button Handlers
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
runBtn.addEventListener('click', runCode);
|
runBtn.addEventListener('click', runCode);
|
||||||
|
shareBtn.addEventListener('click', showShareModal);
|
||||||
resetBtn.addEventListener('click', resetEnvironment);
|
resetBtn.addEventListener('click', resetEnvironment);
|
||||||
clearBtn.addEventListener('click', clearOutput);
|
clearBtn.addEventListener('click', clearOutput);
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl+Enter to run
|
// ===========================================
|
||||||
// IMPORTANT: Stop propagation to prevent SDL from consuming keystrokes
|
// Initialize
|
||||||
codeEditor.addEventListener('keydown', function(e) {
|
// ===========================================
|
||||||
e.stopPropagation(); // Prevent SDL from receiving this event
|
|
||||||
|
|
||||||
if (e.ctrlKey && e.key === 'Enter') {
|
init();
|
||||||
e.preventDefault();
|
|
||||||
if (!runBtn.disabled) {
|
|
||||||
runCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Tab key inserts spaces instead of changing focus
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
var start = this.selectionStart;
|
|
||||||
var end = this.selectionEnd;
|
|
||||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
|
||||||
this.selectionStart = this.selectionEnd = start + 4;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stop all keyboard events from reaching SDL when REPL is focused
|
|
||||||
codeEditor.addEventListener('keyup', function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
codeEditor.addEventListener('keypress', function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{{{ SCRIPT }}}
|
{{{ SCRIPT }}}
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue