diff --git a/PanoPainter.vcxproj b/PanoPainter.vcxproj index 15437a4..d16925e 100644 --- a/PanoPainter.vcxproj +++ b/PanoPainter.vcxproj @@ -570,6 +570,14 @@ + + + true + true + true + true + + diff --git a/PanoPainter.vcxproj.filters b/PanoPainter.vcxproj.filters index f284f7f..bab1563 100644 --- a/PanoPainter.vcxproj.filters +++ b/PanoPainter.vcxproj.filters @@ -739,4 +739,9 @@ Resource Files + + + extras + + \ No newline at end of file diff --git a/data/layout.xml b/data/layout.xml index 84f6085..c777d3c 100644 --- a/data/layout.xml +++ b/data/layout.xml @@ -1,5 +1,8 @@ - + @@ -60,15 +63,15 @@ - + - + - + - + @@ -2009,5 +2012,6 @@ Here's a list of what's available in this release. --> - + + diff --git a/extra/layout.xsd b/extra/layout.xsd new file mode 100644 index 0000000..11d2739 --- /dev/null +++ b/extra/layout.xsd @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index 20832d3..de23d3b 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -113,7 +113,7 @@ void App::dialog_newdoc() dialog->btn_ok->on_click = [this, dialog](Node*) { - std::string name = dialog->input->m_string; + std::string name = dialog->input->m_text; std::string path = work_path + "/" + name + ".ppi"; if (name.empty()) @@ -381,7 +381,7 @@ void App::dialog_save() dialog->btn_ok->on_click = [this, dialog](Node*) { - std::string name = dialog->input->m_string; + std::string name = dialog->input->m_text; std::string path = work_path + "/" + name + ".ppi"; if (name.empty()) diff --git a/src/font.cpp b/src/font.cpp index 804e454..a1a41c1 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -83,7 +83,7 @@ std::vector TextMesh::tokenize(const std::string& s, const Font bool is_delim = std::find(delims.begin(), delims.end(), c) != delims.end(); wrap |= is_delim; // set wrap to notify a delimiter has been reached // when a new non-delim char is detected, start a new token - if (wrap && !is_delim) + if (wrap && !is_delim && !tmp.empty()) { parts.push_back(tmp); tmp.clear(); @@ -130,40 +130,61 @@ bool TextMesh::create() return true; } -void TextMesh::update(kFont id, const char* text) +void TextMesh::update(kFont id, const std::string& text) { font_id = id; auto& f = FontManager::get(id); + float spacing = f.bounds.w - f.bounds.y; + float avg_width = f.bounds.z - f.bounds.x; + cur_box = glm::vec4(0, 0, 5, spacing); + + glm::vec2 bbmin(FLT_MAX); + glm::vec2 bbmax(-FLT_MAX); + if (text.empty()) + { + bbmin = { 0, -spacing * 0.5f }; + bbmax = { 1, +spacing * 0.5f }; + } + if (f.chars.size()) { float x = 0; float y = 0; std::vector v; std::vector idx; - glm::vec2 bbmin(FLT_MAX); - glm::vec2 bbmax(-FLT_MAX); std::vector parts = max_width > 0 ? tokenize(text, f) : std::vector{ Token(text, 0.f) }; + if (parts.empty()) + { + bbmin = { 0, -spacing * 0.5f }; + bbmax = { 1, +spacing * 0.5f }; + } + for (auto p : parts) { - if (max_width > 0 && x + p.w > max_width * f.scale) + if (max_width > 0 && (x + p.w) * f.scale > max_width) { x = 0; - y += f.size * f.scale; + y += spacing; } for (char c : p.s) { if (c == '\n') { x = 0; - y += f.size * f.scale; + y += spacing; continue; } stbtt_aligned_quad q; stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, c - f.start_char, &x, &y, &q, true); + if (max_width > 0 && x * f.scale > max_width /*font scale factor*/) + { + x = 0; + y += spacing; + } auto n = (int)v.size(); v.emplace_back(q.x0 / f.scale, q.y1 / f.scale, q.s0, q.t1); v.emplace_back(q.x0 / f.scale, q.y0 / f.scale, q.s0, q.t0); @@ -177,6 +198,7 @@ void TextMesh::update(kFont id, const char* text) idx.push_back(n + 3); bbmin = glm::min(bbmin, { q.x0 / f.scale, q.y0 / f.scale }); bbmax = glm::max(bbmax, { q.x1 / f.scale, q.y1 / f.scale }); + cur_box = glm::vec4(x, y, 5, spacing); } } for (auto& vi : v) diff --git a/src/font.h b/src/font.h index f411d36..6e70d6e 100644 --- a/src/font.h +++ b/src/font.h @@ -58,7 +58,8 @@ public: GLuint font_buffers[2] = {0, 0}; kFont font_id; glm::vec2 bb = { 0, 0 }; + glm::vec4 cur_box; bool create(); - void update(kFont id, const char* text); + void update(kFont id, const std::string& text); void draw(); }; diff --git a/src/node.cpp b/src/node.cpp index 1aecbce..0fc32fd 100644 --- a/src/node.cpp +++ b/src/node.cpp @@ -1036,7 +1036,7 @@ void Node::update_internal(const glm::vec2& origin, const glm::mat4& proj, float m_mvp = proj * pos * scale * pivot * prescale; m_proj = proj; - if (m_size != old_size || m_zoom != zoom) + if (!glm::any(glm::isnan(m_size)) && m_size != old_size || m_zoom != zoom) { m_zoom = zoom; handle_resize(old_size, m_size, zoom); diff --git a/src/node.h b/src/node.h index 3ca96c4..52e55d9 100644 --- a/src/node.h +++ b/src/node.h @@ -48,6 +48,9 @@ enum class kAttribute : uint16_t AutoSize = const_hash("autosize"), MouseCapture = const_hash("mouse-capture"), ScrollColor = const_hash("scroll-color"), + Multiline = const_hash("multiline"), + TextAlign = const_hash("text-align"), + TextVerticalAlign = const_hash("text-vertical-align"), }; enum class kWidget : uint16_t diff --git a/src/node_dialog_layer_rename.cpp b/src/node_dialog_layer_rename.cpp index bec72da..07cd302 100644 --- a/src/node_dialog_layer_rename.cpp +++ b/src/node_dialog_layer_rename.cpp @@ -43,5 +43,5 @@ void NodeDialogLayerRename::loaded() std::string NodeDialogLayerRename::get_name() { - return input ? input->m_string : ""; + return input ? input->m_text : ""; } diff --git a/src/node_input_box.cpp b/src/node_input_box.cpp index 347b891..02b8cfc 100644 --- a/src/node_input_box.cpp +++ b/src/node_input_box.cpp @@ -22,7 +22,7 @@ void NodeInputBox::init() btn_ok = m_template->find("btn-ok"); btn_ok->on_click = [&](Node*) { if (on_submit) - on_submit(this, m_field_text->m_text->m_text); + on_submit(this, m_field_text->m_text); }; btn_cancel = m_template->find("btn-cancel"); btn_cancel->on_click = [&](Node*) { destroy(); }; @@ -39,7 +39,7 @@ kEventResult NodeInputBox::handle_event(Event* e) break; case kEventType::KeyUp: if (ke->m_key == kKey::KeyEnter && on_submit) - on_submit(this, m_field_text->m_text->m_text); + on_submit(this, m_field_text->m_text); break; case kEventType::KeyChar: break; diff --git a/src/node_text.cpp b/src/node_text.cpp index ce11c96..f3305f7 100644 --- a/src/node_text.cpp +++ b/src/node_text.cpp @@ -14,7 +14,7 @@ void NodeText::clone_copy(Node* dest) const NodeText* n = static_cast(dest); n->m_text_mesh.max_width = m_text_mesh.max_width; n->m_text_mesh.create(); - n->m_text_mesh.update(font_id, m_text.c_str()); + n->m_text_mesh.update(font_id, m_text); n->m_text = m_text; n->m_font = m_font; n->m_color = m_color; @@ -31,7 +31,7 @@ void NodeText::create() sprintf(font, "%s-%d", m_font.c_str(), m_font_size); font_id = (kFont)const_hash(font); m_text_mesh.create(); - m_text_mesh.update(font_id, m_text.c_str()); + m_text_mesh.update(font_id, m_text); SetSize(m_text_mesh.bb); } } @@ -40,7 +40,7 @@ void NodeText::set_font(kFont fontID) { font_id = fontID; m_text_mesh.create(); - m_text_mesh.update(font_id, m_text.c_str()); + m_text_mesh.update(font_id, m_text); SetSize(m_text_mesh.bb); } @@ -116,5 +116,6 @@ void NodeText::draw() void NodeText::handle_resize(glm::vec2 old_size, glm::vec2 new_size, float zoom) { - m_text_mesh.update(font_id, m_text.c_str()); + if (old_size != new_size) + m_text_mesh.update(font_id, m_text); } diff --git a/src/node_text_input.cpp b/src/node_text_input.cpp index d435d6e..97f14e7 100644 --- a/src/node_text_input.cpp +++ b/src/node_text_input.cpp @@ -14,53 +14,39 @@ void NodeTextInput::on_tick(float dt) timer += dt; bool focus = root()->current_key_capture.get() == this; - if (m_cursor && !focus) - m_cursor->m_display = false; + if (!focus) + m_cursor_visible = false; m_thinkness = focus; if (timer > 1.0) { timer = 0; - if (focus && m_cursor) + if (focus) { - m_cursor->m_display = !m_cursor->m_display; + m_cursor_visible = !m_cursor_visible; app_redraw(); } } } -void NodeTextInput::clone_finalize(Node* dest) const +void NodeTextInput::clone_copy(Node* dest) const { NodeBorder::clone_copy(dest); NodeTextInput* n = static_cast(dest); - if (n->m_children.size()) - { - n->m_text = (NodeText*)n->m_children[0].get(); - n->m_cursor = (NodeBorder*)n->m_children[1].get(); - } - n->m_string = m_string; -} + n->m_multiline = m_multiline; -void NodeTextInput::init() -{ - init_controls(); -} - -void NodeTextInput::init_controls() -{ - m_text = new NodeText; - add_child(m_text); - m_text->m_font = "arial"; - m_text->m_font_size = 11; - m_text->m_text = "TextInput"; - m_text->create(); - m_string = "TextInput"; - YGNodeStyleSetFlexDirection(y_node, YGFlexDirectionRow); - m_cursor = add_child(); - m_cursor->SetWidth(4); - m_cursor->SetHeightP(100); - m_cursor->SetMargin(0, 0, 0, 2); + n->m_text_mesh.max_width = m_text_mesh.max_width; + n->m_text_mesh.create(); + n->m_text_mesh.update(font_id, m_text); + n->m_text = m_text; + n->m_font = m_font; + n->m_color = m_color; + n->m_font_size = m_font_size; + n->font_id = font_id; + n->m_off = m_off; + n->m_text_align_v = m_text_align_v; + n->m_text_align_h = m_text_align_h; } kEventResult NodeTextInput::handle_event(Event* e) @@ -74,15 +60,14 @@ kEventResult NodeTextInput::handle_event(Event* e) case kEventType::MouseUpL: key_capture(); timer = 0; - if (m_cursor) - m_cursor->m_display = true; + m_cursor_visible = true; break; case kEventType::KeyDown: //switch (ke->m_key) //{ //case VK_BACK: // m_string.erase(m_string.end() - 1); - // m_text->set_text(m_string.c_str()); + // m_text->set_text(m_string); // break; //default: // break; @@ -93,38 +78,42 @@ kEventResult NodeTextInput::handle_event(Event* e) on_return(this); if (ke->m_key == kKey::KeyBackspace) { - if (!m_string.empty()) + if (!m_text.empty()) { - m_string.erase(m_string.end() - 1); - m_text->set_text(m_string.c_str()); + m_text.erase(m_text.end() - 1); + set_text(m_text); } } break; case kEventType::KeyChar: - if (m_cursor) - { - timer = 0; - m_cursor->m_display = true; - } + timer = 0; + m_cursor_visible = true; if (ke->m_char == '\b' // backspace || ke->m_char == 0x7f // DEL ) { - if (!m_string.empty()) + if (!m_text.empty()) { - m_string.erase(m_string.end() - 1); - m_text->set_text(m_string.c_str()); + m_text.erase(m_text.end() - 1); + set_text(m_text); } } else if (ke->m_char == '\n' || ke->m_char == '\r') // enter/return { - if (on_return) + if (m_multiline) + { + m_text += '\n'; + set_text(m_text); + } + else if (on_return) + { on_return(this); + } } else if (ke->m_char >= 32 && ke->m_char < (32 + 96)) { - m_string += (char)ke->m_char; - m_text->set_text(m_string.c_str()); + m_text += (char)ke->m_char; + set_text(m_text); } break; default: @@ -136,9 +125,117 @@ kEventResult NodeTextInput::handle_event(Event* e) void NodeTextInput::set_text(const std::string& s) { - if (m_text) - m_text->set_text(s.c_str()); - m_string = s; + m_text = s; + m_text_mesh.update(font_id, s); + update_layout(); +} + +void NodeTextInput::set_text_format(const char* fmt, ...) +{ + static char buffer[4096]; + va_list args; + va_start(args, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end(args); + m_text = buffer; + m_text_mesh.update(font_id, buffer); + update_layout(); +} + +void NodeTextInput::update_layout() +{ + auto pad = GetPadding(); + float h = GetHeight() - (pad[1] + pad[3]); + float w = GetWidth() - (pad[0] + pad[2]); + + if (m_text_align_v == TextAlign::Begin) + m_off.y = pad[0]; + else if (m_text_align_v == TextAlign::Center) + m_off.y = (h - m_text_mesh.bb.y) * 0.5f + pad[0]; + else + m_off.y = (h - m_text_mesh.bb.y) + pad[0]; + + if (m_text_align_h == TextAlign::Begin) + m_off.x = pad[3]; + else if (m_text_align_v == TextAlign::Center) + m_off.x = (w - m_text_mesh.bb.x) * 0.5f + pad[3]; + else + m_off.x = (w - m_text_mesh.bb.x) + pad[3]; +} + +void NodeTextInput::handle_resize(glm::vec2 old_size, glm::vec2 new_size, float zoom) +{ + auto pad = GetPadding(); + m_text_mesh.max_width = m_multiline ? new_size.x - (pad[1] + pad[3]) : 0; + update_layout(); +} + +void NodeTextInput::create() +{ + NodeBorder::create(); + if (!m_font.empty()) + { + char font[64]; + sprintf(font, "%s-%d", m_font.c_str(), m_font_size); + font_id = (kFont)const_hash(font); + m_text_mesh.create(); + m_text_mesh.update(font_id, m_text); + update_layout(); + } +} + +void NodeTextInput::draw() +{ + NodeBorder::draw(); + + glm::mat4 pos = glm::translate(glm::vec3(glm::floor(m_pos + m_off), 0)); + ShaderManager::use(kShader::Font); + ShaderManager::u_int(kShaderUniform::Tex, 0); + ShaderManager::u_mat4(kShaderUniform::MVP, m_proj * pos); + ShaderManager::u_vec4(kShaderUniform::Col, m_color); + glEnable(GL_BLEND); + m_text_mesh.draw(); + glDisable(GL_BLEND); + + if (m_cursor_visible) + { + glDisable(GL_BLEND); + glm::mat4 cur_pos = glm::translate(glm::vec3(m_pos + m_off + xy(m_text_mesh.cur_box), 0)); + glm::mat4 cur_scale = glm::scale(glm::vec3(zw(m_text_mesh.cur_box), 1)); + glm::mat4 cur_pivot = glm::translate(glm::vec3(0.5, 0.5, 0)); + ShaderManager::use(kShader::Color); + ShaderManager::u_mat4(kShaderUniform::MVP, m_proj * cur_pos * cur_scale * cur_pivot); + ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 1 }); + m_plane.draw_fill(); + } +} + +void NodeTextInput::parse_attributes(kAttribute ka, const tinyxml2::XMLAttribute* attr) +{ + switch (ka) + { + case kAttribute::Multiline: + m_multiline = attr->BoolValue(); + break; + case kAttribute::TextAlign: + if (strcmp(attr->Value(), "left") == 0) + m_text_align_h = TextAlign::Begin; + else if (strcmp(attr->Value(), "center") == 0) + m_text_align_h = TextAlign::Center; + else if (strcmp(attr->Value(), "right") == 0) + m_text_align_h = TextAlign::End; + break; + case kAttribute::TextVerticalAlign: + if (strcmp(attr->Value(), "top") == 0) + m_text_align_v = TextAlign::Begin; + else if (strcmp(attr->Value(), "center") == 0) + m_text_align_v = TextAlign::Center; + else if (strcmp(attr->Value(), "bottom") == 0) + m_text_align_v = TextAlign::End; + break; + default: + NodeBorder::parse_attributes(ka, attr); + } } void NodeTextInput::destroy() diff --git a/src/node_text_input.h b/src/node_text_input.h index 00375b6..1395a5c 100644 --- a/src/node_text_input.h +++ b/src/node_text_input.h @@ -1,22 +1,44 @@ #pragma once #include "node_border.h" #include "node_text.h" +#include "shape.h" class NodeTextInput : public NodeBorder { -public: float timer = 0; - NodeText* m_text; - NodeBorder* m_cursor; - NodeBorder* m_border; - std::string m_string; +public: + enum class TextAlign + { + Begin, + Center, + End, + }; + + TextMesh m_text_mesh; + std::string m_text; + std::string m_font = "arial"; + glm::vec4 m_color{ 1, 1, 1, 1 }; + int m_font_size = 11; + kFont font_id; + bool m_multiline = false; + bool m_cursor_visible = false; + glm::vec2 m_off; + TextAlign m_text_align_v = TextAlign::Center; + TextAlign m_text_align_h = TextAlign::Begin; + std::function on_return; + virtual Node* clone_instantiate() const override; - virtual void clone_finalize(Node* dest) const override; - virtual void init() override; + virtual void clone_copy(Node* dest) const override; virtual void on_tick(float dt) override; virtual kEventResult handle_event(Event* e) override; virtual void destroy() override; - void init_controls(); + virtual void parse_attributes(kAttribute ka, const tinyxml2::XMLAttribute* attr) override; + virtual void draw() override; + virtual void create() override; + virtual void handle_resize(glm::vec2 old_size, glm::vec2 new_size, float zoom) override; + void set_text(const std::string& s); + void set_text_format(const char* fmt, ...); + void update_layout(); };