mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-23 07:23:19 +08:00
1524 lines
47 KiB
C++
1524 lines
47 KiB
C++
// Released under the MIT License. See LICENSE for details.
|
||
|
||
#include "ballistica/base/ui/dev_console.h"
|
||
|
||
#include "ballistica/base/app_adapter/app_adapter.h"
|
||
#include "ballistica/base/app_mode/app_mode.h"
|
||
#include "ballistica/base/audio/audio.h"
|
||
#include "ballistica/base/graphics/component/simple_component.h"
|
||
#include "ballistica/base/graphics/mesh/nine_patch_mesh.h"
|
||
#include "ballistica/base/graphics/text/text_graphics.h"
|
||
#include "ballistica/base/logic/logic.h"
|
||
#include "ballistica/base/platform/base_platform.h"
|
||
#include "ballistica/base/python/base_python.h"
|
||
#include "ballistica/base/support/repeater.h"
|
||
#include "ballistica/base/ui/ui.h"
|
||
#include "ballistica/shared/foundation/event_loop.h"
|
||
#include "ballistica/shared/generic/utils.h"
|
||
#include "ballistica/shared/python/python_command.h"
|
||
#include "ballistica/shared/python/python_sys.h"
|
||
|
||
namespace ballistica::base {
|
||
|
||
// How much of the screen the console covers when it is at full size.
|
||
const float kDevConsoleSize{0.9f};
|
||
const int kDevConsoleLineLimit{80};
|
||
const int kDevConsoleStringBreakUpSize{1950};
|
||
const float kDevConsoleTabButtonCornerRadius{16.0f};
|
||
|
||
const double kTransitionSeconds{0.15};
|
||
|
||
enum class DevConsoleHAnchor_ { kLeft, kCenter, kRight };
|
||
enum class DevButtonStyle_ { kNormal, kDark };
|
||
|
||
static auto DevButtonStyleFromStr_(const char* strval) {
|
||
if (!strcmp(strval, "dark")) {
|
||
return DevButtonStyle_::kDark;
|
||
}
|
||
assert(!strcmp(strval, "normal"));
|
||
return DevButtonStyle_::kNormal;
|
||
}
|
||
|
||
static auto DevConsoleHAttachFromStr_(const char* strval) {
|
||
if (!strcmp(strval, "left")) {
|
||
return DevConsoleHAnchor_::kLeft;
|
||
} else if (!strcmp(strval, "right")) {
|
||
return DevConsoleHAnchor_::kRight;
|
||
}
|
||
assert(!strcmp(strval, "center"));
|
||
return DevConsoleHAnchor_::kCenter;
|
||
}
|
||
|
||
static auto TextMeshHAlignFromStr_(const char* strval) {
|
||
if (!strcmp(strval, "left")) {
|
||
return TextMesh::HAlign::kLeft;
|
||
} else if (!strcmp(strval, "right")) {
|
||
return TextMesh::HAlign::kRight;
|
||
}
|
||
assert(!strcmp(strval, "center"));
|
||
return TextMesh::HAlign::kCenter;
|
||
}
|
||
|
||
static auto TextMeshVAlignFromStr_(const char* strval) {
|
||
if (!strcmp(strval, "top")) {
|
||
return TextMesh::VAlign::kTop;
|
||
} else if (!strcmp(strval, "bottom")) {
|
||
return TextMesh::VAlign::kBottom;
|
||
} else if (!strcmp(strval, "none")) {
|
||
return TextMesh::VAlign::kNone;
|
||
}
|
||
assert(!strcmp(strval, "center"));
|
||
return TextMesh::VAlign::kCenter;
|
||
}
|
||
|
||
static auto XOffs(DevConsoleHAnchor_ attach) -> float {
|
||
switch (attach) {
|
||
case DevConsoleHAnchor_::kLeft:
|
||
return 0.0f;
|
||
case DevConsoleHAnchor_::kRight:
|
||
return g_base->graphics->screen_virtual_width();
|
||
case DevConsoleHAnchor_::kCenter:
|
||
return g_base->graphics->screen_virtual_width() * 0.5f;
|
||
}
|
||
assert(false);
|
||
return 0.0f;
|
||
}
|
||
|
||
static auto IsValidHungryChar_(uint32_t this_char) -> bool {
|
||
// Include letters, numbers, and underscore.
|
||
return ((this_char >= 65 && this_char <= 90)
|
||
|| (this_char >= 97 && this_char <= 122)
|
||
|| (this_char >= 48 && this_char <= 57) || this_char == '_');
|
||
}
|
||
|
||
static void DrawRect(RenderPass* pass, Mesh* mesh, float bottom, float x,
|
||
float y, float width, float height,
|
||
const Vector3f& bgcolor) {
|
||
SimpleComponent c(pass);
|
||
c.SetTransparent(true);
|
||
c.SetColor(bgcolor.x, bgcolor.y, bgcolor.z, 1.0f);
|
||
c.SetTexture(g_base->assets->SysTexture(SysTextureID::kCircle));
|
||
// Draw mesh bg.
|
||
if (mesh) {
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(x, y + bottom, kDevConsoleZDepth);
|
||
c.DrawMesh(mesh);
|
||
}
|
||
}
|
||
|
||
static void DrawText(RenderPass* pass, TextGroup* tgrp, float tscale,
|
||
float bottom, float x, float y, const Vector3f& fgcolor) {
|
||
SimpleComponent c(pass);
|
||
c.SetTransparent(true);
|
||
// Draw text.
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(x, y + bottom, kDevConsoleZDepth);
|
||
c.Scale(tscale, tscale, 1.0f);
|
||
int elem_count = tgrp->GetElementCount();
|
||
c.SetColor(fgcolor.x, fgcolor.y, fgcolor.z, 1.0f);
|
||
c.SetFlatness(1.0f);
|
||
for (int e = 0; e < elem_count; e++) {
|
||
c.SetTexture(tgrp->GetElementTexture(e));
|
||
c.DrawMesh(tgrp->GetElementMesh(e));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Anyone iterating through or mucking with the UI lists should hold one
|
||
/// of these while doing so; they simply keep us informed if we're editing
|
||
/// UI stuff where we shouldn't be.
|
||
class DevConsole::ScopedUILock_ {
|
||
public:
|
||
explicit ScopedUILock_(DevConsole* dev_console) : dev_console_{dev_console} {
|
||
assert(g_base->InLogicThread());
|
||
dev_console_->ui_lock_count_++;
|
||
}
|
||
~ScopedUILock_() {
|
||
assert(g_base->InLogicThread());
|
||
dev_console_->ui_lock_count_--;
|
||
assert(dev_console_->ui_lock_count_ >= 0);
|
||
}
|
||
|
||
private:
|
||
DevConsole* dev_console_;
|
||
};
|
||
|
||
/// Super-simple widget type for populating dev-console
|
||
/// (we don't want to depend on any of our full UI feature-sets).
|
||
class DevConsole::Widget_ {
|
||
public:
|
||
virtual ~Widget_() = default;
|
||
virtual auto HandleMouseDown(float mx, float my) -> bool { return false; }
|
||
virtual void HandleMouseUp(float mx, float my) {}
|
||
virtual void Draw(RenderPass* pass, float bottom) = 0;
|
||
};
|
||
|
||
class DevConsole::Text_ : public DevConsole::Widget_ {
|
||
public:
|
||
DevConsoleHAnchor_ h_attach;
|
||
TextMesh::HAlign h_align;
|
||
TextMesh::VAlign v_align;
|
||
float x;
|
||
float y;
|
||
float scale;
|
||
TextGroup text_group;
|
||
DevButtonStyle_ style;
|
||
|
||
Text_(const std::string& text, float x, float y, DevConsoleHAnchor_ h_attach,
|
||
TextMesh::HAlign h_align, TextMesh::VAlign v_align, float scale)
|
||
: h_attach{h_attach},
|
||
h_align(h_align),
|
||
v_align(v_align),
|
||
x{x},
|
||
y{y},
|
||
scale{scale} {
|
||
text_group.SetText(text, h_align, v_align);
|
||
}
|
||
|
||
void Draw(RenderPass* pass, float bottom) override {
|
||
auto fgcolor = Vector3f{0.8f, 0.7f, 0.8f};
|
||
DrawText(pass, &text_group, scale, bottom, x + XOffs(h_attach), y, fgcolor);
|
||
}
|
||
};
|
||
|
||
class DevConsole::Button_ : public DevConsole::Widget_ {
|
||
public:
|
||
DevConsoleHAnchor_ attach;
|
||
float x;
|
||
float y;
|
||
float width;
|
||
float height;
|
||
bool pressed{};
|
||
Object::Ref<Runnable> call;
|
||
NinePatchMesh mesh;
|
||
TextGroup text_group;
|
||
float text_scale;
|
||
DevButtonStyle_ style;
|
||
|
||
template <typename F>
|
||
Button_(const std::string& label, float text_scale, DevConsoleHAnchor_ attach,
|
||
float x, float y, float width, float height, float corner_radius,
|
||
DevButtonStyle_ style, const F& lambda)
|
||
: attach{attach},
|
||
x{x},
|
||
y{y},
|
||
width{width},
|
||
height{height},
|
||
call{NewLambdaRunnable(lambda)},
|
||
text_scale{text_scale},
|
||
style{style},
|
||
mesh(0.0f, 0.0f, 0.0f, width, height,
|
||
NinePatchMesh::BorderForRadius(corner_radius, width, height),
|
||
NinePatchMesh::BorderForRadius(corner_radius, height, width),
|
||
NinePatchMesh::BorderForRadius(corner_radius, width, height),
|
||
NinePatchMesh::BorderForRadius(corner_radius, height, width)) {
|
||
text_group.SetText(label, TextMesh::HAlign::kCenter,
|
||
TextMesh::VAlign::kCenter);
|
||
}
|
||
|
||
auto InUs(float mx, float my) -> bool {
|
||
mx -= XOffs(attach);
|
||
return (mx >= x && mx <= (x + width) && my >= y && my <= (y + height));
|
||
}
|
||
|
||
auto HandleMouseDown(float mx, float my) -> bool override {
|
||
if (InUs(mx, my)) {
|
||
pressed = true;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void HandleMouseUp(float mx, float my) override {
|
||
if (pressed) {
|
||
pressed = false;
|
||
if (InUs(mx, my)) {
|
||
if (call.Exists()) {
|
||
call.Get()->Run();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void Draw(RenderPass* pass, float bottom) override {
|
||
Vector3f fgcolor;
|
||
Vector3f bgcolor;
|
||
switch (style) {
|
||
case DevButtonStyle_::kDark:
|
||
fgcolor =
|
||
pressed ? Vector3f{0.0f, 0.0f, 0.0f} : Vector3f{0.8f, 0.7f, 0.8f};
|
||
bgcolor =
|
||
pressed ? Vector3f{0.6f, 0.5f, 0.6f} : Vector3f{0.16, 0.07f, 0.18f};
|
||
break;
|
||
default:
|
||
assert(style == DevButtonStyle_::kNormal);
|
||
fgcolor =
|
||
pressed ? Vector3f{0.0f, 0.0f, 0.0f} : Vector3f{0.8f, 0.7f, 0.8f};
|
||
bgcolor =
|
||
pressed ? Vector3f{0.8f, 0.7f, 0.8f} : Vector3f{0.25, 0.2f, 0.3f};
|
||
}
|
||
DrawRect(pass, &mesh, bottom, x + XOffs(attach), y, width, height, bgcolor);
|
||
DrawText(pass, &text_group, text_scale, bottom,
|
||
x + XOffs(attach) + width * 0.5f, y + height * 0.5f, fgcolor);
|
||
}
|
||
};
|
||
|
||
class DevConsole::ToggleButton_ : public DevConsole::Widget_ {
|
||
public:
|
||
DevConsoleHAnchor_ attach;
|
||
float x;
|
||
float y;
|
||
float width;
|
||
float height;
|
||
bool pressed{};
|
||
bool on{};
|
||
Object::Ref<Runnable> on_call;
|
||
Object::Ref<Runnable> off_call;
|
||
NinePatchMesh mesh;
|
||
TextGroup text_group;
|
||
float text_scale;
|
||
|
||
template <typename F, typename G>
|
||
ToggleButton_(const std::string& label, float text_scale,
|
||
DevConsoleHAnchor_ attach, float x, float y, float width,
|
||
float height, float corner_radius, const F& on_call,
|
||
const G& off_call)
|
||
: attach{attach},
|
||
x{x},
|
||
y{y},
|
||
width{width},
|
||
height{height},
|
||
on_call{NewLambdaRunnable(on_call)},
|
||
off_call{NewLambdaRunnable(off_call)},
|
||
text_scale{text_scale},
|
||
mesh(0.0f, 0.0f, 0.0f, width, height,
|
||
NinePatchMesh::BorderForRadius(corner_radius, width, height),
|
||
NinePatchMesh::BorderForRadius(corner_radius, height, width),
|
||
NinePatchMesh::BorderForRadius(corner_radius, width, height),
|
||
NinePatchMesh::BorderForRadius(corner_radius, height, width)) {
|
||
text_group.SetText(label, TextMesh::HAlign::kCenter,
|
||
TextMesh::VAlign::kCenter);
|
||
}
|
||
|
||
auto InUs(float mx, float my) -> bool {
|
||
mx -= XOffs(attach);
|
||
return (mx >= x && mx <= (x + width) && my >= y && my <= (y + height));
|
||
}
|
||
|
||
auto HandleMouseDown(float mx, float my) -> bool override {
|
||
if (InUs(mx, my)) {
|
||
pressed = true;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void HandleMouseUp(float mx, float my) override {
|
||
if (pressed) {
|
||
pressed = false;
|
||
if (InUs(mx, my)) {
|
||
on = !on;
|
||
auto&& call = on ? on_call : off_call;
|
||
if (call.Exists()) {
|
||
call.Get()->Run();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void Draw(RenderPass* pass, float bottom) override {
|
||
DrawRect(pass, &mesh, bottom, x + XOffs(attach), y, width, height,
|
||
pressed ? Vector3f{0.5f, 0.2f, 1.0f}
|
||
: on ? Vector3f{0.5f, 0.4f, 0.6f}
|
||
: Vector3f{0.25, 0.2f, 0.3f});
|
||
DrawText(pass, &text_group, text_scale, bottom,
|
||
x + XOffs(attach) + width * 0.5f, y + height * 0.5f,
|
||
pressed ? Vector3f{1.0f, 1.0f, 1.0f}
|
||
: on ? Vector3f{1.0f, 1.0f, 1.0f}
|
||
: Vector3f{0.8f, 0.7f, 0.8f});
|
||
}
|
||
};
|
||
|
||
class DevConsole::TabButton_ : public DevConsole::Widget_ {
|
||
public:
|
||
DevConsoleHAnchor_ attach;
|
||
float x;
|
||
float y;
|
||
float width;
|
||
float height;
|
||
bool pressed{};
|
||
bool selected{};
|
||
Object::Ref<Runnable> call;
|
||
TextGroup text_group;
|
||
NinePatchMesh mesh;
|
||
float text_scale;
|
||
|
||
template <typename F>
|
||
TabButton_(const std::string& label, bool selected, float text_scale,
|
||
DevConsoleHAnchor_ attach, float x, float y, float width,
|
||
float height, const F& call)
|
||
: attach{attach},
|
||
x{x},
|
||
y{y},
|
||
selected{selected},
|
||
width{width},
|
||
height{height},
|
||
call{NewLambdaRunnable(call)},
|
||
text_scale{text_scale},
|
||
mesh(0.0f, 0.0f, 0.0f, width, height,
|
||
NinePatchMesh::BorderForRadius(kDevConsoleTabButtonCornerRadius,
|
||
width, height),
|
||
NinePatchMesh::BorderForRadius(kDevConsoleTabButtonCornerRadius,
|
||
height, width),
|
||
NinePatchMesh::BorderForRadius(kDevConsoleTabButtonCornerRadius,
|
||
width, height),
|
||
0.0f) {
|
||
text_group.SetText(label, TextMesh::HAlign::kCenter,
|
||
TextMesh::VAlign::kCenter);
|
||
}
|
||
|
||
auto InUs(float mx, float my) -> bool {
|
||
mx -= XOffs(attach);
|
||
return (mx >= x && mx <= (x + width) && my >= y && my <= (y + height));
|
||
}
|
||
|
||
auto HandleMouseDown(float mx, float my) -> bool override {
|
||
if (InUs(mx, my) && !selected) {
|
||
pressed = true;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void HandleMouseUp(float mx, float my) override {
|
||
if (pressed) {
|
||
pressed = false;
|
||
if (InUs(mx, my)) {
|
||
// Technically this callback should cause us to be recreated in a
|
||
// selected state, but that happens in a deferred call, so go ahead
|
||
// and set ourself as selected already so we don't flash as
|
||
// unselected for a frame before the deferred call runs.
|
||
selected = true;
|
||
|
||
if (call.Exists()) {
|
||
call.Get()->Run();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void Draw(RenderPass* pass, float bottom) override {
|
||
DrawRect(pass, &mesh, bottom, x + XOffs(attach), y, width, height,
|
||
pressed ? Vector3f{0.4f, 0.2f, 0.8f}
|
||
: selected ? Vector3f{0.4f, 0.3f, 0.4f}
|
||
: Vector3f{0.25, 0.2f, 0.3f});
|
||
DrawText(pass, &text_group, text_scale, bottom,
|
||
x + XOffs(attach) + width * 0.5f, y + height * 0.5f,
|
||
pressed ? Vector3f{1.0f, 1.0f, 1.0f}
|
||
: selected ? Vector3f{1.0f, 1.0f, 1.0f}
|
||
: Vector3f{0.6f, 0.5f, 0.6f});
|
||
}
|
||
};
|
||
|
||
class DevConsole::OutputLine_ {
|
||
public:
|
||
OutputLine_(std::string s_in, double c)
|
||
: creation_time(c), s(std::move(s_in)) {}
|
||
double creation_time;
|
||
std::string s;
|
||
auto GetText() -> TextGroup& {
|
||
if (!s_mesh_.Exists()) {
|
||
s_mesh_ = Object::New<TextGroup>();
|
||
s_mesh_->SetText(s);
|
||
}
|
||
return *s_mesh_;
|
||
}
|
||
|
||
private:
|
||
Object::Ref<TextGroup> s_mesh_;
|
||
};
|
||
|
||
DevConsole::DevConsole() {
|
||
assert(g_base->InLogicThread());
|
||
std::string title = std::string("BallisticaKit ") + kEngineVersion + " ("
|
||
+ std::to_string(kEngineBuildNumber) + ")";
|
||
if (g_buildconfig.debug_build()) {
|
||
title += " (debug)";
|
||
}
|
||
if (g_buildconfig.test_build()) {
|
||
title += " (test)";
|
||
}
|
||
title_text_group_.SetText(title);
|
||
built_text_group_.SetText("Built: " __DATE__ " " __TIME__);
|
||
prompt_text_group_.SetText(">");
|
||
}
|
||
|
||
void DevConsole::RefreshTabButtons_() {
|
||
// IMPORTANT: This code should always be run in its own top level call and
|
||
// never directly from user code. Otherwise we can wind up mucking with
|
||
// the UI list as we're iterating through it.
|
||
assert(!ui_lock_count_);
|
||
|
||
// Ask the Python layer for the latest set of tabs.
|
||
tabs_ = g_base->python->objs()
|
||
.Get(BasePython::ObjID::kGetDevConsoleTabNamesCall)
|
||
.Call()
|
||
.ValueAsStringSequence();
|
||
// If we have tabs and none of them are selected, select the first.
|
||
if (!tabs_.empty()) {
|
||
bool found{};
|
||
for (auto&& tab : tabs_) {
|
||
if (active_tab_ == tab) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
active_tab_ = tabs_.front();
|
||
}
|
||
}
|
||
|
||
// Now rebuild our buttons for them.
|
||
tab_buttons_.clear();
|
||
float bs = BaseScale();
|
||
float bwidth = 90.0f * bs;
|
||
float bheight = 26.0f * bs;
|
||
float bscale = 0.6f * bs;
|
||
float total_width = tabs_.size() * bwidth;
|
||
float x = total_width * -0.5f;
|
||
for (auto&& tab : tabs_) {
|
||
tab_buttons_.emplace_back(std::make_unique<TabButton_>(
|
||
tab, active_tab_ == tab, bscale, DevConsoleHAnchor_::kCenter, x,
|
||
-bheight, bwidth, bheight, [this, tab] {
|
||
active_tab_ = tab;
|
||
// Can't muck with UI from code called while iterating through UI.
|
||
// So defer it.
|
||
g_base->logic->event_loop()->PushCall([this] {
|
||
RefreshTabButtons_();
|
||
RefreshTabContents_();
|
||
});
|
||
}));
|
||
x += bwidth;
|
||
}
|
||
}
|
||
|
||
void DevConsole::RefreshTabContents_() {
|
||
BA_PRECONDITION(g_base->InLogicThread());
|
||
|
||
// IMPORTANT: This code should always be run in its own top level call and
|
||
// never directly from user code. Otherwise we can wind up mucking with
|
||
// the UI list as we're iterating through it.
|
||
assert(!ui_lock_count_);
|
||
|
||
// Consider any refresh requests fulfilled. Subsequent refresh-requests
|
||
// will generate a new refresh at this point.
|
||
refresh_pending_ = false;
|
||
|
||
// Clear to an empty slate.
|
||
widgets_.clear();
|
||
python_terminal_visible_ = false;
|
||
|
||
// Now ask the Python layer to fill this tab in.
|
||
PythonRef args(Py_BuildValue("(s)", active_tab_.c_str()), PythonRef::kSteal);
|
||
g_base->python->objs()
|
||
.Get(BasePython::ObjID::kAppDevConsoleDoRefreshTabCall)
|
||
.Call(args);
|
||
}
|
||
|
||
void DevConsole::AddText(const char* text, float x, float y,
|
||
const char* h_anchor_str, const char* h_align_str,
|
||
const char* v_align_str, float scale) {
|
||
auto h_anchor = DevConsoleHAttachFromStr_(h_anchor_str);
|
||
auto h_align = TextMeshHAlignFromStr_(h_align_str);
|
||
auto v_align = TextMeshVAlignFromStr_(v_align_str);
|
||
|
||
widgets_.emplace_back(
|
||
std::make_unique<Text_>(text, x, y, h_anchor, h_align, v_align, scale));
|
||
}
|
||
|
||
void DevConsole::AddButton(const char* label, float x, float y, float width,
|
||
float height, PyObject* call,
|
||
const char* h_anchor_str, float label_scale,
|
||
float corner_radius, const char* style_str) {
|
||
assert(g_base->InLogicThread());
|
||
|
||
auto style = DevButtonStyleFromStr_(style_str);
|
||
auto h_anchor = DevConsoleHAttachFromStr_(h_anchor_str);
|
||
|
||
widgets_.emplace_back(std::make_unique<Button_>(
|
||
label, label_scale, h_anchor, x, y, width, height, corner_radius, style,
|
||
[this, callref = PythonRef::Acquired(call)] {
|
||
if (callref.Get() != Py_None) {
|
||
callref.Call();
|
||
}
|
||
}));
|
||
}
|
||
|
||
void DevConsole::AddPythonTerminal() {
|
||
float bs = BaseScale();
|
||
widgets_.emplace_back(std::make_unique<Button_>(
|
||
"Exec", 0.5f * bs, DevConsoleHAnchor_::kRight, -33.0f * bs, 15.95f * bs,
|
||
32.0f * bs, 13.0f * bs, 2.0 * bs, DevButtonStyle_::kNormal,
|
||
[this] { Exec(); }));
|
||
python_terminal_visible_ = true;
|
||
}
|
||
|
||
void DevConsole::RequestRefresh() {
|
||
assert(g_base->InLogicThread());
|
||
|
||
// Schedule a refresh. If one is already scheduled but hasn't run, do
|
||
// nothing.
|
||
if (refresh_pending_) {
|
||
return;
|
||
}
|
||
refresh_pending_ = true;
|
||
g_base->logic->event_loop()->PushCall([this] { RefreshTabContents_(); });
|
||
}
|
||
|
||
auto DevConsole::HandleMouseDown(int button, float x, float y) -> bool {
|
||
assert(g_base->InLogicThread());
|
||
|
||
if (state_ == State_::kInactive) {
|
||
return false;
|
||
}
|
||
float bottom{Bottom_()};
|
||
|
||
// Pass to any buttons (in bottom-local space).
|
||
if (button == 1) {
|
||
// Make sure we don't muck with our UI while we're in here.
|
||
auto lock = ScopedUILock_(this);
|
||
|
||
for (auto&& button : tab_buttons_) {
|
||
if (button->HandleMouseDown(x, y - bottom)) {
|
||
return true;
|
||
}
|
||
}
|
||
for (auto&& button : widgets_) {
|
||
if (button->HandleMouseDown(x, y - bottom)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (y < bottom) {
|
||
return false;
|
||
}
|
||
|
||
if (button == 1 && python_terminal_visible_) {
|
||
python_terminal_pressed_ = true;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
auto DevConsole::Width() -> float {
|
||
return g_base->graphics->screen_virtual_width();
|
||
}
|
||
|
||
auto DevConsole::Height() -> float {
|
||
return g_base->graphics->screen_virtual_height() - Bottom_();
|
||
}
|
||
|
||
void DevConsole::HandleMouseUp(int button, float x, float y) {
|
||
assert(g_base->InLogicThread());
|
||
float bottom{Bottom_()};
|
||
|
||
if (button == 1) {
|
||
// Make sure we don't muck with our UI while we're in here.
|
||
auto lock = ScopedUILock_(this);
|
||
|
||
for (auto&& button : tab_buttons_) {
|
||
button->HandleMouseUp(x, y - bottom);
|
||
}
|
||
for (auto&& button : widgets_) {
|
||
button->HandleMouseUp(x, y - bottom);
|
||
}
|
||
}
|
||
|
||
if (button == 1 && python_terminal_pressed_) {
|
||
python_terminal_pressed_ = false;
|
||
if (y > bottom) {
|
||
// If we're not getting fed keyboard events and have a string editor
|
||
// available, invoke it.
|
||
if (!g_base->ui->UIHasDirectKeyboardInput()
|
||
&& g_base->platform->HaveStringEditor()) {
|
||
InvokeStringEditor_();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void DevConsole::InvokeStringEditor_() {
|
||
// If there's already a valid edit-adapter attached to us, do nothing.
|
||
if (string_edit_adapter_.Exists()
|
||
&& !g_base->python->CanPyStringEditAdapterBeReplaced(
|
||
string_edit_adapter_.Get())) {
|
||
return;
|
||
}
|
||
|
||
// Create a Python StringEditAdapter for this widget, passing ourself as
|
||
// the sole arg.
|
||
auto result = g_base->python->objs()
|
||
.Get(BasePython::ObjID::kDevConsoleStringEditAdapterClass)
|
||
.Call();
|
||
if (!result.Exists()) {
|
||
Log(LogLevel::kError, "Error invoking string edit dialog.");
|
||
return;
|
||
}
|
||
|
||
// If this new one is already marked replacable, it means it wasn't able
|
||
// to register as the active one, so we can ignore it.
|
||
if (g_base->python->CanPyStringEditAdapterBeReplaced(result.Get())) {
|
||
return;
|
||
}
|
||
|
||
// Ok looks like we're good; store the adapter as our active one.
|
||
string_edit_adapter_ = result;
|
||
|
||
g_base->platform->InvokeStringEditor(string_edit_adapter_.Get());
|
||
}
|
||
|
||
void DevConsole::set_input_string(const std::string& val) {
|
||
assert(g_base->InLogicThread());
|
||
input_string_ = val;
|
||
input_text_dirty_ = true;
|
||
// Move carat to end.
|
||
carat_char_ =
|
||
static_cast<int>(Utils::UnicodeFromUTF8(input_string_, "fj43t").size());
|
||
assert(CaratCharValid_());
|
||
carat_dirty_ = true;
|
||
}
|
||
|
||
void DevConsole::InputAdapterFinish() {
|
||
assert(g_base->InLogicThread());
|
||
string_edit_adapter_.Release();
|
||
}
|
||
|
||
auto DevConsole::HandleKeyPress(const SDL_Keysym* keysym) -> bool {
|
||
assert(g_base->InLogicThread());
|
||
|
||
// Any presses or releases cancels repeat actions.
|
||
key_repeater_.Clear();
|
||
|
||
if (state_ == State_::kInactive) {
|
||
return false;
|
||
}
|
||
|
||
// Stuff we always look for.
|
||
switch (keysym->sym) {
|
||
case SDLK_ESCAPE:
|
||
Dismiss();
|
||
return true;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// Stuff we look for only when direct keyboard input is enabled and our
|
||
// Python terminal is up.
|
||
if (python_terminal_visible_ && g_base->ui->UIHasDirectKeyboardInput()) {
|
||
bool do_carat_right{};
|
||
bool do_hungry_carat_right{};
|
||
bool do_carat_left{};
|
||
bool do_hungry_carat_left{};
|
||
bool do_history_up{};
|
||
bool do_history_down{};
|
||
bool do_backspace{};
|
||
bool do_forward_delete{};
|
||
bool do_hungry_backspace{};
|
||
bool do_hungry_forward_delete{};
|
||
bool do_move_to_end{};
|
||
bool do_move_to_beginning{};
|
||
bool do_kill_line{};
|
||
switch (keysym->sym) {
|
||
case SDLK_BACKSPACE: {
|
||
if (keysym->mod & KMOD_ALT) {
|
||
do_hungry_backspace = true;
|
||
} else {
|
||
do_backspace = true;
|
||
}
|
||
break;
|
||
}
|
||
case SDLK_DELETE: {
|
||
if (keysym->mod & KMOD_ALT) {
|
||
do_hungry_forward_delete = true;
|
||
} else {
|
||
do_forward_delete = true;
|
||
}
|
||
break;
|
||
}
|
||
case SDLK_HOME:
|
||
do_move_to_beginning = true;
|
||
break;
|
||
case SDLK_END:
|
||
do_move_to_end = true;
|
||
break;
|
||
case SDLK_UP:
|
||
do_history_up = true;
|
||
break;
|
||
case SDLK_DOWN:
|
||
do_history_down = true;
|
||
break;
|
||
case SDLK_RIGHT:
|
||
if (keysym->mod & KMOD_ALT) {
|
||
do_hungry_carat_right = true;
|
||
} else {
|
||
do_carat_right = true;
|
||
}
|
||
break;
|
||
case SDLK_LEFT:
|
||
if (keysym->mod & KMOD_ALT) {
|
||
do_hungry_carat_left = true;
|
||
} else {
|
||
do_carat_left = true;
|
||
}
|
||
break;
|
||
case SDLK_KP_ENTER:
|
||
case SDLK_RETURN: {
|
||
Exec();
|
||
break;
|
||
}
|
||
|
||
// Wheeee emacs key shortcuts!!
|
||
case SDLK_n:
|
||
if (keysym->mod & KMOD_CTRL) {
|
||
do_history_down = true;
|
||
}
|
||
break;
|
||
case SDLK_f:
|
||
if (keysym->mod & KMOD_CTRL) {
|
||
do_carat_right = true;
|
||
} else if (keysym->mod & KMOD_ALT) {
|
||
do_hungry_carat_right = true;
|
||
}
|
||
break;
|
||
case SDLK_b:
|
||
if (keysym->mod & KMOD_CTRL) {
|
||
do_carat_left = true;
|
||
} else if (keysym->mod & KMOD_ALT) {
|
||
do_hungry_carat_left = true;
|
||
}
|
||
break;
|
||
case SDLK_p:
|
||
if (keysym->mod & KMOD_CTRL) {
|
||
do_history_up = true;
|
||
}
|
||
break;
|
||
case SDLK_a:
|
||
if (keysym->mod & KMOD_CTRL) {
|
||
do_move_to_beginning = true;
|
||
}
|
||
break;
|
||
case SDLK_d:
|
||
if (keysym->mod & KMOD_CTRL) {
|
||
do_forward_delete = true;
|
||
} else if (keysym->mod & KMOD_ALT) {
|
||
do_hungry_forward_delete = true;
|
||
}
|
||
break;
|
||
case SDLK_e:
|
||
if (keysym->mod & KMOD_CTRL) {
|
||
do_move_to_end = true;
|
||
}
|
||
break;
|
||
case SDLK_k:
|
||
if (keysym->mod & KMOD_CTRL) {
|
||
do_kill_line = true;
|
||
}
|
||
default: {
|
||
break;
|
||
}
|
||
}
|
||
if (do_kill_line) {
|
||
auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco38");
|
||
assert(CaratCharValid_());
|
||
unichars.resize(carat_char_);
|
||
assert(CaratCharValid_());
|
||
input_string_ = Utils::UTF8FromUnicode(unichars);
|
||
input_text_dirty_ = true;
|
||
carat_dirty_ = true;
|
||
}
|
||
if (do_move_to_beginning) {
|
||
carat_char_ = 0;
|
||
assert(CaratCharValid_());
|
||
carat_dirty_ = true;
|
||
}
|
||
if (do_move_to_end) {
|
||
// Move carat to end.
|
||
carat_char_ = static_cast<int>(
|
||
Utils::UnicodeFromUTF8(input_string_, "fj43t").size());
|
||
assert(CaratCharValid_());
|
||
carat_dirty_ = true;
|
||
}
|
||
if (do_hungry_backspace || do_hungry_carat_left) {
|
||
auto do_delete = do_hungry_backspace;
|
||
key_repeater_ = Repeater::New(
|
||
g_base->app_adapter->GetKeyRepeatDelay(),
|
||
g_base->app_adapter->GetKeyRepeatInterval(), [this, do_delete] {
|
||
auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco38");
|
||
bool found_valid{};
|
||
// Delete/move until we've found at least one valid char and the
|
||
// stop at the first invalid one.
|
||
while (carat_char_ > 0) {
|
||
assert(CaratCharValid_());
|
||
auto this_char = unichars[carat_char_ - 1];
|
||
auto is_valid = IsValidHungryChar_(this_char);
|
||
if (found_valid && !is_valid) {
|
||
break;
|
||
}
|
||
if (is_valid) {
|
||
found_valid = true;
|
||
}
|
||
if (do_delete) {
|
||
unichars.erase(unichars.begin() + carat_char_ - 1);
|
||
}
|
||
carat_char_ -= 1;
|
||
assert(CaratCharValid_());
|
||
}
|
||
if (do_delete) {
|
||
input_string_ = Utils::UTF8FromUnicode(unichars);
|
||
input_text_dirty_ = true;
|
||
}
|
||
carat_dirty_ = true;
|
||
});
|
||
}
|
||
if (do_hungry_forward_delete || do_hungry_carat_right) {
|
||
auto do_delete = do_hungry_forward_delete;
|
||
key_repeater_ = Repeater::New(
|
||
g_base->app_adapter->GetKeyRepeatDelay(),
|
||
g_base->app_adapter->GetKeyRepeatInterval(), [this, do_delete] {
|
||
auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco38");
|
||
bool found_valid{};
|
||
// Move until we've found at least one valid char and the
|
||
// stop at the first invalid one.
|
||
while (carat_char_ < static_cast<int>(unichars.size())) {
|
||
assert(CaratCharValid_());
|
||
auto this_char = unichars[carat_char_];
|
||
auto is_valid = IsValidHungryChar_(this_char);
|
||
if (found_valid && !is_valid) {
|
||
break;
|
||
}
|
||
if (is_valid) {
|
||
found_valid = true;
|
||
}
|
||
if (do_delete) {
|
||
unichars.erase(unichars.begin() + carat_char_);
|
||
} else {
|
||
carat_char_ += 1;
|
||
}
|
||
assert(CaratCharValid_());
|
||
}
|
||
if (do_delete) {
|
||
input_string_ = Utils::UTF8FromUnicode(unichars);
|
||
input_text_dirty_ = true;
|
||
}
|
||
carat_dirty_ = true;
|
||
});
|
||
}
|
||
if (do_backspace) {
|
||
key_repeater_ = Repeater::New(
|
||
g_base->app_adapter->GetKeyRepeatDelay(),
|
||
g_base->app_adapter->GetKeyRepeatInterval(), [this] {
|
||
auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco38");
|
||
if (!unichars.empty() && carat_char_ > 0) {
|
||
assert(CaratCharValid_());
|
||
unichars.erase(unichars.begin() + carat_char_ - 1);
|
||
input_string_ = Utils::UTF8FromUnicode(unichars);
|
||
input_text_dirty_ = true;
|
||
carat_char_ -= 1;
|
||
assert(CaratCharValid_());
|
||
carat_dirty_ = true;
|
||
}
|
||
});
|
||
}
|
||
if (do_forward_delete) {
|
||
key_repeater_ = Repeater::New(
|
||
g_base->app_adapter->GetKeyRepeatDelay(),
|
||
g_base->app_adapter->GetKeyRepeatInterval(), [this] {
|
||
auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco33");
|
||
if (!unichars.empty()
|
||
&& carat_char_ < static_cast<int>(unichars.size())) {
|
||
assert(CaratCharValid_());
|
||
unichars.erase(unichars.begin() + carat_char_);
|
||
input_string_ = Utils::UTF8FromUnicode(unichars);
|
||
input_text_dirty_ = true;
|
||
carat_dirty_ = true; // Didn't move but might change size.
|
||
assert(CaratCharValid_());
|
||
}
|
||
});
|
||
}
|
||
if (do_carat_left || do_carat_right) {
|
||
key_repeater_ = Repeater::New(
|
||
g_base->app_adapter->GetKeyRepeatDelay(),
|
||
g_base->app_adapter->GetKeyRepeatInterval(),
|
||
[do_carat_left, do_carat_right, this] {
|
||
int offset = do_carat_right ? 1 : -1;
|
||
carat_char_ = std::clamp(
|
||
carat_char_ + offset, 0,
|
||
static_cast<int>(
|
||
Utils::UnicodeFromUTF8(input_string_, "fffwe").size()));
|
||
assert(CaratCharValid_());
|
||
carat_dirty_ = true;
|
||
});
|
||
}
|
||
|
||
if ((do_history_up || do_history_down) && !input_history_.empty()) {
|
||
if (do_history_up) {
|
||
input_history_position_++;
|
||
} else {
|
||
input_history_position_--;
|
||
}
|
||
int input_history_position_used =
|
||
(input_history_position_ - 1)
|
||
% static_cast<int>(input_history_.size());
|
||
int j = 0;
|
||
for (auto& i : input_history_) {
|
||
if (j == input_history_position_used) {
|
||
input_string_ = i;
|
||
carat_char_ = static_cast<int>(
|
||
Utils::UnicodeFromUTF8(input_string_, "fffwe").size());
|
||
assert(CaratCharValid_());
|
||
input_text_dirty_ = true;
|
||
carat_dirty_ = true;
|
||
break;
|
||
}
|
||
j++;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// By default don't claim key events; we want to be able to show the
|
||
// console while still playing/navigating normally.
|
||
return false;
|
||
}
|
||
|
||
auto DevConsole::HandleTextEditing(const std::string& text) -> bool {
|
||
assert(g_base->InLogicThread());
|
||
if (state_ == State_::kInactive) {
|
||
return false;
|
||
}
|
||
assert(CaratCharValid_());
|
||
auto unichars = Utils::UnicodeFromUTF8(input_string_, "jfof8");
|
||
auto addunichars = Utils::UnicodeFromUTF8(text, "jfoef8");
|
||
unichars.insert(unichars.begin() + carat_char_, addunichars.begin(),
|
||
addunichars.end());
|
||
input_string_ = Utils::UTF8FromUnicode(unichars);
|
||
input_text_dirty_ = true;
|
||
carat_char_ += addunichars.size();
|
||
assert(CaratCharValid_());
|
||
carat_dirty_ = true;
|
||
return true;
|
||
}
|
||
|
||
auto DevConsole::HandleKeyRelease(const SDL_Keysym* keysym) -> bool {
|
||
// Any presses or releases cancels repeat actions.
|
||
key_repeater_.Clear();
|
||
|
||
// Otherwise absorb *all* key-ups when we're active.
|
||
return state_ != State_::kInactive;
|
||
}
|
||
|
||
void DevConsole::Exec() {
|
||
BA_PRECONDITION(g_base->InLogicThread());
|
||
if (!input_enabled_) {
|
||
Log(LogLevel::kWarning, "Console input is not allowed yet.");
|
||
return;
|
||
}
|
||
input_history_position_ = 0;
|
||
if (input_string_ == "clear") {
|
||
last_line_.clear();
|
||
output_lines_.clear();
|
||
} else {
|
||
SubmitPythonCommand_(input_string_);
|
||
}
|
||
input_history_.push_front(input_string_);
|
||
if (input_history_.size() > 100) {
|
||
input_history_.pop_back();
|
||
}
|
||
input_string_.resize(0);
|
||
carat_char_ = 0;
|
||
assert(CaratCharValid_());
|
||
input_text_dirty_ = true;
|
||
carat_dirty_ = true;
|
||
}
|
||
|
||
// Just for sanity testing.
|
||
auto DevConsole::CaratCharValid_() -> bool {
|
||
return carat_char_ >= 0
|
||
&& carat_char_ <= static_cast<int>(
|
||
Utils::UnicodeFromUTF8(input_string_, "fwewffe").size());
|
||
}
|
||
|
||
void DevConsole::SubmitPythonCommand_(const std::string& command) {
|
||
assert(g_base);
|
||
g_base->logic->event_loop()->PushCall([command, this] {
|
||
// These are always run in whichever context is 'visible'.
|
||
ScopedSetContext ssc(g_base->app_mode()->GetForegroundContext());
|
||
PythonCommand cmd(command, "<console>");
|
||
if (!g_core->user_ran_commands) {
|
||
g_core->user_ran_commands = true;
|
||
}
|
||
if (cmd.CanEval()) {
|
||
auto obj = cmd.Eval(true, nullptr, nullptr);
|
||
if (obj.Exists() && obj.Get() != Py_None) {
|
||
Print(obj.Repr() + "\n");
|
||
}
|
||
} else {
|
||
// Not eval-able; just exec it.
|
||
cmd.Exec(true, nullptr, nullptr);
|
||
}
|
||
});
|
||
}
|
||
|
||
void DevConsole::EnableInput() {
|
||
assert(g_base->InLogicThread());
|
||
input_enabled_ = true;
|
||
}
|
||
|
||
void DevConsole::Dismiss() {
|
||
assert(g_base->InLogicThread());
|
||
if (state_ == State_::kInactive) {
|
||
return;
|
||
}
|
||
|
||
state_prev_ = state_;
|
||
state_ = State_::kInactive;
|
||
transition_start_ = g_base->logic->display_time();
|
||
}
|
||
|
||
void DevConsole::ToggleState() {
|
||
assert(g_base->InLogicThread());
|
||
state_prev_ = state_;
|
||
switch (state_) {
|
||
case State_::kInactive:
|
||
state_ = State_::kMini;
|
||
// Can't muck with UI from code (potentially) called while iterating
|
||
// through UI. So defer it.
|
||
g_base->logic->event_loop()->PushCall([this] {
|
||
RefreshTabButtons_();
|
||
RefreshTabContents_();
|
||
});
|
||
break;
|
||
case State_::kMini:
|
||
state_ = State_::kFull;
|
||
// Can't muck with UI from code (potentially) called while iterating
|
||
// through UI. So defer it.
|
||
g_base->logic->event_loop()->PushCall([this] { RefreshTabContents_(); });
|
||
break;
|
||
case State_::kFull:
|
||
state_ = State_::kInactive;
|
||
break;
|
||
}
|
||
g_base->audio->PlaySound(g_base->assets->SysSound(SysSoundID::kBlip));
|
||
transition_start_ = g_base->logic->display_time();
|
||
}
|
||
|
||
void DevConsole::Print(const std::string& s_in) {
|
||
assert(g_base->InLogicThread());
|
||
std::string s = Utils::GetValidUTF8(s_in.c_str(), "cspr");
|
||
last_line_ += s;
|
||
std::vector<std::string> broken_up;
|
||
g_base->text_graphics->BreakUpString(
|
||
last_line_.c_str(), kDevConsoleStringBreakUpSize, &broken_up);
|
||
|
||
// Spit out all completed lines and keep the last one as lastline.
|
||
for (size_t i = 0; i < broken_up.size() - 1; i++) {
|
||
output_lines_.emplace_back(broken_up[i], g_base->logic->display_time());
|
||
if (output_lines_.size() > kDevConsoleLineLimit) {
|
||
output_lines_.pop_front();
|
||
}
|
||
}
|
||
last_line_ = broken_up[broken_up.size() - 1];
|
||
last_line_mesh_dirty_ = true;
|
||
}
|
||
|
||
auto DevConsole::Bottom_() const -> float {
|
||
float vh = g_base->graphics->screen_virtual_height();
|
||
|
||
float ratio =
|
||
(g_base->logic->display_time() - transition_start_) / kTransitionSeconds;
|
||
float bottom;
|
||
|
||
// NOTE: Originally I was tweaking this based on UI scale, but I decided
|
||
// that it would be a better idea to have a constant value everywhere.
|
||
// dev-consoles are not meant to be especially pretty and I think it is
|
||
// more important for them to be able to be written to a known hard-coded
|
||
// mini-size.
|
||
float mini_size = 100.0f;
|
||
|
||
// Now that we have tabs and drop-shadows hanging down, we have to
|
||
// overshoot the top of the screen when transitioning out.
|
||
float top_buffer = 100.0f;
|
||
if (state_ == State_::kMini) {
|
||
bottom = vh - mini_size;
|
||
} else {
|
||
bottom = vh - vh * kDevConsoleSize;
|
||
}
|
||
if (g_base->logic->display_time() - transition_start_ < kTransitionSeconds) {
|
||
float from_height;
|
||
if (state_prev_ == State_::kMini) {
|
||
from_height = vh - mini_size;
|
||
} else if (state_prev_ == State_::kFull) {
|
||
from_height = vh - vh * kDevConsoleSize;
|
||
} else {
|
||
from_height = vh + top_buffer;
|
||
}
|
||
float to_height;
|
||
if (state_ == State_::kMini) {
|
||
to_height = vh - mini_size;
|
||
} else if (state_ == State_::kFull) {
|
||
to_height = vh - vh * kDevConsoleSize;
|
||
} else {
|
||
to_height = vh + top_buffer;
|
||
}
|
||
bottom = to_height * ratio + from_height * (1.0 - ratio);
|
||
}
|
||
return bottom;
|
||
}
|
||
|
||
void DevConsole::Draw(FrameDef* frame_def) {
|
||
float bs = BaseScale();
|
||
RenderPass* pass = frame_def->overlay_front_pass();
|
||
|
||
// If we're not yet transitioning in for the first time OR have completed
|
||
// transitioning out, do nothing.
|
||
if (transition_start_ <= 0.0
|
||
|| (state_ == State_::kInactive
|
||
&& ((g_base->logic->display_time() - transition_start_)
|
||
>= kTransitionSeconds))) {
|
||
return;
|
||
}
|
||
|
||
float bottom = Bottom_();
|
||
|
||
float border_height{3.0f};
|
||
{
|
||
bg_mesh_.SetPositionAndSize(0, bottom, kDevConsoleZDepth,
|
||
pass->virtual_width(),
|
||
(pass->virtual_height() - bottom));
|
||
stripe_mesh_.SetPositionAndSize(0, bottom + 15.0f * bs, kDevConsoleZDepth,
|
||
pass->virtual_width(), 15.0f * bs);
|
||
border_mesh_.SetPositionAndSize(0, bottom - border_height * bs,
|
||
kDevConsoleZDepth, pass->virtual_width(),
|
||
border_height * bs);
|
||
{
|
||
SimpleComponent c(pass);
|
||
c.SetTransparent(true);
|
||
c.SetColor(0, 0, 0.1f, 0.9f);
|
||
c.DrawMesh(&bg_mesh_);
|
||
c.Submit();
|
||
if (python_terminal_visible_) {
|
||
c.SetColor(1.0f, 1.0f, 1.0f, 0.1f);
|
||
c.DrawMesh(&stripe_mesh_);
|
||
c.Submit();
|
||
}
|
||
c.SetColor(0.25f, 0.2f, 0.3f, 1.0f);
|
||
c.DrawMesh(&border_mesh_);
|
||
}
|
||
}
|
||
|
||
// Drop shadow.
|
||
{
|
||
SimpleComponent c(pass);
|
||
c.SetTransparent(true);
|
||
c.SetColor(0.03, 0, 0.09, 0.9f);
|
||
c.SetTexture(g_base->assets->SysTexture(SysTextureID::kSoftRectVertical));
|
||
{
|
||
auto scissor = c.ScopedScissor({0.0f, 0.0f, pass->virtual_width(),
|
||
bottom - (border_height * 0.75f) * bs});
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(pass->virtual_width() * 0.5f, bottom + 160.0f);
|
||
c.Scale(pass->virtual_width() * 1.2f, 600.0f);
|
||
c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1));
|
||
}
|
||
}
|
||
|
||
if (python_terminal_visible_) {
|
||
if (input_text_dirty_) {
|
||
input_text_group_.SetText(input_string_);
|
||
input_text_dirty_ = false;
|
||
}
|
||
{
|
||
SimpleComponent c(pass);
|
||
c.SetFlatness(1.0f);
|
||
c.SetTransparent(true);
|
||
c.SetColor(0.4f, 0.33f, 0.45f, 0.8f);
|
||
|
||
// Build.
|
||
int elem_count = built_text_group_.GetElementCount();
|
||
for (int e = 0; e < elem_count; e++) {
|
||
c.SetTexture(built_text_group_.GetElementTexture(e));
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(pass->virtual_width() - 115.0f * bs, bottom + 1.9f * bs,
|
||
kDevConsoleZDepth);
|
||
c.Scale(0.35f * bs, 0.35f * bs, 1.0f);
|
||
c.DrawMesh(built_text_group_.GetElementMesh(e));
|
||
}
|
||
}
|
||
|
||
// Title.
|
||
elem_count = title_text_group_.GetElementCount();
|
||
for (int e = 0; e < elem_count; e++) {
|
||
c.SetTexture(title_text_group_.GetElementTexture(e));
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(10.0f * bs, bottom + 1.9f * bs, kDevConsoleZDepth);
|
||
c.Scale(0.35f * bs, 0.35f * bs, 1.0f);
|
||
c.DrawMesh(title_text_group_.GetElementMesh(e));
|
||
}
|
||
}
|
||
|
||
// Prompt.
|
||
elem_count = prompt_text_group_.GetElementCount();
|
||
for (int e = 0; e < elem_count; e++) {
|
||
c.SetTexture(prompt_text_group_.GetElementTexture(e));
|
||
c.SetColor(1, 1, 1, 1);
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(5.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth);
|
||
c.Scale(0.5f * bs, 0.5f * bs, 1.0f);
|
||
c.DrawMesh(prompt_text_group_.GetElementMesh(e));
|
||
}
|
||
}
|
||
|
||
// Input line.
|
||
elem_count = input_text_group_.GetElementCount();
|
||
for (int e = 0; e < elem_count; e++) {
|
||
c.SetTexture(input_text_group_.GetElementTexture(e));
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(15.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth);
|
||
c.Scale(0.5f * bs, 0.5f * bs, 1.0f);
|
||
c.DrawMesh(input_text_group_.GetElementMesh(e));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Carat.
|
||
if (!carat_mesh_.Exists() || carat_dirty_) {
|
||
// Note: we explicitly update here if carat is dirty because
|
||
// that updates last_carat_change_time_ which affects whether
|
||
// we draw or not. GetCaratX_() only updates it *if* we draw.
|
||
UpdateCarat_();
|
||
}
|
||
millisecs_t app_time = pass->frame_def()->app_time_millisecs();
|
||
millisecs_t since_change = app_time - last_carat_x_change_time_;
|
||
if (since_change < 300 || since_change % 1000 < 500) {
|
||
SimpleComponent c(pass);
|
||
c.SetTransparent(true);
|
||
c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShadow));
|
||
c.SetColor(0.8, 0.0, 1.0, 0.3f);
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
auto carat_x = GetCaratX_();
|
||
c.Translate(15.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth);
|
||
c.Scale(0.5f * bs, 0.5f * bs, 1.0f);
|
||
c.Translate(carat_x, 0.0f, 0.0f);
|
||
c.DrawMesh(carat_glow_mesh_.Get());
|
||
}
|
||
c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShadowSharp));
|
||
c.SetColor(1.0, 1.0, 1.0, 1.0f);
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
auto carat_x = GetCaratX_();
|
||
c.Translate(15.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth);
|
||
c.Scale(0.5f * bs, 0.5f * bs, 1.0f);
|
||
c.Translate(carat_x, 0.0f, 0.0f);
|
||
c.DrawMesh(carat_mesh_.Get());
|
||
}
|
||
}
|
||
|
||
// Output lines.
|
||
{
|
||
SimpleComponent c(pass);
|
||
c.SetTransparent(true);
|
||
c.SetColor(1, 1, 1, 1);
|
||
c.SetFlatness(1.0f);
|
||
float draw_scale = 0.6f;
|
||
float v_inc = 18.0f;
|
||
float h = 0.5f
|
||
* (g_base->graphics->screen_virtual_width()
|
||
- (kDevConsoleStringBreakUpSize * draw_scale));
|
||
float v = bottom + 32.0f * bs;
|
||
if (!last_line_.empty()) {
|
||
if (last_line_mesh_dirty_) {
|
||
if (!last_line_mesh_group_.Exists()) {
|
||
last_line_mesh_group_ = Object::New<TextGroup>();
|
||
}
|
||
last_line_mesh_group_->SetText(last_line_);
|
||
last_line_mesh_dirty_ = false;
|
||
}
|
||
int elem_count = last_line_mesh_group_->GetElementCount();
|
||
for (int e = 0; e < elem_count; e++) {
|
||
c.SetTexture(last_line_mesh_group_->GetElementTexture(e));
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(h, v + 2, kDevConsoleZDepth);
|
||
c.Scale(draw_scale, draw_scale);
|
||
c.DrawMesh(last_line_mesh_group_->GetElementMesh(e));
|
||
}
|
||
}
|
||
v += v_inc;
|
||
}
|
||
for (auto i = output_lines_.rbegin(); i != output_lines_.rend(); i++) {
|
||
int elem_count = i->GetText().GetElementCount();
|
||
for (int e = 0; e < elem_count; e++) {
|
||
c.SetTexture(i->GetText().GetElementTexture(e));
|
||
{
|
||
auto xf = c.ScopedTransform();
|
||
c.Translate(h, v + 2, kDevConsoleZDepth);
|
||
c.Scale(draw_scale, draw_scale);
|
||
c.DrawMesh(i->GetText().GetElementMesh(e));
|
||
}
|
||
}
|
||
v += v_inc;
|
||
if (v > pass->virtual_height() + v_inc) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Tab Buttons.
|
||
{
|
||
// Make sure we don't muck with our UI while we're in here.
|
||
auto lock = ScopedUILock_(this);
|
||
|
||
for (auto&& button : tab_buttons_) {
|
||
button->Draw(pass, bottom);
|
||
}
|
||
}
|
||
|
||
// Buttons.
|
||
{
|
||
// Make sure we don't muck with our UI while we're in here.
|
||
auto lock = ScopedUILock_(this);
|
||
|
||
for (auto&& button : widgets_) {
|
||
button->Draw(pass, bottom);
|
||
}
|
||
}
|
||
}
|
||
|
||
auto DevConsole::BaseScale() const -> float {
|
||
switch (g_base->ui->scale()) {
|
||
case UIScale::kLarge:
|
||
return 1.5f;
|
||
case UIScale::kMedium:
|
||
return 1.75f;
|
||
case UIScale::kSmall:
|
||
case UIScale::kLast:
|
||
return 2.0f;
|
||
}
|
||
FatalError("Unhandled scale.");
|
||
return 1.0f;
|
||
}
|
||
|
||
void DevConsole::StepDisplayTime() {
|
||
assert(g_base->InLogicThread());
|
||
|
||
// IMPORTANT: We can muck with UI here so make sure noone is iterating
|
||
// through or editing it.
|
||
assert(!ui_lock_count_);
|
||
|
||
// If we're inactive, blow away all our stuff once we transition fully
|
||
// off screen. This will kill any Python stuff attached to our widgets
|
||
// so things can clean themselves up.
|
||
if (state_ == State_::kInactive && !tab_buttons_.empty()) {
|
||
if ((g_base->logic->display_time() - transition_start_)
|
||
>= kTransitionSeconds) {
|
||
// Reset to a blank slate but *don't refresh anything (that will
|
||
// happen once we get vis'ed again).
|
||
tab_buttons_.clear();
|
||
widgets_.clear();
|
||
python_terminal_visible_ = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
auto DevConsole::PasteFromClipboard() -> bool {
|
||
if (state_ != State_::kInactive) {
|
||
if (python_terminal_visible_) {
|
||
if (g_base->app_adapter->ClipboardIsSupported()) {
|
||
if (g_base->app_adapter->ClipboardHasText()) {
|
||
auto text = g_base->app_adapter->ClipboardGetText();
|
||
if (strstr(text.c_str(), "\n") || strstr(text.c_str(), "\r")) {
|
||
g_base->audio->PlaySound(
|
||
g_base->assets->SysSound(SysSoundID::kErrorBeep));
|
||
ScreenMessage("Can only paste single lines of text.",
|
||
Vector3f(1.0f, 0.0f, 0.0f));
|
||
} else {
|
||
HandleTextEditing(text);
|
||
}
|
||
// Ok, we either pasted or complained, so consider it handled.
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void DevConsole::UpdateCarat_() {
|
||
last_carat_x_change_time_ = g_core->GetAppTimeMillisecs();
|
||
auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjfwef");
|
||
auto unichars_clamped = unichars;
|
||
|
||
unichars_clamped.resize(carat_char_);
|
||
auto clamped_str = Utils::UTF8FromUnicode(unichars_clamped);
|
||
carat_x_ = g_base->text_graphics->GetStringWidth(clamped_str);
|
||
|
||
// Use a base width if we're not covering a char, and use the char's width
|
||
// if we are.
|
||
float width = 14.0f;
|
||
if (carat_char_ < static_cast<int>(unichars.size())) {
|
||
std::vector<uint32_t> covered_char{unichars[carat_char_]};
|
||
auto covered_char_str = Utils::UTF8FromUnicode(covered_char);
|
||
width =
|
||
std::max(3.0f, g_base->text_graphics->GetStringWidth(covered_char_str));
|
||
}
|
||
|
||
float height = 32.0f;
|
||
float x_extend = 15.0f;
|
||
float y_extend = 20.0f;
|
||
float x_offset = 2.0f;
|
||
float y_offset = -0.0f;
|
||
float corner_radius = 20.0f;
|
||
float width_fin = width + x_extend * 2.0f;
|
||
float height_fin = height + y_extend * 2.0f;
|
||
float x_border =
|
||
NinePatchMesh::BorderForRadius(corner_radius, width_fin, height_fin);
|
||
float y_border =
|
||
NinePatchMesh::BorderForRadius(corner_radius, height_fin, width_fin);
|
||
carat_glow_mesh_ = Object::New<NinePatchMesh>(
|
||
-x_extend + x_offset, -y_extend + y_offset, 0.0f, width_fin, height_fin,
|
||
x_border, y_border, x_border, y_border);
|
||
|
||
corner_radius = 3.0f;
|
||
x_extend = 0.0f;
|
||
y_extend = -3.0f;
|
||
x_offset = 1.0f;
|
||
y_offset = 0.0f;
|
||
width_fin = width + x_extend * 2.0f;
|
||
height_fin = height + y_extend * 2.0f;
|
||
x_border =
|
||
NinePatchMesh::BorderForRadius(corner_radius, width_fin, height_fin);
|
||
y_border =
|
||
NinePatchMesh::BorderForRadius(corner_radius, height_fin, width_fin);
|
||
carat_mesh_ = Object::New<NinePatchMesh>(
|
||
-x_extend + x_offset, -y_extend + y_offset, 0.0f, width_fin, height_fin,
|
||
x_border, y_border, x_border, y_border);
|
||
}
|
||
|
||
auto DevConsole::GetCaratX_() -> float {
|
||
if (carat_dirty_) {
|
||
UpdateCarat_();
|
||
carat_dirty_ = false;
|
||
}
|
||
return carat_x_;
|
||
}
|
||
|
||
} // namespace ballistica::base
|