330 lines
7.5 KiB
C++
330 lines
7.5 KiB
C++
#include <psemek/ui/label.hpp>
|
|
|
|
#include <stdexcept>
|
|
#include <cctype>
|
|
|
|
namespace psemek::ui
|
|
{
|
|
|
|
label::label(std::string text)
|
|
: text_(std::move(text))
|
|
{}
|
|
|
|
void label::set_text(std::string text)
|
|
{
|
|
text_ = std::move(text);
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_font(font_type f)
|
|
{
|
|
font_ = f;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_halign(halignment value)
|
|
{
|
|
halign_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_valign(valignment value)
|
|
{
|
|
valign_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_wrap(bool value)
|
|
{
|
|
wrap_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_skip_spaces(bool value)
|
|
{
|
|
skip_spaces_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_overflow(overflow_mode value)
|
|
{
|
|
overflow_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::reshape(geom::box<float, 2> const & bbox)
|
|
{
|
|
shape_.box = bbox;
|
|
cached_state_.reset();
|
|
}
|
|
|
|
geom::box<float, 2> label::size_constraints() const
|
|
{
|
|
static float const inf = std::numeric_limits<float>::infinity();
|
|
|
|
if (!cached_state_inf_)
|
|
cached_state_inf_ = cached_state_for({{{0.f, inf}, {0.f, inf}}});
|
|
|
|
return {{{cached_state_inf_->size[0], inf}, {cached_state_inf_->size[1], inf}}};
|
|
}
|
|
|
|
geom::interval<float> label::width_constraints(float height) const
|
|
{
|
|
static float const inf = std::numeric_limits<float>::infinity();
|
|
|
|
auto state = cached_state_for({{{0.f, inf}, {0.f, height}}});
|
|
return {state.size[0], inf};
|
|
}
|
|
|
|
geom::interval<float> label::height_constraints(float width) const
|
|
{
|
|
static float const inf = std::numeric_limits<float>::infinity();
|
|
|
|
auto state = cached_state_for({{{0.f, width}, {0.f, inf}}});
|
|
return {state.size[1], inf};
|
|
}
|
|
|
|
void label::style_updated() const
|
|
{
|
|
element::style_updated();
|
|
cached_state_.reset();
|
|
cached_state_inf_.reset();
|
|
}
|
|
|
|
void label::own_style_updated() const
|
|
{
|
|
element::own_style_updated();
|
|
cached_state_.reset();
|
|
cached_state_inf_.reset();
|
|
}
|
|
|
|
void label::draw(painter & p) const
|
|
{
|
|
if (!cached_state_)
|
|
update_cached_state();
|
|
|
|
auto st = merged_own_style();
|
|
if (!st) return;
|
|
|
|
if (st->text_shadow_offset != geom::vector{0, 0})
|
|
{
|
|
auto const offset = geom::cast<float>(*st->text_shadow_offset);
|
|
for (auto const & batch : cached_state_->batches)
|
|
for (auto const & image : batch.images)
|
|
p.draw_image(image.position + offset, gfx::texture_view_2d{batch.texture, image.texcoords}, *st->shadow_color);
|
|
}
|
|
|
|
for (auto const & batch : cached_state_->batches)
|
|
for (auto const & image : batch.images)
|
|
p.draw_image(image.position, gfx::texture_view_2d{batch.texture, image.texcoords}, *st->text_color);
|
|
}
|
|
|
|
void label::on_state_changed()
|
|
{
|
|
cached_state_.reset();
|
|
cached_state_inf_.reset();
|
|
post_reshape();
|
|
}
|
|
|
|
static bool is_newline(char32_t c)
|
|
{
|
|
return (c == '\n') || (c == '\r');
|
|
}
|
|
|
|
void label::update_cached_state() const
|
|
{
|
|
cached_state_ = cached_state_for(shape_.box);
|
|
}
|
|
|
|
label::cached_state label::cached_state_for(geom::box<float, 2> const & bbox) const
|
|
{
|
|
auto state = cached_state{};
|
|
|
|
if (text_.empty()) return state;
|
|
|
|
auto st = merged_own_style();
|
|
if (!st) return state;
|
|
|
|
auto font = (font_ == font_type::normal) ? st->font.get() : st->bold_font.get();
|
|
|
|
if (!font) return state;
|
|
|
|
shape_options opts;
|
|
opts.scale = *st->text_scale;
|
|
auto glyphs = font->shape(text_, opts);
|
|
|
|
geom::box<float, 2> raw_bbox;
|
|
for (auto const & g : glyphs)
|
|
raw_bbox |= g.position;
|
|
|
|
std::size_t max_lines = 1;
|
|
|
|
if (wrap_)
|
|
{
|
|
max_lines = std::isfinite(bbox[1].length())
|
|
? std::max<std::size_t>(1, std::floor(bbox[1].length() / font->size()[1] / (*st->text_scale)))
|
|
: std::numeric_limits<std::size_t>::max();
|
|
}
|
|
|
|
std::vector<std::pair<std::size_t, std::size_t>> lines;
|
|
|
|
std::size_t current_glyph = 0;
|
|
|
|
for (std::size_t line = 0; line < max_lines; ++line)
|
|
{
|
|
if (line != 0)
|
|
++current_glyph;
|
|
|
|
if (skip_spaces_)
|
|
{
|
|
while (current_glyph < glyphs.size() && std::isspace(glyphs[current_glyph].character))
|
|
++current_glyph;
|
|
}
|
|
|
|
std::size_t line_begin = current_glyph;
|
|
std::size_t line_end = line_begin;
|
|
|
|
geom::interval<float> x_range;
|
|
while (line_end < glyphs.size())
|
|
{
|
|
if (is_newline(glyphs[line_end].character))
|
|
break;
|
|
|
|
x_range |= glyphs[line_end].position[0];
|
|
|
|
if (x_range.length() > bbox[0].length())
|
|
break;
|
|
|
|
++line_end;
|
|
}
|
|
|
|
if (line_end < glyphs.size())
|
|
{
|
|
std::size_t space_pos = line_end;
|
|
while (space_pos > line_begin && !std::isspace(glyphs[space_pos].character))
|
|
--space_pos;
|
|
|
|
if (space_pos > line_begin)
|
|
line_end = space_pos;
|
|
}
|
|
|
|
if (line + 1 == max_lines && line_end < glyphs.size())
|
|
{
|
|
switch (overflow_)
|
|
{
|
|
case overflow_mode::ignore:
|
|
line_end = glyphs.size();
|
|
break;
|
|
case overflow_mode::drop:
|
|
glyphs.erase(glyphs.begin() + line_end, glyphs.end());
|
|
break;
|
|
case overflow_mode::ellipsis:
|
|
{
|
|
std::size_t el_start = std::max<std::size_t>(3, line_end) - 3;
|
|
|
|
float x_offset = 0.f;
|
|
if (el_start > 0)
|
|
x_offset = glyphs[el_start - 1].position[0].max;
|
|
|
|
char const el_str[] = "...";
|
|
auto els = font->shape(el_str, opts);
|
|
for (std::size_t i = el_start; i < line_end; ++i)
|
|
{
|
|
glyphs[i] = els[i - el_start];
|
|
glyphs[i].position[0] += x_offset;
|
|
}
|
|
|
|
glyphs.erase(glyphs.begin() + line_end, glyphs.end());
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
current_glyph = line_end;
|
|
lines.push_back({line_begin, line_end});
|
|
if (line_end == glyphs.size())
|
|
break;
|
|
}
|
|
|
|
float max_line_size = 0.f;
|
|
|
|
for (std::size_t l = 0; l < lines.size(); ++l)
|
|
{
|
|
geom::interval<float> x_range;
|
|
int spaces = 0;
|
|
|
|
for (std::size_t i = lines[l].first; i < lines[l].second; ++i)
|
|
{
|
|
x_range |= glyphs[i].position[0];
|
|
if (std::isspace(glyphs[i].character))
|
|
++spaces;
|
|
}
|
|
|
|
max_line_size = std::max(max_line_size, x_range.length());
|
|
|
|
geom::vector<float, 2> offset;
|
|
float space_extra = 0.f;
|
|
|
|
switch (halign_)
|
|
{
|
|
case halignment::left:
|
|
offset[0] = bbox[0].min - x_range.min;
|
|
break;
|
|
case halignment::center:
|
|
offset[0] = bbox[0].center() - x_range.length() / 2.f - x_range.min;
|
|
break;
|
|
case halignment::right:
|
|
offset[0] = bbox[0].max - x_range.length() - x_range.min;
|
|
break;
|
|
case halignment::stretch:
|
|
offset[0] = bbox[0].min - x_range.min;
|
|
if ((l + 1 != lines.size() && text_[lines[l].second] != '\n') && spaces > 0)
|
|
space_extra = (bbox[0].length() - x_range.length()) / spaces;
|
|
break;
|
|
}
|
|
|
|
switch (valign_)
|
|
{
|
|
case valignment::top:
|
|
offset[1] = bbox[1].min + l * (*st->text_scale) * font->size()[1];
|
|
break;
|
|
case valignment::center:
|
|
offset[1] = bbox[1].center() + (l - lines.size() / 2.f) * (*st->text_scale) * font->size()[1];
|
|
break;
|
|
case valignment::bottom:
|
|
offset[1] = bbox[1].max + (l - lines.size() * 1.f) * (*st->text_scale) * font->size()[1];
|
|
break;
|
|
}
|
|
|
|
for (std::size_t i = lines[l].first; i < lines[l].second; ++i)
|
|
{
|
|
glyphs[i].position += offset;
|
|
if (std::isspace(glyphs[i].character))
|
|
offset[0] += space_extra;
|
|
}
|
|
}
|
|
|
|
{
|
|
auto & batch = state.batches.emplace_back();
|
|
batch.texture = &font->atlas();
|
|
|
|
for (auto const & g : glyphs)
|
|
{
|
|
auto tc = font->texcoords(g.character);
|
|
if (!tc) continue;
|
|
|
|
auto & image = batch.images.emplace_back();
|
|
image.position = g.position;
|
|
image.position[0] += (std::round(image.position[0].min) - image.position[0].min);
|
|
image.position[1] += (std::round(image.position[1].min) - image.position[1].min);
|
|
image.texcoords = *tc;
|
|
}
|
|
}
|
|
|
|
state.size[0] = max_line_size;
|
|
state.size[1] = lines.size() * (*st->text_scale) * font->size()[1];
|
|
|
|
return state;
|
|
}
|
|
|
|
}
|