mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-30 11:13:17 +08:00
1010 lines
33 KiB
C++
1010 lines
33 KiB
C++
// Released under the MIT License. See LICENSE for details.
|
|
|
|
#include "ballistica/ui_v1/widget/text_widget.h"
|
|
|
|
#include "ballistica/base/audio/audio.h"
|
|
#include "ballistica/base/graphics/component/empty_component.h"
|
|
#include "ballistica/base/graphics/component/simple_component.h"
|
|
#include "ballistica/base/graphics/text/text_graphics.h"
|
|
#include "ballistica/base/input/device/keyboard_input.h"
|
|
#include "ballistica/base/input/input.h"
|
|
#include "ballistica/base/logic/logic.h"
|
|
#include "ballistica/base/platform/base_platform.h"
|
|
#include "ballistica/base/python/base_python.h"
|
|
#include "ballistica/base/python/support/python_context_call.h"
|
|
#include "ballistica/shared/generic/utils.h"
|
|
#include "ballistica/shared/python/python.h"
|
|
#include "ballistica/shared/python/python_sys.h"
|
|
#include "ballistica/ui_v1/python/ui_v1_python.h"
|
|
#include "ballistica/ui_v1/widget/container_widget.h"
|
|
|
|
namespace ballistica::ui_v1 {
|
|
|
|
const float kClearMargin{13.0f};
|
|
|
|
TextWidget::TextWidget() {
|
|
// We always show our clear button except for in android when we don't
|
|
// have a touchscreen (android-tv type situations).
|
|
//
|
|
// FIXME - should generalize this to any controller-only situation.
|
|
if (g_buildconfig.ostype_android()) {
|
|
if (g_base->input->touch_input() == nullptr) {
|
|
do_clear_button_ = false;
|
|
}
|
|
}
|
|
birth_time_millisecs_ =
|
|
static_cast<millisecs_t>(g_base->logic->display_time() * 1000.0);
|
|
}
|
|
|
|
TextWidget::~TextWidget() = default;
|
|
|
|
void TextWidget::set_on_return_press_call(PyObject* call_tuple) {
|
|
on_return_press_call_ = Object::New<base::PythonContextCall>(call_tuple);
|
|
}
|
|
|
|
void TextWidget::set_on_activate_call(PyObject* call_tuple) {
|
|
on_activate_call_ = Object::New<base::PythonContextCall>(call_tuple);
|
|
}
|
|
|
|
void TextWidget::SetWidth(float width_in) {
|
|
highlight_dirty_ = outline_dirty_ = true;
|
|
width_ = width_in;
|
|
}
|
|
|
|
void TextWidget::SetHeight(float height_in) {
|
|
highlight_dirty_ = outline_dirty_ = true;
|
|
height_ = height_in;
|
|
}
|
|
|
|
void TextWidget::SetEditable(bool e) {
|
|
if (e == editable_) {
|
|
return;
|
|
}
|
|
|
|
// We don't translate when editable=true; need to refresh it.
|
|
text_translation_dirty_ = true;
|
|
editable_ = e;
|
|
|
|
// Deselect us if we're selected.... update: why do we do this?
|
|
if (!editable_ && !selectable_ && selected() && parent_widget())
|
|
parent_widget()->SelectWidget(nullptr);
|
|
}
|
|
|
|
void TextWidget::SetEnabled(bool val) {
|
|
enabled_ = val;
|
|
|
|
// Deselect us if we're selected.
|
|
if (!enabled_ && selected() && parent_widget()) {
|
|
parent_widget()->SelectWidget(nullptr);
|
|
}
|
|
}
|
|
|
|
void TextWidget::Draw(base::RenderPass* pass, bool draw_transparent) {
|
|
// All our stuff currently happens in the transparent pass.
|
|
if (!draw_transparent) {
|
|
return;
|
|
}
|
|
|
|
millisecs_t current_time = pass->frame_def()->display_time_millisecs();
|
|
|
|
float l = padding_;
|
|
float r = l + width_ - padding_;
|
|
float b = padding_;
|
|
float t = b + height_ - padding_;
|
|
|
|
// If we're on a button or something, add tilt.
|
|
{
|
|
float tilt_scale = draw_control_parent() ? 0.04f : 0.01f;
|
|
Vector3f tilt = tilt_scale * g_base->graphics->tilt();
|
|
l -= tilt.y;
|
|
r -= tilt.y;
|
|
b += tilt.x;
|
|
t += tilt.x;
|
|
}
|
|
|
|
// Center-scale.
|
|
{
|
|
// We should really be scaling our bounds and things, but for now lets
|
|
// just do a hacky overall scale.
|
|
base::EmptyComponent c(pass);
|
|
c.SetTransparent(true);
|
|
|
|
// FIXME(ericf): This component has an unmatched push and we have
|
|
// another component at the end with the matching pop. This only works
|
|
// because the components in the middle wind up writing to the same draw
|
|
// list, but there is nothing checking or enforcing that so it would be
|
|
// easy to break. Should improve this somehow. (perhaps by using a single
|
|
// component and enforcing list uniformity between push/pop blocks?)
|
|
c.PushTransform();
|
|
|
|
// Move to middle, scale down, move back.
|
|
float offs_x = (r + l) / 2;
|
|
float offs_y = (t + b) / 2;
|
|
c.Translate(offs_x, offs_y, 0);
|
|
c.Scale(center_scale_, center_scale_, 1.0f);
|
|
c.Translate(-offs_x, -offs_y, 0);
|
|
c.Submit();
|
|
}
|
|
|
|
if (editable() || IsSelectable()) {
|
|
float bound_l = l - padding_;
|
|
float bound_r = r + padding_;
|
|
float bound_t = t + padding_;
|
|
float bound_b = b - padding_;
|
|
float border_width = -2;
|
|
float border_height = -2;
|
|
|
|
// Draw highlight.
|
|
if ((IsSelectable()
|
|
&& ((selected() && always_highlight_) || IsHierarchySelected())
|
|
&& (always_highlight_ || g_base->ui->ShouldHighlightWidgets()))
|
|
|| ((pressed_ && mouse_over_)
|
|
|| (current_time - last_activate_time_millisecs_ < 200))) {
|
|
float m;
|
|
|
|
// Only pulsate if regular widget highlighting is on.
|
|
if (g_base->ui->ShouldHighlightWidgets()) {
|
|
if (IsHierarchySelected()) {
|
|
m = 0.5f
|
|
+ std::abs(sinf(static_cast<float>(current_time) * 0.006467f)
|
|
* 0.4f);
|
|
} else if (always_highlight_) {
|
|
m = 0.7f;
|
|
} else {
|
|
m = 0.25f;
|
|
}
|
|
} else {
|
|
m = 0.7f;
|
|
}
|
|
|
|
if (highlight_dirty_) {
|
|
float l_border, r_border, b_border, t_border;
|
|
float l2 = bound_l;
|
|
float r2 = bound_r;
|
|
float t2 = bound_t;
|
|
float b2 = bound_b;
|
|
l_border = std::max(10.0f, (r2 - l2) * 0.05f);
|
|
r_border = 0;
|
|
b_border = std::max(16.0f, (t2 - b2) * 0.16f);
|
|
t_border = std::max(14.0f, (t2 - b2) * 0.14f);
|
|
highlight_width_ = r2 - l2 + l_border + r_border;
|
|
highlight_height_ = t2 - b2 + b_border + t_border;
|
|
highlight_center_x_ = l2 - l_border + highlight_width_ * 0.5f;
|
|
highlight_center_y_ = b2 - b_border + highlight_height_ * 0.5f;
|
|
highlight_dirty_ = false;
|
|
}
|
|
|
|
base::SimpleComponent c(pass);
|
|
c.SetTransparent(true);
|
|
c.SetPremultiplied(true);
|
|
c.SetColor(0.25f * m, 0.3f * m, 0, 0.3f * m);
|
|
c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kGlow));
|
|
{
|
|
auto xf = c.ScopedTransform();
|
|
c.Translate(highlight_center_x_, highlight_center_y_, 0.1f);
|
|
c.Scale(highlight_width_, highlight_height_);
|
|
c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage4x1));
|
|
}
|
|
c.Submit();
|
|
}
|
|
|
|
// Outline.
|
|
if (editable()) {
|
|
if (outline_dirty_) {
|
|
float l_border = (r - l) * 0.04f;
|
|
float r_border = (r - l) * 0.02f;
|
|
float b_border = (t - b) * 0.07f;
|
|
float t_border = (t - b) * 0.16f;
|
|
outline_width_ = r - l + l_border + r_border;
|
|
outline_height_ = t - b + b_border + t_border;
|
|
outline_center_x_ = l - l_border + outline_width_ * 0.5f;
|
|
outline_center_y_ = b - b_border + outline_height_ * 0.5f;
|
|
outline_dirty_ = false;
|
|
}
|
|
base::SimpleComponent c(pass);
|
|
c.SetTransparent(true);
|
|
c.SetColor(1, 1, 1, 1);
|
|
c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kUIAtlas));
|
|
{
|
|
auto xf = c.ScopedTransform();
|
|
c.Translate(outline_center_x_, outline_center_y_, 0.1f);
|
|
c.Scale(outline_width_, outline_height_);
|
|
c.DrawMeshAsset(
|
|
g_base->assets->SysMesh(base::SysMeshID::kTextBoxTransparent));
|
|
}
|
|
c.Submit();
|
|
}
|
|
|
|
// Clear button.
|
|
if (editable() && (IsHierarchySelected() || always_show_carat_)
|
|
&& !text_raw_.empty() && do_clear_button_) {
|
|
base::SimpleComponent c(pass);
|
|
c.SetTransparent(true);
|
|
if (clear_pressed_ && clear_mouse_over_) {
|
|
c.SetColor(0.3f, 0.3f, 0.3f, 1);
|
|
} else {
|
|
c.SetColor(0.5f, 0.5f, 0.5f, 1);
|
|
}
|
|
c.SetTexture(
|
|
g_base->assets->SysTexture(base::SysTextureID::kTextClearButton));
|
|
{
|
|
auto xf = c.ScopedTransform();
|
|
c.Translate(r - 20, b * 0.5f + t * 0.5f, 0.1f);
|
|
if (g_base->ui->scale() == UIScale::kSmall) {
|
|
c.Scale(30, 30);
|
|
} else {
|
|
c.Scale(25, 25);
|
|
}
|
|
c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1));
|
|
}
|
|
c.Submit();
|
|
}
|
|
|
|
// Constrain drawing to our bounds.
|
|
if (editable()) {
|
|
base::EmptyComponent c(pass);
|
|
c.SetTransparent(true);
|
|
c.ScissorPush(Rect(l + border_width, b + border_height, r - border_width,
|
|
t - border_height));
|
|
c.Submit();
|
|
}
|
|
}
|
|
|
|
float x_offset, y_offset;
|
|
|
|
base::TextMesh::HAlign align_h;
|
|
base::TextMesh::VAlign align_v;
|
|
|
|
switch (alignment_h_) {
|
|
case HAlign::kLeft:
|
|
x_offset = l;
|
|
align_h = base::TextMesh::HAlign::kLeft;
|
|
break;
|
|
case HAlign::kCenter:
|
|
x_offset = (l + r) * 0.5f;
|
|
align_h = base::TextMesh::HAlign::kCenter;
|
|
break;
|
|
case HAlign::kRight:
|
|
x_offset = r;
|
|
align_h = base::TextMesh::HAlign::kRight;
|
|
break;
|
|
default:
|
|
throw Exception("Invalid HAlign");
|
|
}
|
|
switch (alignment_v_) {
|
|
case VAlign::kTop:
|
|
y_offset = t;
|
|
align_v = base::TextMesh::VAlign::kTop;
|
|
break;
|
|
case VAlign::kCenter:
|
|
y_offset = (b + t) * 0.5f;
|
|
align_v = base::TextMesh::VAlign::kCenter;
|
|
break;
|
|
case VAlign::kBottom:
|
|
y_offset = b;
|
|
align_v = base::TextMesh::VAlign::kBottom;
|
|
break;
|
|
default:
|
|
throw Exception("Invalid VAlign");
|
|
}
|
|
|
|
float transition =
|
|
(static_cast<float>(birth_time_millisecs_) + transition_delay_)
|
|
- static_cast<float>(current_time);
|
|
if (transition > 0) {
|
|
x_offset -= transition * 4.0f / (std::max(0.001f, center_scale_));
|
|
}
|
|
|
|
// Apply subs/resources to get our actual text if need be.
|
|
UpdateTranslation_();
|
|
|
|
if (!text_group_.Exists()) {
|
|
text_group_ = Object::New<base::TextGroup>();
|
|
}
|
|
if (text_group_dirty_) {
|
|
text_group_->SetText(text_translated_, align_h, align_v, big_, res_scale_);
|
|
text_width_ = g_base->text_graphics->GetStringWidth(text_translated_, big_);
|
|
|
|
// FIXME: doesnt support big.
|
|
text_height_ = g_base->text_graphics->GetStringHeight(text_translated_);
|
|
text_group_dirty_ = false;
|
|
}
|
|
|
|
// Calc scaling factors due to max width/height restrictions.
|
|
float max_width_scale = 1.0f;
|
|
float max_height_scale = 1.0f;
|
|
if (max_width_ > 0.0f && text_width_ > 0.0
|
|
&& ((text_width_ * center_scale_) > max_width_)) {
|
|
max_width_scale = max_width_ / (text_width_ * center_scale_);
|
|
}
|
|
// Currently cant do max-height with big.
|
|
assert(max_height_ <= 0.0 || !big_);
|
|
if (max_height_ > 0.0f && text_height_ > 0.0
|
|
&& ((text_height_ * center_scale_ * max_width_scale) > max_height_)) {
|
|
max_height_scale =
|
|
max_height_ / (text_height_ * center_scale_ * max_width_scale);
|
|
}
|
|
|
|
DoDrawText_(pass, x_offset, y_offset, max_width_scale, max_height_scale);
|
|
|
|
if (editable()) {
|
|
// Draw the carat.
|
|
DoDrawCarat_(pass, align_h, align_v, x_offset, y_offset, max_width_scale,
|
|
max_height_scale);
|
|
|
|
base::EmptyComponent c(pass);
|
|
c.SetTransparent(true);
|
|
c.ScissorPop();
|
|
c.Submit();
|
|
}
|
|
|
|
// Pop initial positioning.
|
|
{
|
|
base::EmptyComponent c(pass);
|
|
c.SetTransparent(true);
|
|
c.PopTransform();
|
|
c.Submit();
|
|
}
|
|
}
|
|
|
|
void TextWidget::DoDrawText_(base::RenderPass* pass, float x_offset,
|
|
float y_offset, float max_width_scale,
|
|
float max_height_scale) {
|
|
millisecs_t current_time{pass->frame_def()->display_time_millisecs()};
|
|
|
|
// Calc draw-brightness (for us and our children).
|
|
float color_mult = 1.0f;
|
|
if (Widget* draw_controller = draw_control_parent()) {
|
|
color_mult *= draw_controller->GetDrawBrightness(current_time);
|
|
}
|
|
|
|
float fin_a = enabled_ ? color_a_ : 0.4f * color_a_;
|
|
|
|
base::SimpleComponent c(pass);
|
|
c.SetTransparent(true);
|
|
|
|
if ((pressed_ && mouse_over_)
|
|
|| (current_time - last_activate_time_millisecs_ < 200)) {
|
|
color_mult *= 2.0f;
|
|
} else if (always_highlight_ && selected()) {
|
|
color_mult *= 1.4f;
|
|
}
|
|
float fin_color_r = color_r_ * color_mult;
|
|
float fin_color_g = color_g_ * color_mult;
|
|
float fin_color_b = color_b_ * color_mult;
|
|
|
|
int elem_count = text_group_->GetElementCount();
|
|
for (int e = 0; e < elem_count; e++) {
|
|
// Gracefully skip unloaded textures.
|
|
base::TextureAsset* t2 = text_group_->GetElementTexture(e);
|
|
if (!t2->preloaded()) {
|
|
continue;
|
|
}
|
|
c.SetTexture(t2);
|
|
c.SetMaskUV2Texture(text_group_->GetElementMaskUV2Texture(e));
|
|
c.SetShadow(-0.004f * text_group_->GetElementUScale(e),
|
|
-0.004f * text_group_->GetElementVScale(e), 0.0f,
|
|
shadow_ * color_a_);
|
|
if (text_group_->GetElementCanColor(e)) {
|
|
c.SetColor(fin_color_r, fin_color_g, fin_color_b, fin_a);
|
|
} else {
|
|
c.SetColor(1, 1, 1, fin_a);
|
|
}
|
|
|
|
// In VR, draw everything flat because it's generally harder to read.
|
|
if (g_core->IsVRMode()) {
|
|
c.SetFlatness(text_group_->GetElementMaxFlatness(e));
|
|
} else {
|
|
c.SetFlatness(std::min(text_group_->GetElementMaxFlatness(e), flatness_));
|
|
}
|
|
{
|
|
auto xf = c.ScopedTransform();
|
|
c.Translate(x_offset, y_offset, 0.1f);
|
|
if (rotate_ != 0.0f) {
|
|
c.Rotate(rotate_, 0, 0, 1);
|
|
}
|
|
if (max_width_scale != 1.0f) {
|
|
c.Scale(max_width_scale, max_width_scale);
|
|
}
|
|
if (max_height_scale != 1.0f) {
|
|
c.Scale(max_height_scale, max_height_scale);
|
|
}
|
|
c.DrawMesh(text_group_->GetElementMesh(e));
|
|
}
|
|
}
|
|
c.Submit();
|
|
}
|
|
|
|
void TextWidget::DoDrawCarat_(base::RenderPass* pass,
|
|
base::TextMesh::HAlign align_h,
|
|
base::TextMesh::VAlign align_v, float x_offset,
|
|
float y_offset, float max_width_scale,
|
|
float max_height_scale) {
|
|
millisecs_t current_time{pass->frame_def()->display_time_millisecs()};
|
|
if (IsHierarchySelected() || always_show_carat_) {
|
|
bool show_cursor = true;
|
|
if (ShouldUseStringEditor_()) {
|
|
show_cursor = false;
|
|
}
|
|
if (show_cursor
|
|
&& ((current_time / 100) % 2 == 0
|
|
|| (current_time - last_carat_change_time_millisecs_ < 250))) {
|
|
int str_size = Utils::UTF8StringLength(text_raw_.c_str());
|
|
if (carat_position_ > str_size) {
|
|
carat_position_ = str_size;
|
|
}
|
|
float h, v;
|
|
text_group_->GetCaratPts(text_raw_, align_h, align_v, carat_position_, &h,
|
|
&v);
|
|
base::SimpleComponent c(pass);
|
|
c.SetPremultiplied(true);
|
|
c.SetTransparent(true);
|
|
{
|
|
auto xf = c.ScopedTransform();
|
|
c.SetColor(0.17f, 0.12f, 0, 0);
|
|
c.Translate(x_offset, y_offset);
|
|
float max_width_height_scale = max_width_scale * max_height_scale;
|
|
c.Scale(max_width_height_scale, max_width_height_scale);
|
|
c.Translate(h + 4, v + 17.0f);
|
|
c.Scale(6, 27);
|
|
c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1));
|
|
c.SetColor(1, 1, 1, 0);
|
|
c.Scale(0.3f, 0.8f);
|
|
c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1));
|
|
}
|
|
c.Submit();
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextWidget::set_res_scale(float res_scale) {
|
|
if (res_scale != res_scale_) {
|
|
text_group_dirty_ = true;
|
|
}
|
|
res_scale_ = res_scale;
|
|
}
|
|
|
|
void TextWidget::SetText(const std::string& text_in_raw) {
|
|
std::string text_in = Utils::GetValidUTF8(text_in_raw.c_str(), "twst1");
|
|
|
|
// In some cases we want to make sure this is a valid resource-string
|
|
// since catching the error here is much more useful than if we catch
|
|
// it at draw-time. However this is expensive so we only do it for debug
|
|
// mode or if the string looks suspicious.
|
|
bool do_format_check{};
|
|
bool print_false_positives{};
|
|
|
|
// Only non-editable text support resource-strings.
|
|
if (!editable_) {
|
|
if (g_buildconfig.debug_build()) {
|
|
do_format_check = explicit_bool(true);
|
|
} else {
|
|
if (text_in_raw.size() > 1 && text_in_raw[0] == '{'
|
|
&& text_in_raw[text_in_raw.size() - 1] == '}') {
|
|
// Ok, its got bounds like json; now if its either missing quotes or a
|
|
// colon then let's check it.
|
|
if (!strstr(text_in_raw.c_str(), "\"")
|
|
|| !strstr(text_in_raw.c_str(), ":")) {
|
|
do_format_check = true;
|
|
|
|
// We wanna avoid doing this check when we don't have to.
|
|
// so lets print if we get a false positive
|
|
print_false_positives = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (do_format_check) {
|
|
bool valid;
|
|
g_base->assets->CompileResourceString(
|
|
text_in_raw, "TextWidget::set_text format check", &valid);
|
|
if (!valid) {
|
|
BA_LOG_ONCE(LogLevel::kError,
|
|
"Invalid resource string: '" + text_in_raw + "'");
|
|
Python::PrintStackTrace();
|
|
} else if (explicit_bool(print_false_positives)) {
|
|
BA_LOG_ONCE(LogLevel::kError,
|
|
"Got false positive for json check on '" + text_in_raw + "'");
|
|
Python::PrintStackTrace();
|
|
}
|
|
}
|
|
if (text_in != text_raw_) {
|
|
text_translation_dirty_ = true;
|
|
}
|
|
text_raw_ = text_in;
|
|
|
|
// Do our clamping in unicode-space.
|
|
if (Utils::UTF8StringLength(text_raw_.c_str()) > max_chars_) {
|
|
std::vector<uint32_t> uni = Utils::UnicodeFromUTF8(text_raw_, "fjcoiwef");
|
|
assert(max_chars_ >= 0);
|
|
uni.resize(static_cast<size_t>(max_chars_));
|
|
text_raw_ = Utils::UTF8FromUnicode(uni);
|
|
}
|
|
carat_position_ = 9999;
|
|
}
|
|
|
|
void TextWidget::SetBig(bool big) {
|
|
if (big != big_) {
|
|
text_group_dirty_ = true;
|
|
}
|
|
big_ = big;
|
|
}
|
|
|
|
// FIXME: Unify this with the drawing code.
|
|
auto TextWidget::GetWidth() -> float {
|
|
// Changing this to just return set width.
|
|
// What benefit would we get by returning adaptive vals?
|
|
return width_;
|
|
}
|
|
|
|
// FIXME: Unify this with the drawing code.
|
|
auto TextWidget::GetHeight() -> float {
|
|
// Changing this to just return set height.
|
|
// What benefit would we get by returning adaptive vals?
|
|
return height_;
|
|
}
|
|
|
|
void TextWidget::Activate() {
|
|
last_activate_time_millisecs_ =
|
|
static_cast<millisecs_t>(g_base->logic->display_time() * 1000.0);
|
|
|
|
if (auto* call = on_activate_call_.Get()) {
|
|
// Call this in the next cycle (don't wanna risk mucking with UI from
|
|
// within a UI loop).
|
|
call->ScheduleWeak();
|
|
}
|
|
|
|
// Bring up an editor if applicable.
|
|
if (editable_ && ShouldUseStringEditor_()) {
|
|
InvokeStringEditor_();
|
|
}
|
|
}
|
|
|
|
auto TextWidget::ShouldUseStringEditor_() const -> bool {
|
|
assert(!g_core->HeadlessMode()); // Should not get called here.
|
|
|
|
// Obscure cases such as the text-widget *on* our built-in on-screen
|
|
// editor (obviously it should itself not pop up an editor).
|
|
if (force_internal_editing_) {
|
|
return false;
|
|
}
|
|
|
|
// If the user wants to use our widget-based keyboard, always say yes
|
|
// here.
|
|
if (g_ui_v1->always_use_internal_on_screen_keyboard()) {
|
|
return true;
|
|
}
|
|
|
|
// If the UI is getting fed actual keyboard events, no string-editor needed.
|
|
return !g_base->ui->UIHasDirectKeyboardInput();
|
|
}
|
|
|
|
void TextWidget::InvokeStringEditor_() {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// If there's already a valid edit 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 args = PythonRef::Stolen(Py_BuildValue("(O)", BorrowPyRef()));
|
|
auto result = g_ui_v1->python->objs()
|
|
.Get(UIV1Python::ObjID::kTextWidgetStringEditAdapterClass)
|
|
.Call(args);
|
|
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 and hand it over
|
|
// to whoever will be driving it.
|
|
string_edit_adapter_ = result;
|
|
|
|
// Use the platform string-editor if we have one unless the user
|
|
// explicitly wants us to use our own.
|
|
if (g_base->platform->HaveStringEditor()
|
|
&& !g_ui_v1->always_use_internal_on_screen_keyboard()) {
|
|
g_base->platform->InvokeStringEditor(string_edit_adapter_.Get());
|
|
} else {
|
|
g_ui_v1->python->InvokeStringEditor(string_edit_adapter_.Get());
|
|
}
|
|
}
|
|
|
|
void TextWidget::AdapterFinished() {
|
|
BA_PRECONDITION(g_base->InLogicThread());
|
|
string_edit_adapter_.Release();
|
|
}
|
|
|
|
auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
|
|
if (g_core->HeadlessMode()) {
|
|
return false;
|
|
}
|
|
|
|
// How far outside our bounds touches register.
|
|
float left_overlap, top_overlap, right_overlap, bottom_overlap;
|
|
if (g_core->platform->IsRunningOnDesktop()) {
|
|
left_overlap = 0.0f;
|
|
top_overlap = 0.0f;
|
|
right_overlap = 0.0f;
|
|
bottom_overlap = 0.0f;
|
|
} else {
|
|
left_overlap = 3.0f * extra_touch_border_scale_;
|
|
top_overlap = 3.0f * extra_touch_border_scale_;
|
|
right_overlap = 3.0f * extra_touch_border_scale_;
|
|
bottom_overlap = 3.0f * extra_touch_border_scale_;
|
|
}
|
|
|
|
// If we're doing inline editing, handle clipboard paste.
|
|
if (editable() && !ShouldUseStringEditor_()
|
|
&& m.type == base::WidgetMessage::Type::kPaste) {
|
|
if (g_core->platform->ClipboardIsSupported()) {
|
|
if (g_core->platform->ClipboardHasText()) {
|
|
// Just enter it char by char as if we had typed it...
|
|
AddCharsToText_(g_core->platform->ClipboardGetText());
|
|
}
|
|
}
|
|
}
|
|
// If we're doing inline editing, handle some key events.
|
|
if (m.has_keysym && !ShouldUseStringEditor_()) {
|
|
last_carat_change_time_millisecs_ =
|
|
static_cast<millisecs_t>(g_base->logic->display_time() * 1000.0);
|
|
|
|
text_group_dirty_ = true;
|
|
bool claimed = false;
|
|
switch (m.keysym.sym) {
|
|
case SDLK_UP:
|
|
case SDLK_DOWN:
|
|
case SDLK_TAB:
|
|
// never claim up/down/tab
|
|
return false;
|
|
case SDLK_RETURN:
|
|
case SDLK_KP_ENTER:
|
|
if (g_buildconfig.ostype_ios_tvos() || g_buildconfig.ostype_android()) {
|
|
// On mobile, return currently just deselects us.
|
|
g_base->audio->PlaySound(
|
|
g_base->assets->SysSound(base::SysSoundID::kSwish));
|
|
parent_widget()->SelectWidget(nullptr);
|
|
return true;
|
|
} else {
|
|
if (auto* call = on_return_press_call_.Get()) {
|
|
claimed = true;
|
|
// Call this in the next cycle (don't wanna risk mucking with UI
|
|
// from within a UI loop)
|
|
call->ScheduleWeak();
|
|
}
|
|
}
|
|
break;
|
|
case SDLK_LEFT:
|
|
if (editable()) {
|
|
claimed = true;
|
|
if (carat_position_ > 0) {
|
|
carat_position_--;
|
|
}
|
|
}
|
|
break;
|
|
case SDLK_RIGHT:
|
|
if (editable()) {
|
|
claimed = true;
|
|
carat_position_++;
|
|
}
|
|
break;
|
|
case SDLK_BACKSPACE:
|
|
case SDLK_DELETE:
|
|
if (editable()) {
|
|
claimed = true;
|
|
std::vector<uint32_t> unichars =
|
|
Utils::UnicodeFromUTF8(text_raw_, "c94j8f");
|
|
auto len = static_cast<int>(unichars.size());
|
|
if (len > 0) {
|
|
if (carat_position_ > 0) {
|
|
int pos = carat_position_ - 1;
|
|
if (pos > len - 1) {
|
|
pos = len - 1;
|
|
}
|
|
unichars.erase(unichars.begin() + pos);
|
|
text_raw_ = Utils::UTF8FromUnicode(unichars);
|
|
text_translation_dirty_ = true;
|
|
carat_position_--;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (!claimed) {
|
|
// Pop in a char.
|
|
if (editable()) {
|
|
claimed = true;
|
|
|
|
// #if BA_SDL2_BUILD || BA_MINSDL_BUILD
|
|
// // On SDL2, chars come through as TEXT_INPUT messages;
|
|
// // can ignore this.
|
|
// #else
|
|
// std::vector<uint32_t> unichars =
|
|
// Utils::UnicodeFromUTF8(text_raw_, "2jf987");
|
|
// int len = static_cast<int>(unichars.size());
|
|
|
|
// if (len < max_chars_) {
|
|
// if ((m.keysym.unicode >= 32) && (m.keysym.sym != SDLK_TAB))
|
|
// {
|
|
// claimed = true;
|
|
// int pos = carat_position_;
|
|
// if (pos > len) pos = len;
|
|
// unichars.insert(unichars.begin() + pos,
|
|
// m.keysym.unicode); text_raw_ =
|
|
// Utils::UTF8FromUnicode(unichars); text_translation_dirty_
|
|
// = true; carat_position_++;
|
|
// } else {
|
|
// // These don't seem to come through cleanly as unicode:
|
|
// // FIXME - should re-check this on SDL2 builds
|
|
|
|
// claimed = true;
|
|
// std::string s;
|
|
// uint32_t pos = carat_position_;
|
|
// if (pos > len) pos = len;
|
|
// switch (m.keysym.sym) {
|
|
// case SDLK_KP0:
|
|
// s = '0';
|
|
// break;
|
|
// case SDLK_KP1:
|
|
// s = '1';
|
|
// break;
|
|
// case SDLK_KP2:
|
|
// s = '2';
|
|
// break;
|
|
// case SDLK_KP3:
|
|
// s = '3';
|
|
// break;
|
|
// case SDLK_KP4:
|
|
// s = '4';
|
|
// break;
|
|
// case SDLK_KP5:
|
|
// s = '5';
|
|
// break;
|
|
// case SDLK_KP6:
|
|
// s = '6';
|
|
// break;
|
|
// case SDLK_KP7:
|
|
// s = '7';
|
|
// break;
|
|
// case SDLK_KP8:
|
|
// s = '8';
|
|
// break;
|
|
// case SDLK_KP9:
|
|
// s = '9';
|
|
// break;
|
|
// case SDLK_KP_PERIOD:
|
|
// s = '.';
|
|
// break;
|
|
// case SDLK_KP_DIVIDE:
|
|
// s = '/';
|
|
// break;
|
|
// case SDLK_KP_MULTIPLY:
|
|
// s = '*';
|
|
// break;
|
|
// case SDLK_KP_MINUS:
|
|
// s = '-';
|
|
// break;
|
|
// case SDLK_KP_PLUS:
|
|
// s = '+';
|
|
// break;
|
|
// case SDLK_KP_EQUALS:
|
|
// s = '=';
|
|
// break;
|
|
// default:
|
|
// break;
|
|
// }
|
|
// if (s.size() > 0) {
|
|
// unichars.insert(unichars.begin() + pos, s[0]);
|
|
// text_raw_ = Utils::UTF8FromUnicode(unichars);
|
|
// text_translation_dirty_ = true;
|
|
// carat_position_++;
|
|
// }
|
|
// }
|
|
// }
|
|
// #endif // BA_SDL2_BUILD
|
|
}
|
|
}
|
|
return claimed;
|
|
}
|
|
switch (m.type) {
|
|
case base::WidgetMessage::Type::kTextInput: {
|
|
// If we're using an edit dialog, any attempted text input just kicks us
|
|
// over to that.
|
|
if (editable() && ShouldUseStringEditor_()) {
|
|
InvokeStringEditor_();
|
|
} else {
|
|
// Otherwise apply the text directly.
|
|
if (editable() && m.sval != nullptr) {
|
|
AddCharsToText_(*m.sval);
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case base::WidgetMessage::Type::kMouseMove: {
|
|
if (!IsSelectable()) {
|
|
return false;
|
|
}
|
|
float x{ScaleAdjustedX_(m.fval1)};
|
|
float y{ScaleAdjustedY_(m.fval2)};
|
|
bool claimed = (m.fval3 > 0.0f);
|
|
if (claimed) {
|
|
mouse_over_ = clear_mouse_over_ = false;
|
|
} else {
|
|
mouse_over_ =
|
|
((x >= (-left_overlap)) && (x < (width_ + right_overlap))
|
|
&& (y >= (-bottom_overlap)) && (y < (height_ + top_overlap)));
|
|
clear_mouse_over_ =
|
|
((x >= width_ - 35 - kClearMargin) && (x < width_ + kClearMargin)
|
|
&& (y > -kClearMargin) && (y < height_ + kClearMargin));
|
|
}
|
|
return mouse_over_;
|
|
}
|
|
case base::WidgetMessage::Type::kMouseDown: {
|
|
if (!IsSelectable()) {
|
|
return false;
|
|
}
|
|
float x{ScaleAdjustedX_(m.fval1)};
|
|
float y{ScaleAdjustedY_(m.fval2)};
|
|
|
|
auto click_count = static_cast<int>(m.fval3);
|
|
|
|
// See if a click is in our clear button.
|
|
if (editable() && (IsHierarchySelected() || always_show_carat_)
|
|
&& !text_raw_.empty() && (x >= width_ - 35)
|
|
&& (x < width_ + kClearMargin) && (y > -kClearMargin)
|
|
&& (y < height_ + kClearMargin) && do_clear_button_) {
|
|
clear_pressed_ = clear_mouse_over_ = true;
|
|
return true;
|
|
}
|
|
if ((x >= (-left_overlap)) && (x < (width_ + right_overlap))
|
|
&& (y >= (-bottom_overlap)) && (y < (height_ + top_overlap))) {
|
|
ContainerWidget* c = parent_widget();
|
|
if (c && IsSelectable()) {
|
|
// In cases where we have a keyboard, this also sets that as
|
|
// the ui input device. If we don't, an on-screen keyboard will
|
|
// likely pop up for the current input-device.
|
|
// FIXME: may need to test/tweak this behavior for cases where
|
|
// we pop up a UI dialog for text input..
|
|
if (editable()) {
|
|
if (auto* kb = g_base->input->keyboard_input()) {
|
|
g_base->ui->SetUIInputDevice(kb);
|
|
}
|
|
}
|
|
GlobalSelect();
|
|
pressed_ = true;
|
|
|
|
// Second click (or first if we want) puts us in
|
|
// potentially-activating-mode.
|
|
pressed_activate_ =
|
|
(click_count == 2 || click_activate_) && !editable_;
|
|
if (click_count == 1) {
|
|
g_base->audio->PlaySound(
|
|
g_base->assets->SysSound(base::SysSoundID::kTap));
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
case base::WidgetMessage::Type::kMouseUp: {
|
|
float x{ScaleAdjustedX_(m.fval1)};
|
|
float y{ScaleAdjustedY_(m.fval2)};
|
|
bool claimed = (m.fval3 > 0.0f);
|
|
|
|
if (clear_pressed_ && !claimed && editable()
|
|
&& (IsHierarchySelected() || always_show_carat_)
|
|
&& (!text_raw_.empty()) && (x >= width_ - 35 - kClearMargin)
|
|
&& (x < width_ + kClearMargin) && (y >= 0 - kClearMargin)
|
|
&& (y < height_ + kClearMargin)) {
|
|
text_raw_ = "";
|
|
text_translation_dirty_ = true;
|
|
carat_position_ = 0;
|
|
text_group_dirty_ = true;
|
|
clear_pressed_ = false;
|
|
g_base->audio->PlaySound(
|
|
g_base->assets->SysSound(base::SysSoundID::kTap));
|
|
return true;
|
|
}
|
|
clear_pressed_ = false;
|
|
if (pressed_) {
|
|
pressed_ = false;
|
|
|
|
// for non-editable text, mouse-ups within our region trigger an
|
|
// activate
|
|
if (pressed_activate_ && (x >= (-left_overlap))
|
|
&& (x < (width_ + right_overlap)) && (y >= (-bottom_overlap))
|
|
&& (y < (height_ + top_overlap)) && !claimed) {
|
|
Activate();
|
|
pressed_activate_ = false;
|
|
} else if (editable_ && ShouldUseStringEditor_()
|
|
&& (x >= (-left_overlap)) && (x < (width_ + right_overlap))
|
|
&& (y >= (-bottom_overlap)) && (y < (height_ + top_overlap))
|
|
&& !claimed) {
|
|
// With dialog-editing, a click/tap brings up our editor.
|
|
InvokeStringEditor_();
|
|
}
|
|
|
|
// Pressed buttons always claim mouse-ups presented to them.
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
auto TextWidget::ScaleAdjustedX_(float x) -> float {
|
|
// Account for our center_scale_ value.
|
|
float offsx = x - width_ * 0.5f;
|
|
return width_ * 0.5f + offsx / center_scale_;
|
|
}
|
|
|
|
auto TextWidget::ScaleAdjustedY_(float y) -> float {
|
|
// Account for our center_scale_ value.
|
|
float offsy = y - height_ * 0.5f;
|
|
return height_ * 0.5f + offsy / center_scale_;
|
|
}
|
|
|
|
void TextWidget::AddCharsToText_(const std::string& addchars) {
|
|
assert(editable());
|
|
std::vector<uint32_t> unichars = Utils::UnicodeFromUTF8(text_raw_, "jcjwf8f");
|
|
int len = static_cast<int>(unichars.size());
|
|
std::vector<uint32_t> sval = Utils::UnicodeFromUTF8(addchars, "j4958fbv");
|
|
for (unsigned int i : sval) {
|
|
if (len < max_chars_) {
|
|
text_group_dirty_ = true;
|
|
if (carat_position_ > len) {
|
|
carat_position_ = len;
|
|
}
|
|
unichars.insert(unichars.begin() + carat_position_, i);
|
|
len++;
|
|
carat_position_++;
|
|
}
|
|
}
|
|
text_raw_ = Utils::UTF8FromUnicode(unichars);
|
|
text_translation_dirty_ = true;
|
|
}
|
|
|
|
void TextWidget::UpdateTranslation_() {
|
|
// Apply subs/resources to get our actual text if need be.
|
|
if (text_translation_dirty_) {
|
|
// We don't run translations on user-editable text.
|
|
if (editable()) {
|
|
text_translated_ = text_raw_;
|
|
} else {
|
|
text_translated_ = g_base->assets->CompileResourceString(
|
|
text_raw_, "TextWidget::UpdateTranslation");
|
|
}
|
|
text_translation_dirty_ = false;
|
|
text_group_dirty_ = true;
|
|
}
|
|
}
|
|
|
|
auto TextWidget::GetTextWidth() -> float {
|
|
UpdateTranslation_();
|
|
|
|
// Should we cache this?
|
|
return g_base->text_graphics->GetStringWidth(text_translated_, big_);
|
|
}
|
|
|
|
void TextWidget::OnLanguageChange() { text_translation_dirty_ = true; }
|
|
|
|
} // namespace ballistica::ui_v1
|