psemek/libs/ui/source/label.cpp

487 lines
11 KiB
C++

#include <psemek/ui/label.hpp>
#include <stdexcept>
#include <cctype>
namespace psemek::ui
{
label::label(std::string text)
{
set_text(std::move(text));
}
void label::set_text(std::string text)
{
text_ = std::move(text);
chunks_.clear();
chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}});
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, painter::color_mode::multiply});
}
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, painter::color_mode::multiply});
}
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 (chunks_.empty()) return state;
auto st = merged_own_style();
auto const font_height = st->font->size()[1] * (*st->text_scale);
// Shape glyphs chunk by chunk
// All glyphs within a chunk have
// the exact same font properties
struct glyph_chunk
{
std::size_t end;
text_style style;
};
std::vector<glyph> glyphs;
std::vector<glyph_chunk> glyph_chunks;
{
geom::point<float, 2> pen{0.f, 0.f};
shape_options opts;
opts.scale = *st->text_scale;
for (auto const & chunk : chunks_)
{
if (auto text = std::get_if<text_chunk>(&chunk))
{
if (text->text.find("yo") != std::string::npos)
{
int x = 42;
(void)x;
}
auto const merged_text_style = *st->text_style | text->style;
bool const bold = merged_text_style.is_set(text_style_flag::bold);
auto font = bold ? st->bold_font.get() : st->font.get();
auto chunk_glyphs = font->shape(text->text, opts, pen);
glyphs.reserve(glyphs.size() + chunk_glyphs.size());
glyphs.insert(glyphs.end(), chunk_glyphs.begin(), chunk_glyphs.end());
glyph_chunks.push_back({glyphs.size(), merged_text_style});
}
}
}
// Break glyphs into lines
struct line_range
{
std::size_t begin;
std::size_t end;
};
std::vector<line_range> lines;
{
std::size_t current_glyph = 0;
while (current_glyph < glyphs.size())
{
if (skip_spaces_)
{
while (current_glyph < glyphs.size() && std::isspace(glyphs[current_glyph].character))
++current_glyph;
}
std::size_t line_begin = current_glyph;
bool newline_end = false;
bool wrap_end = false;
geom::interval<float> x_range;
while (current_glyph < glyphs.size())
{
if (is_newline(glyphs[current_glyph].character))
{
newline_end = true;
break;
}
x_range |= glyphs[current_glyph].position[0];
if (x_range.length() > bbox[0].length() && wrap_)
{
wrap_end = true;
break;
}
++current_glyph;
}
if (wrap_end && current_glyph > line_begin)
{
std::size_t space_pos = current_glyph - 1;
while (space_pos > line_begin && !std::isspace(glyphs[space_pos].character))
--space_pos;
if (space_pos > line_begin)
current_glyph = space_pos;
}
lines.push_back({line_begin, current_glyph});
if (newline_end)
++current_glyph;
}
}
// Compute line bboxes
std::vector<geom::box<float, 2>> line_bbox(lines.size());
for (std::size_t l = 0; l < lines.size(); ++l)
{
geom::box<float, 2> bbox;
// Glyphs might be smaller than total text height, so
// hardcode font height as min line height
bbox[1] = {0.f, 1.f * font_height};
for (std::size_t i = lines[l].begin; i < lines[l].end; ++i)
bbox |= glyphs[i].position;
line_bbox[l] = bbox;
}
// Compute total text size
geom::vector<float, 2> text_size{0.f, 0.f};
for (auto const & l : line_bbox)
{
text_size[0] = std::max(text_size[0], l[0].length());
text_size[1] += l[1].length();
}
// TODO: handle text overflow
// Position lines & glyphs inside label bbox
std::vector<geom::vector<float, 2>> line_offset(lines.size());
{
float current_y;
switch (valign_)
{
case valignment::top:
current_y = bbox[1].min;
break;
case valignment::center:
current_y = bbox[1].center() - text_size[1] / 2.f;
break;
case valignment::bottom:
current_y = bbox[1].max - text_size[1];
break;
}
for (std::size_t l = 0; l < lines.size(); ++l)
{
int spaces = 0;
for (std::size_t i = lines[l].begin; i < lines[l].end; ++i)
if (std::isspace(glyphs[i].character))
++spaces;
geom::vector<float, 2> offset{0.f, current_y};
float space_extra = 0.f;
switch (halign_)
{
case halignment::left:
offset[0] = bbox[0].min - line_bbox[l][0].min;
break;
case halignment::center:
offset[0] = bbox[0].center() - line_bbox[l][0].length() / 2.f - line_bbox[l][0].min;
break;
case halignment::right:
offset[0] = bbox[0].max - line_bbox[l][0].length() - line_bbox[l][0].min;
break;
case halignment::stretch:
offset[0] = bbox[0].min - line_bbox[l][0].min;
if ((l + 1 != lines.size() && text_[lines[l].end] != '\n') && spaces > 0)
space_extra = (bbox[0].length() - line_bbox[l][0].length()) / spaces;
break;
}
line_offset[l] = offset;
for (std::size_t i = lines[l].begin; i < lines[l].end; ++i)
{
glyphs[i].position += offset;
if (std::isspace(glyphs[i].character))
offset[0] += space_extra;
}
current_y += line_bbox[l][1].length();
}
}
// Convert chunks into image batches
{
std::size_t begin = 0;
for (auto const & ch : glyph_chunks)
{
auto & batch = state.batches.emplace_back();
auto font = ch.style.is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get();
batch.texture = &font->atlas();
for (std::size_t i = begin; i < ch.end; ++i)
{
auto & g = glyphs[i];
auto tc = font->texcoords(g.character);
if (!tc) continue;
g.position[0] += (std::round(g.position[0].min) - g.position[0].min);
g.position[1] += (std::round(g.position[1].min) - g.position[1].min);
auto & image = batch.images.emplace_back();
image.position = g.position;
image.texcoords = *tc;
}
begin = ch.end;
}
}
// Convert chunks into underline & strikethrough batches
{
float const line_width = 1.f * (*st->text_scale);
auto & batch = state.batches.emplace_back();
batch.texture = single_white_pixel_texture().get();
std::size_t ch_begin = 0;
std::size_t ch = 0;
for (std::size_t l = 0; l < lines.size(); ++l)
{
float underline_y = std::round(line_offset[l][1] + font_height - line_width);
float strikethrough_y = std::round(line_offset[l][1] + font_height / 2.f - line_width / 2.f);
std::optional<geom::interval<float>> current_underline;
std::optional<geom::interval<float>> current_strikethrough;
auto flush_underline = [&]{
auto & image = batch.images.emplace_back();
image.position[0] = *current_underline;
image.position[1] = {underline_y, underline_y + line_width};
current_underline = std::nullopt;
};
auto flush_strikethrough = [&]{
auto & image = batch.images.emplace_back();
image.position[0] = *current_strikethrough;
image.position[1] = {strikethrough_y, strikethrough_y + line_width};
current_strikethrough = std::nullopt;
};
for (; ch < glyph_chunks.size(); ++ch)
{
auto const & chunk = glyph_chunks[ch];
bool const underline = chunk.style.is_set(text_style_flag::underline);
bool const strikethrough = chunk.style.is_set(text_style_flag::strikethrough);
std::size_t ibegin = std::max(ch_begin, lines[l].begin);
std::size_t iend = std::min(chunk.end, lines[l].end);
if (ibegin < iend)
{
geom::interval<float> x_range;
for (std::size_t i = ibegin; i < iend; ++i)
x_range |= glyphs[i].position[0];
if (underline && current_underline)
{
*current_underline |= x_range;
}
else if (underline && !current_underline)
{
current_underline = x_range;
}
else if (!underline && current_underline)
{
flush_underline();
}
if (strikethrough && current_strikethrough)
{
*current_strikethrough |= x_range;
}
else if (strikethrough && !current_strikethrough)
{
current_strikethrough = x_range;
}
else if (!strikethrough && current_strikethrough)
{
flush_strikethrough();
}
}
if (chunk.end > lines[l].end)
break;
ch_begin = chunk.end;
}
if (current_underline)
flush_underline();
if (current_strikethrough)
flush_strikethrough();
}
}
state.size = text_size;
return state;
}
std::shared_ptr<gfx::texture_2d> label::single_white_pixel_texture() const
{
static std::weak_ptr<gfx::texture_2d> texture;
if (!single_white_pixel_texture_)
{
std::shared_ptr<gfx::texture_2d> ptr;
if (!(ptr = texture.lock()))
{
ptr = std::make_shared<gfx::texture_2d>();
gfx::pixmap_rgba pm({1, 1}, {255, 255, 255, 255});
ptr->load(pm);
ptr->nearest_filter();
texture = ptr;
}
single_white_pixel_texture_ = ptr;
}
return single_white_pixel_texture_;
}
}