2016-12-11 06:17:49 +00:00
|
|
|
#include "specter/RootView.hpp"
|
2016-03-04 23:03:47 +00:00
|
|
|
#include "specter/TextView.hpp"
|
|
|
|
#include "specter/ViewResources.hpp"
|
2015-11-25 01:46:30 +00:00
|
|
|
#include "utf8proc.h"
|
2015-11-21 23:45:02 +00:00
|
|
|
|
2015-11-26 07:35:43 +00:00
|
|
|
#include <freetype/internal/internal.h>
|
|
|
|
#include <freetype/internal/ftobjs.h>
|
|
|
|
|
2016-03-04 23:03:47 +00:00
|
|
|
namespace specter
|
2015-11-21 23:45:02 +00:00
|
|
|
{
|
2016-03-04 23:03:47 +00:00
|
|
|
static logvisor::Module Log("specter::TextView");
|
2015-11-25 01:46:30 +00:00
|
|
|
|
2017-12-06 03:25:33 +00:00
|
|
|
#if BOO_HAS_GL
|
2016-02-23 02:33:59 +00:00
|
|
|
static const char* GLSLVS =
|
|
|
|
"#version 330\n"
|
2016-02-24 03:19:07 +00:00
|
|
|
BOO_GLSL_BINDING_HEAD
|
2016-02-23 02:33:59 +00:00
|
|
|
"layout(location=0) in vec3 posIn[4];\n"
|
|
|
|
"layout(location=4) in mat4 mvMtx;\n"
|
|
|
|
"layout(location=8) in vec3 uvIn[4];\n"
|
|
|
|
"layout(location=12) in vec4 colorIn;\n"
|
2016-02-24 03:19:07 +00:00
|
|
|
SPECTER_GLSL_VIEW_VERT_BLOCK
|
2016-02-23 02:33:59 +00:00
|
|
|
"struct VertToFrag\n"
|
|
|
|
"{\n"
|
|
|
|
" vec3 uv;\n"
|
|
|
|
" vec4 color;\n"
|
|
|
|
"};\n"
|
2016-07-01 02:32:50 +00:00
|
|
|
"SBINDING(0) out VertToFrag vtf;\n"
|
2016-02-23 02:33:59 +00:00
|
|
|
"void main()\n"
|
|
|
|
"{\n"
|
2017-11-09 08:12:07 +00:00
|
|
|
" vec3 pos = posIn[gl_VertexID];\n"
|
2016-02-23 02:33:59 +00:00
|
|
|
" vtf.uv = uvIn[gl_VertexID];\n"
|
|
|
|
" vtf.color = colorIn * mulColor;\n"
|
2017-11-09 08:12:07 +00:00
|
|
|
" gl_Position = mv * mvMtx * vec4(pos, 1.0);\n"
|
2016-07-02 03:45:47 +00:00
|
|
|
" gl_Position = FLIPFROMGL(gl_Position);\n"
|
2016-02-23 02:33:59 +00:00
|
|
|
"}\n";
|
|
|
|
|
|
|
|
static const char* GLSLFSReg =
|
|
|
|
"#version 330\n"
|
2016-02-24 03:19:07 +00:00
|
|
|
BOO_GLSL_BINDING_HEAD
|
|
|
|
"TBINDING0 uniform sampler2DArray fontTex;\n"
|
2016-02-23 02:33:59 +00:00
|
|
|
"struct VertToFrag\n"
|
|
|
|
"{\n"
|
|
|
|
" vec3 uv;\n"
|
|
|
|
" vec4 color;\n"
|
|
|
|
"};\n"
|
2016-07-01 02:32:50 +00:00
|
|
|
"SBINDING(0) in VertToFrag vtf;\n"
|
2016-02-23 02:33:59 +00:00
|
|
|
"layout(location=0) out vec4 colorOut;\n"
|
|
|
|
"void main()\n"
|
|
|
|
"{\n"
|
|
|
|
" colorOut = vtf.color;\n"
|
|
|
|
" colorOut.a *= texture(fontTex, vtf.uv).r;\n"
|
|
|
|
"}\n";
|
|
|
|
|
|
|
|
static const char* GLSLFSSubpixel =
|
|
|
|
"#version 330\n"
|
2016-02-24 03:19:07 +00:00
|
|
|
BOO_GLSL_BINDING_HEAD
|
|
|
|
"TBINDING0 uniform sampler2DArray fontTex;\n"
|
2016-02-23 02:33:59 +00:00
|
|
|
"struct VertToFrag\n"
|
|
|
|
"{\n"
|
|
|
|
" vec3 uv;\n"
|
|
|
|
" vec4 color;\n"
|
|
|
|
"};\n"
|
2016-07-01 02:32:50 +00:00
|
|
|
"SBINDING(0) in VertToFrag vtf;\n"
|
2016-02-23 02:33:59 +00:00
|
|
|
"layout(location=0, index=0) out vec4 colorOut;\n"
|
|
|
|
"layout(location=0, index=1) out vec4 blendOut;\n"
|
|
|
|
"void main()\n"
|
|
|
|
"{\n"
|
|
|
|
" colorOut = vtf.color;\n"
|
|
|
|
" blendOut = colorOut.a * texture(fontTex, vtf.uv);\n"
|
|
|
|
"}\n";
|
|
|
|
|
2016-04-15 03:25:50 +00:00
|
|
|
static const char* BlockNames[] = {"SpecterViewBlock"};
|
2016-07-08 00:06:42 +00:00
|
|
|
static const char* TexNames[] = {"fontTex"};
|
2016-04-15 03:25:50 +00:00
|
|
|
|
2016-03-30 19:15:32 +00:00
|
|
|
void TextView::Resources::init(boo::GLDataFactory::Context& ctx, FontCache* fcache)
|
2015-11-25 01:46:30 +00:00
|
|
|
{
|
2015-11-26 00:24:01 +00:00
|
|
|
m_fcache = fcache;
|
|
|
|
|
|
|
|
m_regular =
|
2016-07-08 00:06:42 +00:00
|
|
|
ctx.newShaderPipeline(GLSLVS, GLSLFSReg, 1, TexNames, 1, BlockNames,
|
2016-03-30 19:15:32 +00:00
|
|
|
boo::BlendFactor::SrcAlpha, boo::BlendFactor::InvSrcAlpha,
|
2017-03-14 07:02:24 +00:00
|
|
|
boo::Primitive::TriStrips, boo::ZTest::None, false, true, false, boo::CullMode::None);
|
2015-11-25 01:46:30 +00:00
|
|
|
|
2015-11-26 00:24:01 +00:00
|
|
|
m_subpixel =
|
2016-07-08 00:06:42 +00:00
|
|
|
ctx.newShaderPipeline(GLSLVS, GLSLFSSubpixel, 1, TexNames, 1, BlockNames,
|
2016-03-30 19:15:32 +00:00
|
|
|
boo::BlendFactor::SrcColor1, boo::BlendFactor::InvSrcColor1,
|
2017-03-14 07:02:24 +00:00
|
|
|
boo::Primitive::TriStrips, boo::ZTest::None, false, true, false, boo::CullMode::None);
|
2015-11-25 01:46:30 +00:00
|
|
|
}
|
2017-12-06 03:25:33 +00:00
|
|
|
#endif
|
2015-11-25 01:46:30 +00:00
|
|
|
|
2015-11-28 01:40:59 +00:00
|
|
|
#if _WIN32
|
2015-11-28 04:07:09 +00:00
|
|
|
|
2016-03-30 19:15:32 +00:00
|
|
|
void TextView::Resources::init(boo::ID3DDataFactory::Context& ctx, FontCache* fcache)
|
2015-11-27 22:20:22 +00:00
|
|
|
{
|
|
|
|
m_fcache = fcache;
|
|
|
|
|
|
|
|
static const char* VS =
|
|
|
|
"struct VertData\n"
|
|
|
|
"{\n"
|
|
|
|
" float3 posIn[4] : POSITION;\n"
|
|
|
|
" float4x4 mvMtx : MODELVIEW;\n"
|
|
|
|
" float3 uvIn[4] : UV;\n"
|
|
|
|
" float4 colorIn : COLOR;\n"
|
|
|
|
"};\n"
|
2016-02-24 20:28:37 +00:00
|
|
|
SPECTER_HLSL_VIEW_VERT_BLOCK
|
2015-11-27 22:20:22 +00:00
|
|
|
"struct VertToFrag\n"
|
|
|
|
"{\n"
|
|
|
|
" float4 position : SV_Position;\n"
|
|
|
|
" float3 uv : UV;\n"
|
|
|
|
" float4 color : COLOR;\n"
|
|
|
|
"};\n"
|
|
|
|
"VertToFrag main(in VertData v, in uint vertId : SV_VertexID)\n"
|
|
|
|
"{\n"
|
|
|
|
" VertToFrag vtf;\n"
|
|
|
|
" vtf.uv = v.uvIn[vertId];\n"
|
2015-12-15 21:53:15 +00:00
|
|
|
" vtf.color = v.colorIn * mulColor;\n"
|
2015-11-27 22:20:22 +00:00
|
|
|
" vtf.position = mul(mv, mul(v.mvMtx, float4(v.posIn[vertId], 1.0)));\n"
|
|
|
|
" return vtf;\n"
|
|
|
|
"}\n";
|
|
|
|
|
|
|
|
static const char* FSReg =
|
|
|
|
"Texture2DArray fontTex : register(t0);\n"
|
|
|
|
"SamplerState samp : register(s0);\n"
|
|
|
|
"struct VertToFrag\n"
|
|
|
|
"{\n"
|
|
|
|
" float4 position : SV_Position;\n"
|
|
|
|
" float3 uv : UV;\n"
|
|
|
|
" float4 color : COLOR;\n"
|
|
|
|
"};\n"
|
|
|
|
"float4 main(in VertToFrag vtf) : SV_Target0\n"
|
|
|
|
"{\n"
|
|
|
|
" float4 colorOut = vtf.color;\n"
|
2015-12-07 00:52:07 +00:00
|
|
|
" colorOut.a *= fontTex.Sample(samp, vtf.uv).r;\n"
|
2015-11-27 22:20:22 +00:00
|
|
|
" return colorOut;\n"
|
|
|
|
"}\n";
|
|
|
|
|
|
|
|
static const char* FSSubpixel =
|
|
|
|
"Texture2DArray fontTex : register(t0);\n"
|
|
|
|
"SamplerState samp : register(s0);\n"
|
|
|
|
"struct VertToFrag\n"
|
|
|
|
"{\n"
|
|
|
|
" float4 position : SV_Position;\n"
|
|
|
|
" float3 uv : UV;\n"
|
|
|
|
" float4 color : COLOR;\n"
|
|
|
|
"};\n"
|
|
|
|
"struct BlendOut\n"
|
|
|
|
"{\n"
|
|
|
|
" float4 colorOut : SV_Target0;\n"
|
|
|
|
" float4 blendOut : SV_Target1;\n"
|
|
|
|
"};\n"
|
|
|
|
"BlendOut main(in VertToFrag vtf)\n"
|
|
|
|
"{\n"
|
|
|
|
" BlendOut ret;\n"
|
|
|
|
" ret.colorOut = vtf.color;\n"
|
2015-12-07 00:52:07 +00:00
|
|
|
" ret.blendOut = ret.colorOut.a * fontTex.Sample(samp, vtf.uv);\n"
|
2015-11-27 22:20:22 +00:00
|
|
|
" return ret;\n"
|
|
|
|
"}\n";
|
|
|
|
|
|
|
|
boo::VertexElementDescriptor vdescs[] =
|
|
|
|
{
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Color | boo::VertexSemantic::Instanced}
|
|
|
|
};
|
2016-03-30 20:43:49 +00:00
|
|
|
m_vtxFmt = ctx.newVertexFormat(13, vdescs);
|
2015-11-27 22:20:22 +00:00
|
|
|
|
|
|
|
m_regular =
|
2017-03-05 23:01:57 +00:00
|
|
|
ctx.newShaderPipeline(VS, FSReg, nullptr, nullptr, nullptr, m_vtxFmt,
|
2016-03-30 20:43:49 +00:00
|
|
|
boo::BlendFactor::SrcAlpha, boo::BlendFactor::InvSrcAlpha,
|
2017-03-17 23:31:16 +00:00
|
|
|
boo::Primitive::TriStrips, boo::ZTest::None, false, true, true, boo::CullMode::None);
|
2015-11-27 22:20:22 +00:00
|
|
|
|
|
|
|
m_subpixel =
|
2017-03-05 23:01:57 +00:00
|
|
|
ctx.newShaderPipeline(VS, FSSubpixel, nullptr, nullptr, nullptr, m_vtxFmt,
|
2016-03-30 20:43:49 +00:00
|
|
|
boo::BlendFactor::SrcColor1, boo::BlendFactor::InvSrcColor1,
|
2017-03-17 23:31:16 +00:00
|
|
|
boo::Primitive::TriStrips, boo::ZTest::None, false, true, true, boo::CullMode::None);
|
2015-11-27 22:20:22 +00:00
|
|
|
}
|
2016-02-24 20:28:37 +00:00
|
|
|
|
2016-02-23 02:33:59 +00:00
|
|
|
#endif
|
|
|
|
#if BOO_HAS_METAL
|
2016-02-24 20:28:37 +00:00
|
|
|
|
2016-03-30 19:15:32 +00:00
|
|
|
void TextView::Resources::init(boo::MetalDataFactory::Context& ctx, FontCache* fcache)
|
2015-11-28 04:05:27 +00:00
|
|
|
{
|
|
|
|
m_fcache = fcache;
|
2016-02-24 20:28:37 +00:00
|
|
|
|
2015-11-28 04:05:27 +00:00
|
|
|
static const char* VS =
|
|
|
|
"#include <metal_stdlib>\n"
|
|
|
|
"using namespace metal;\n"
|
|
|
|
"struct VertData\n"
|
|
|
|
"{\n"
|
|
|
|
" float3 posIn[4];\n"
|
|
|
|
" float4x4 mvMtx;\n"
|
|
|
|
" float3 uvIn[4];\n"
|
|
|
|
" float4 colorIn;\n"
|
|
|
|
"};\n"
|
2016-02-24 21:07:25 +00:00
|
|
|
SPECTER_METAL_VIEW_VERT_BLOCK
|
2015-11-28 04:05:27 +00:00
|
|
|
"struct VertToFrag\n"
|
|
|
|
"{\n"
|
|
|
|
" float4 position [[ position ]];\n"
|
|
|
|
" float3 uv;\n"
|
|
|
|
" float4 color;\n"
|
|
|
|
"};\n"
|
|
|
|
"vertex VertToFrag vmain(constant VertData* va [[ buffer(1) ]],\n"
|
|
|
|
" uint vertId [[ vertex_id ]], uint instId [[ instance_id ]],\n"
|
|
|
|
" constant SpecterViewBlock& view [[ buffer(2) ]])\n"
|
|
|
|
"{\n"
|
|
|
|
" VertToFrag vtf;\n"
|
|
|
|
" constant VertData& v = va[instId];\n"
|
|
|
|
" vtf.uv = v.uvIn[vertId];\n"
|
2015-12-15 21:53:15 +00:00
|
|
|
" vtf.color = v.colorIn * view.mulColor;\n"
|
2015-11-28 04:05:27 +00:00
|
|
|
" vtf.position = view.mv * v.mvMtx * float4(v.posIn[vertId], 1.0);\n"
|
|
|
|
" return vtf;\n"
|
|
|
|
"}\n";
|
2016-02-24 20:28:37 +00:00
|
|
|
|
2015-11-28 04:05:27 +00:00
|
|
|
static const char* FSReg =
|
|
|
|
"#include <metal_stdlib>\n"
|
|
|
|
"using namespace metal;\n"
|
|
|
|
"struct VertToFrag\n"
|
|
|
|
"{\n"
|
|
|
|
" float4 position [[ position ]];\n"
|
|
|
|
" float3 uv;\n"
|
|
|
|
" float4 color;\n"
|
|
|
|
"};\n"
|
2018-01-07 05:19:23 +00:00
|
|
|
"fragment float4 fmain(VertToFrag vtf [[ stage_in ]],\n"
|
|
|
|
" sampler samp [[ sampler(0) ]],\n"
|
|
|
|
" texture2d_array<float> fontTex [[ texture(0) ]])\n"
|
2015-11-28 04:05:27 +00:00
|
|
|
"{\n"
|
|
|
|
" float4 colorOut = vtf.color;\n"
|
2015-12-07 00:52:07 +00:00
|
|
|
" colorOut.a *= fontTex.sample(samp, vtf.uv.xy, vtf.uv.z).r;\n"
|
2015-11-28 04:05:27 +00:00
|
|
|
" return colorOut;\n"
|
|
|
|
"}\n";
|
2016-02-24 20:28:37 +00:00
|
|
|
|
2015-11-28 04:05:27 +00:00
|
|
|
boo::VertexElementDescriptor vdescs[] =
|
|
|
|
{
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Color | boo::VertexSemantic::Instanced}
|
|
|
|
};
|
2016-03-30 21:08:29 +00:00
|
|
|
m_vtxFmt = ctx.newVertexFormat(13, vdescs);
|
2016-02-24 20:28:37 +00:00
|
|
|
|
2015-11-28 04:05:27 +00:00
|
|
|
m_regular =
|
2018-01-07 05:19:23 +00:00
|
|
|
ctx.newShaderPipeline(VS, FSReg, nullptr, nullptr, m_vtxFmt,
|
2016-03-30 21:08:29 +00:00
|
|
|
boo::BlendFactor::SrcAlpha, boo::BlendFactor::InvSrcAlpha,
|
2017-12-20 06:06:21 +00:00
|
|
|
boo::Primitive::TriStrips, boo::ZTest::None, false, true, false, boo::CullMode::None);
|
2015-11-28 04:05:27 +00:00
|
|
|
}
|
2016-02-24 20:28:37 +00:00
|
|
|
|
2016-02-23 02:33:59 +00:00
|
|
|
#endif
|
|
|
|
#if BOO_HAS_VULKAN
|
|
|
|
|
2016-03-30 19:15:32 +00:00
|
|
|
void TextView::Resources::init(boo::VulkanDataFactory::Context& ctx, FontCache* fcache)
|
2016-02-23 02:33:59 +00:00
|
|
|
{
|
|
|
|
m_fcache = fcache;
|
|
|
|
|
|
|
|
boo::VertexElementDescriptor vdescs[] =
|
|
|
|
{
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{nullptr, nullptr, boo::VertexSemantic::Color | boo::VertexSemantic::Instanced}
|
|
|
|
};
|
2016-03-30 19:15:32 +00:00
|
|
|
m_vtxFmt = ctx.newVertexFormat(13, vdescs);
|
2016-02-23 02:33:59 +00:00
|
|
|
|
|
|
|
m_regular =
|
2016-03-30 19:15:32 +00:00
|
|
|
ctx.newShaderPipeline(GLSLVS, GLSLFSReg, m_vtxFmt,
|
|
|
|
boo::BlendFactor::SrcAlpha, boo::BlendFactor::InvSrcAlpha,
|
2017-03-17 23:31:16 +00:00
|
|
|
boo::Primitive::TriStrips,boo::ZTest::None, false, true, true, boo::CullMode::None);
|
2016-02-23 02:33:59 +00:00
|
|
|
}
|
|
|
|
|
2015-11-28 01:40:59 +00:00
|
|
|
#endif
|
2015-11-27 22:20:22 +00:00
|
|
|
|
2016-12-11 06:17:49 +00:00
|
|
|
void TextView::_commitResources(size_t capacity)
|
|
|
|
{
|
|
|
|
auto& res = rootView().viewRes();
|
2018-01-10 06:18:56 +00:00
|
|
|
auto fontTex = m_fontAtlas.texture(res.m_factory);
|
2018-05-18 04:15:11 +00:00
|
|
|
View::commitResources(res, [&](boo::IGraphicsDataFactory::Context& ctx)
|
2016-12-11 06:17:49 +00:00
|
|
|
{
|
|
|
|
buildResources(ctx, res);
|
|
|
|
|
|
|
|
if (capacity)
|
|
|
|
{
|
2017-01-29 03:57:48 +00:00
|
|
|
m_glyphBuf = res.m_textRes.m_glyphPool.allocateBlock(res.m_factory, capacity);
|
2016-12-11 06:17:49 +00:00
|
|
|
|
2017-11-05 06:16:45 +00:00
|
|
|
boo::ObjToken<boo::IShaderPipeline> shader;
|
2016-12-11 06:17:49 +00:00
|
|
|
if (m_fontAtlas.subpixel())
|
|
|
|
shader = res.m_textRes.m_subpixel;
|
|
|
|
else
|
|
|
|
shader = res.m_textRes.m_regular;
|
|
|
|
|
2017-01-29 03:57:48 +00:00
|
|
|
auto vBufInfo = m_glyphBuf.getBufferInfo();
|
|
|
|
auto uBufInfo = m_viewVertBlockBuf.getBufferInfo();
|
2017-11-05 06:16:45 +00:00
|
|
|
boo::ObjToken<boo::IGraphicsBuffer> uBufs[] = {uBufInfo.first.get()};
|
2016-12-11 06:17:49 +00:00
|
|
|
size_t uBufOffs[] = {size_t(uBufInfo.second)};
|
|
|
|
size_t uBufSizes[] = {sizeof(ViewBlock)};
|
2018-01-10 06:18:56 +00:00
|
|
|
boo::ObjToken<boo::ITexture> texs[] = {fontTex.get()};
|
2016-12-11 06:17:49 +00:00
|
|
|
|
|
|
|
if (!res.m_textRes.m_vtxFmt)
|
|
|
|
{
|
|
|
|
boo::VertexElementDescriptor vdescs[] =
|
|
|
|
{
|
2017-11-05 06:16:45 +00:00
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::Position4 | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 0},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 1},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 2},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 3},
|
|
|
|
{vBufInfo.first.get(), nullptr, boo::VertexSemantic::Color | boo::VertexSemantic::Instanced}
|
2016-12-11 06:17:49 +00:00
|
|
|
};
|
|
|
|
m_vtxFmt = ctx.newVertexFormat(13, vdescs, 0, vBufInfo.second);
|
|
|
|
m_shaderBinding = ctx.newShaderDataBinding(shader, m_vtxFmt,
|
2017-11-05 06:16:45 +00:00
|
|
|
nullptr, vBufInfo.first.get(), nullptr, 1,
|
2016-12-11 06:17:49 +00:00
|
|
|
uBufs, nullptr, uBufOffs, uBufSizes,
|
2017-03-14 07:02:24 +00:00
|
|
|
1, texs, nullptr, nullptr, 0, vBufInfo.second);
|
2016-12-11 06:17:49 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
m_shaderBinding = ctx.newShaderDataBinding(shader, res.m_textRes.m_vtxFmt,
|
2017-11-05 06:16:45 +00:00
|
|
|
nullptr, vBufInfo.first.get(), nullptr, 1,
|
2016-12-11 06:17:49 +00:00
|
|
|
uBufs, nullptr, uBufOffs, uBufSizes,
|
2017-03-14 07:02:24 +00:00
|
|
|
1, texs, nullptr, nullptr, 0, vBufInfo.second);
|
2016-12-11 06:17:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-03-30 19:15:32 +00:00
|
|
|
TextView::TextView(ViewResources& res,
|
|
|
|
View& parentView, const FontAtlas& font,
|
|
|
|
Alignment align, size_t capacity)
|
2015-12-05 00:42:46 +00:00
|
|
|
: View(res, parentView),
|
2015-11-26 07:35:43 +00:00
|
|
|
m_capacity(capacity),
|
2015-12-13 21:00:30 +00:00
|
|
|
m_fontAtlas(font),
|
|
|
|
m_align(align)
|
2015-11-25 01:46:30 +00:00
|
|
|
{
|
2016-12-12 20:10:32 +00:00
|
|
|
if (size_t(hecl::VertexBufferPool<RenderGlyph>::bucketCapacity()) < capacity)
|
2016-12-10 02:33:54 +00:00
|
|
|
Log.report(logvisor::Fatal, "bucket overflow [%" PRISize "/%" PRISize "]",
|
2016-12-12 20:10:32 +00:00
|
|
|
capacity, hecl::VertexBufferPool<RenderGlyph>::bucketCapacity());
|
2016-12-10 02:33:54 +00:00
|
|
|
|
2016-12-11 06:17:49 +00:00
|
|
|
_commitResources(0);
|
2015-11-25 01:46:30 +00:00
|
|
|
}
|
|
|
|
|
2015-12-13 21:00:30 +00:00
|
|
|
TextView::TextView(ViewResources& res, View& parentView, FontTag font, Alignment align, size_t capacity)
|
|
|
|
: TextView(res, parentView, res.m_textRes.m_fcache->lookupAtlas(font), align, capacity) {}
|
2015-11-29 02:55:30 +00:00
|
|
|
|
2016-03-04 23:03:47 +00:00
|
|
|
TextView::RenderGlyph::RenderGlyph(int& adv, const FontAtlas::Glyph& glyph, const zeus::CColor& defaultColor)
|
2015-11-26 00:24:01 +00:00
|
|
|
{
|
|
|
|
m_pos[0].assign(adv + glyph.m_leftPadding, glyph.m_verticalOffset + glyph.m_height, 0.f);
|
|
|
|
m_pos[1].assign(adv + glyph.m_leftPadding, glyph.m_verticalOffset, 0.f);
|
|
|
|
m_pos[2].assign(adv + glyph.m_leftPadding + glyph.m_width, glyph.m_verticalOffset + glyph.m_height, 0.f);
|
|
|
|
m_pos[3].assign(adv + glyph.m_leftPadding + glyph.m_width, glyph.m_verticalOffset, 0.f);
|
|
|
|
m_uv[0].assign(glyph.m_uv[0], glyph.m_uv[1], glyph.m_layerFloat);
|
|
|
|
m_uv[1].assign(glyph.m_uv[0], glyph.m_uv[3], glyph.m_layerFloat);
|
|
|
|
m_uv[2].assign(glyph.m_uv[2], glyph.m_uv[1], glyph.m_layerFloat);
|
|
|
|
m_uv[3].assign(glyph.m_uv[2], glyph.m_uv[3], glyph.m_layerFloat);
|
|
|
|
m_color = defaultColor;
|
|
|
|
adv += glyph.m_advance;
|
|
|
|
}
|
|
|
|
|
2015-12-07 00:52:07 +00:00
|
|
|
int TextView::DoKern(FT_Pos val, const FontAtlas& atlas)
|
2015-11-26 07:35:43 +00:00
|
|
|
{
|
2015-12-02 06:13:43 +00:00
|
|
|
if (!val) return 0;
|
2015-11-26 07:35:43 +00:00
|
|
|
val = FT_MulFix(val, atlas.FT_Xscale());
|
|
|
|
|
|
|
|
FT_Pos orig_x = val;
|
|
|
|
|
|
|
|
/* we scale down kerning values for small ppem values */
|
|
|
|
/* to avoid that rounding makes them too big. */
|
|
|
|
/* `25' has been determined heuristically. */
|
|
|
|
if (atlas.FT_XPPem() < 25)
|
|
|
|
val = FT_MulDiv(orig_x, atlas.FT_XPPem(), 25);
|
|
|
|
|
2015-11-26 23:03:56 +00:00
|
|
|
return FT_PIX_ROUND(val) >> 6;
|
2015-11-26 07:35:43 +00:00
|
|
|
}
|
|
|
|
|
2017-11-13 06:14:52 +00:00
|
|
|
void TextView::typesetGlyphs(std::string_view str, const zeus::CColor& defaultColor)
|
2015-11-25 01:46:30 +00:00
|
|
|
{
|
2016-12-11 06:17:49 +00:00
|
|
|
UTF8Iterator it(str.begin());
|
2016-12-12 20:10:32 +00:00
|
|
|
size_t charLen = str.size() ? std::min(it.countTo(str.end()), m_capacity) : 0;
|
2018-05-18 04:15:11 +00:00
|
|
|
if (charLen > m_curSize)
|
|
|
|
{
|
|
|
|
m_curSize = charLen;
|
|
|
|
_commitResources(charLen);
|
|
|
|
}
|
2016-12-11 06:17:49 +00:00
|
|
|
|
2015-12-02 06:13:43 +00:00
|
|
|
uint32_t lCh = -1;
|
2015-11-26 00:24:01 +00:00
|
|
|
m_glyphs.clear();
|
2016-12-11 06:17:49 +00:00
|
|
|
m_glyphs.reserve(charLen);
|
2015-12-20 21:59:23 +00:00
|
|
|
m_glyphInfo.clear();
|
2016-12-11 06:17:49 +00:00
|
|
|
m_glyphInfo.reserve(charLen);
|
2015-11-26 00:24:01 +00:00
|
|
|
int adv = 0;
|
|
|
|
|
2016-12-11 06:17:49 +00:00
|
|
|
if (charLen)
|
2015-11-25 01:46:30 +00:00
|
|
|
{
|
2016-12-11 06:17:49 +00:00
|
|
|
for (; it.iter() < str.end() ; ++it)
|
2015-11-26 00:24:01 +00:00
|
|
|
{
|
2016-12-11 06:17:49 +00:00
|
|
|
utf8proc_int32_t ch = *it;
|
|
|
|
if (ch == -1)
|
|
|
|
{
|
|
|
|
Log.report(logvisor::Warning, "invalid UTF-8 char");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (ch == '\n' || ch == '\0')
|
|
|
|
break;
|
2015-11-26 00:24:01 +00:00
|
|
|
|
2016-12-11 06:17:49 +00:00
|
|
|
const FontAtlas::Glyph* glyph = m_fontAtlas.lookupGlyph(ch);
|
|
|
|
if (!glyph)
|
|
|
|
continue;
|
2015-11-25 01:46:30 +00:00
|
|
|
|
2016-12-11 06:17:49 +00:00
|
|
|
if (lCh != -1)
|
|
|
|
adv += DoKern(m_fontAtlas.lookupKern(lCh, glyph->m_glyphIdx), m_fontAtlas);
|
|
|
|
m_glyphs.emplace_back(adv, *glyph, defaultColor);
|
|
|
|
m_glyphInfo.emplace_back(ch, glyph->m_width, glyph->m_height, adv);
|
2015-11-26 07:35:43 +00:00
|
|
|
|
2016-12-11 06:17:49 +00:00
|
|
|
lCh = glyph->m_glyphIdx;
|
|
|
|
|
|
|
|
if (m_glyphs.size() == m_capacity)
|
|
|
|
break;
|
|
|
|
}
|
2015-11-25 01:46:30 +00:00
|
|
|
}
|
2015-11-26 00:24:01 +00:00
|
|
|
|
2015-12-13 21:00:30 +00:00
|
|
|
if (m_align == Alignment::Right)
|
|
|
|
{
|
|
|
|
int adj = -adv;
|
|
|
|
for (RenderGlyph& g : m_glyphs)
|
|
|
|
{
|
|
|
|
g.m_pos[0][0] += adj;
|
|
|
|
g.m_pos[1][0] += adj;
|
|
|
|
g.m_pos[2][0] += adj;
|
|
|
|
g.m_pos[3][0] += adj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (m_align == Alignment::Center)
|
|
|
|
{
|
|
|
|
int adj = -adv / 2;
|
|
|
|
for (RenderGlyph& g : m_glyphs)
|
|
|
|
{
|
|
|
|
g.m_pos[0][0] += adj;
|
|
|
|
g.m_pos[1][0] += adj;
|
|
|
|
g.m_pos[2][0] += adj;
|
|
|
|
g.m_pos[3][0] += adj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-05 00:42:46 +00:00
|
|
|
m_width = adv;
|
2016-12-10 02:33:54 +00:00
|
|
|
invalidateGlyphs();
|
2015-12-05 00:42:46 +00:00
|
|
|
updateSize();
|
2015-11-25 01:46:30 +00:00
|
|
|
}
|
2015-12-07 00:52:07 +00:00
|
|
|
|
2017-11-13 06:14:52 +00:00
|
|
|
void TextView::typesetGlyphs(std::wstring_view str, const zeus::CColor& defaultColor)
|
2015-11-25 01:46:30 +00:00
|
|
|
{
|
2016-12-12 20:10:32 +00:00
|
|
|
size_t charLen = std::min(str.size(), m_capacity);
|
2018-05-18 04:15:11 +00:00
|
|
|
if (charLen > m_curSize)
|
|
|
|
{
|
|
|
|
m_curSize = charLen;
|
|
|
|
_commitResources(charLen);
|
|
|
|
}
|
2016-12-11 06:17:49 +00:00
|
|
|
|
2015-12-02 06:13:43 +00:00
|
|
|
uint32_t lCh = -1;
|
2015-11-26 00:24:01 +00:00
|
|
|
m_glyphs.clear();
|
2016-12-12 20:10:32 +00:00
|
|
|
m_glyphs.reserve(charLen);
|
2015-12-20 21:59:23 +00:00
|
|
|
m_glyphInfo.clear();
|
2016-12-12 20:10:32 +00:00
|
|
|
m_glyphInfo.reserve(charLen);
|
2015-11-26 00:24:01 +00:00
|
|
|
int adv = 0;
|
|
|
|
|
|
|
|
for (wchar_t ch : str)
|
|
|
|
{
|
|
|
|
if (ch == L'\n')
|
|
|
|
break;
|
|
|
|
|
|
|
|
const FontAtlas::Glyph* glyph = m_fontAtlas.lookupGlyph(ch);
|
|
|
|
if (!glyph)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (lCh != -1)
|
2015-12-02 06:13:43 +00:00
|
|
|
adv += DoKern(m_fontAtlas.lookupKern(lCh, glyph->m_glyphIdx), m_fontAtlas);
|
2015-11-28 21:45:38 +00:00
|
|
|
m_glyphs.emplace_back(adv, *glyph, defaultColor);
|
2015-12-20 21:59:23 +00:00
|
|
|
m_glyphInfo.emplace_back(ch, glyph->m_width, glyph->m_height, adv);
|
2015-11-26 00:24:01 +00:00
|
|
|
|
2015-12-02 06:13:43 +00:00
|
|
|
lCh = glyph->m_glyphIdx;
|
2015-11-26 07:35:43 +00:00
|
|
|
|
|
|
|
if (m_glyphs.size() == m_capacity)
|
|
|
|
break;
|
2015-11-26 00:24:01 +00:00
|
|
|
}
|
|
|
|
|
2015-12-13 21:00:30 +00:00
|
|
|
if (m_align == Alignment::Right)
|
|
|
|
{
|
|
|
|
int adj = -adv;
|
|
|
|
for (RenderGlyph& g : m_glyphs)
|
|
|
|
{
|
|
|
|
g.m_pos[0][0] += adj;
|
|
|
|
g.m_pos[1][0] += adj;
|
|
|
|
g.m_pos[2][0] += adj;
|
|
|
|
g.m_pos[3][0] += adj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (m_align == Alignment::Center)
|
|
|
|
{
|
|
|
|
int adj = -adv / 2;
|
|
|
|
for (RenderGlyph& g : m_glyphs)
|
|
|
|
{
|
|
|
|
g.m_pos[0][0] += adj;
|
|
|
|
g.m_pos[1][0] += adj;
|
|
|
|
g.m_pos[2][0] += adj;
|
|
|
|
g.m_pos[3][0] += adj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-05 00:42:46 +00:00
|
|
|
m_width = adv;
|
2016-12-10 02:33:54 +00:00
|
|
|
invalidateGlyphs();
|
2015-12-05 00:42:46 +00:00
|
|
|
updateSize();
|
2015-11-25 01:46:30 +00:00
|
|
|
}
|
|
|
|
|
2016-03-04 23:03:47 +00:00
|
|
|
void TextView::colorGlyphs(const zeus::CColor& newColor)
|
2015-11-25 01:46:30 +00:00
|
|
|
{
|
2015-11-26 07:35:43 +00:00
|
|
|
for (RenderGlyph& glyph : m_glyphs)
|
|
|
|
glyph.m_color = newColor;
|
2016-12-10 02:33:54 +00:00
|
|
|
invalidateGlyphs();
|
2015-11-25 01:46:30 +00:00
|
|
|
}
|
2016-12-10 02:33:54 +00:00
|
|
|
|
2016-03-04 23:03:47 +00:00
|
|
|
void TextView::colorGlyphsTypeOn(const zeus::CColor& newColor, float startInterval, float fadeTime)
|
2015-11-25 01:46:30 +00:00
|
|
|
{
|
|
|
|
}
|
2016-12-10 02:33:54 +00:00
|
|
|
|
|
|
|
void TextView::invalidateGlyphs()
|
|
|
|
{
|
2016-12-11 06:17:49 +00:00
|
|
|
if (m_glyphBuf)
|
|
|
|
{
|
2017-01-29 03:57:48 +00:00
|
|
|
RenderGlyph* out = m_glyphBuf.access();
|
2016-12-11 06:17:49 +00:00
|
|
|
size_t i = 0;
|
|
|
|
for (RenderGlyph& glyph : m_glyphs)
|
|
|
|
out[i++] = glyph;
|
|
|
|
}
|
2016-12-10 02:33:54 +00:00
|
|
|
}
|
|
|
|
|
2015-11-25 01:46:30 +00:00
|
|
|
void TextView::think()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2015-12-13 02:26:41 +00:00
|
|
|
void TextView::resized(const boo::SWindowRect &root, const boo::SWindowRect& sub)
|
2015-12-05 00:42:46 +00:00
|
|
|
{
|
2015-12-13 02:26:41 +00:00
|
|
|
View::resized(root, sub);
|
2015-12-05 00:42:46 +00:00
|
|
|
}
|
|
|
|
|
2015-11-25 01:46:30 +00:00
|
|
|
void TextView::draw(boo::IGraphicsCommandQueue* gfxQ)
|
|
|
|
{
|
2015-11-26 07:35:43 +00:00
|
|
|
View::draw(gfxQ);
|
2015-11-30 03:41:53 +00:00
|
|
|
if (m_glyphs.size())
|
2015-11-26 00:24:01 +00:00
|
|
|
{
|
2015-12-01 00:35:45 +00:00
|
|
|
gfxQ->setShaderDataBinding(m_shaderBinding);
|
2015-11-30 03:41:53 +00:00
|
|
|
gfxQ->drawInstances(0, 4, m_glyphs.size());
|
2015-11-26 00:24:01 +00:00
|
|
|
}
|
2015-11-25 01:46:30 +00:00
|
|
|
}
|
|
|
|
|
2015-12-08 01:44:46 +00:00
|
|
|
std::pair<int,int> TextView::queryGlyphDimensions(size_t pos) const
|
|
|
|
{
|
2015-12-20 21:59:23 +00:00
|
|
|
if (pos >= m_glyphInfo.size())
|
2016-03-04 23:03:47 +00:00
|
|
|
Log.report(logvisor::Fatal,
|
2015-12-08 01:44:46 +00:00
|
|
|
"TextView::queryGlyphWidth(%" PRISize ") out of bounds: %" PRISize,
|
2015-12-20 21:59:23 +00:00
|
|
|
pos, m_glyphInfo.size());
|
2015-12-08 01:44:46 +00:00
|
|
|
|
2015-12-20 21:59:23 +00:00
|
|
|
return m_glyphInfo[pos].m_dims;
|
2015-12-08 01:44:46 +00:00
|
|
|
}
|
|
|
|
|
2015-12-20 04:39:09 +00:00
|
|
|
size_t TextView::reverseSelectGlyph(int x) const
|
|
|
|
{
|
|
|
|
size_t ret = 0;
|
|
|
|
size_t idx = 1;
|
|
|
|
int minDelta = abs(x);
|
2015-12-20 21:59:23 +00:00
|
|
|
for (const RenderGlyphInfo& info : m_glyphInfo)
|
2015-12-20 04:39:09 +00:00
|
|
|
{
|
2015-12-20 21:59:23 +00:00
|
|
|
int thisDelta = abs(info.m_adv-x);
|
2015-12-20 04:39:09 +00:00
|
|
|
if (thisDelta < minDelta)
|
|
|
|
{
|
|
|
|
minDelta = thisDelta;
|
|
|
|
ret = idx;
|
|
|
|
}
|
|
|
|
++idx;
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
int TextView::queryReverseAdvance(size_t idx) const
|
|
|
|
{
|
2015-12-20 21:59:23 +00:00
|
|
|
if (idx > m_glyphInfo.size())
|
2016-03-04 23:03:47 +00:00
|
|
|
Log.report(logvisor::Fatal,
|
2015-12-20 04:39:09 +00:00
|
|
|
"TextView::queryReverseGlyph(%" PRISize ") out of inclusive bounds: %" PRISize,
|
2015-12-20 21:59:23 +00:00
|
|
|
idx, m_glyphInfo.size());
|
2015-12-20 04:39:09 +00:00
|
|
|
if (!idx) return 0;
|
2015-12-20 21:59:23 +00:00
|
|
|
return m_glyphInfo[idx-1].m_adv;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::pair<size_t,size_t> TextView::queryWholeWordRange(size_t idx) const
|
|
|
|
{
|
|
|
|
if (idx > m_glyphInfo.size())
|
2016-03-04 23:03:47 +00:00
|
|
|
Log.report(logvisor::Fatal,
|
2015-12-20 21:59:23 +00:00
|
|
|
"TextView::queryWholeWordRange(%" PRISize ") out of inclusive bounds: %" PRISize,
|
|
|
|
idx, m_glyphInfo.size());
|
|
|
|
if (m_glyphInfo.empty())
|
|
|
|
return {0,0};
|
|
|
|
|
|
|
|
if (idx == m_glyphInfo.size())
|
|
|
|
--idx;
|
|
|
|
|
|
|
|
size_t begin = idx;
|
|
|
|
while (begin > 0 && !m_glyphInfo[begin-1].m_space)
|
|
|
|
--begin;
|
|
|
|
|
|
|
|
size_t end = idx;
|
|
|
|
while (end < m_glyphInfo.size() && !m_glyphInfo[end].m_space)
|
|
|
|
++end;
|
|
|
|
|
|
|
|
return {begin, end-begin};
|
2015-12-20 04:39:09 +00:00
|
|
|
}
|
2015-11-21 23:45:02 +00:00
|
|
|
|
|
|
|
}
|