ui::label tagged text support: implement color tag

This commit is contained in:
Nikita Lisitsa 2022-05-21 21:45:23 +03:00
parent 7b99158b4d
commit 183f5283e2
2 changed files with 82 additions and 35 deletions

View file

@ -89,6 +89,7 @@ namespace psemek::ui
{ {
std::string_view text; std::string_view text;
text_style style; // to be OR'd with base style text_style style; // to be OR'd with base style
std::optional<gfx::color_rgba> color;
}; };
using chunk = std::variant<text_chunk>; using chunk = std::variant<text_chunk>;
@ -107,6 +108,7 @@ namespace psemek::ui
struct batch struct batch
{ {
gfx::texture_2d const * texture; gfx::texture_2d const * texture;
gfx::color_rgba color;
std::vector<image> images; std::vector<image> images;
}; };

View file

@ -17,11 +17,35 @@ namespace psemek::ui
text_ = std::move(text); text_ = std::move(text);
chunks_.clear(); chunks_.clear();
chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}}); chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}, std::nullopt});
on_state_changed(); on_state_changed();
} }
static std::unordered_set<std::string_view> const known_tags
{
"bold",
"uline",
"strike",
"color",
};
void check_attribute(std::string_view tag, std::optional<std::string_view> const & attribute)
{
if (tag == "bold" || tag == "uline" || tag == "strike")
{
if (attribute)
throw std::runtime_error("tag [" + std::string(tag) + " doesn't support attributes");
}
else if (tag == "color")
{
if (!attribute)
throw std::runtime_error("tag [color] requires an attribute");
if (!gfx::parse_color(*attribute))
throw std::runtime_error("failed to parse color \"" + std::string(*attribute) + "\"");
}
}
void label::set_tagged_text(std::string text) void label::set_tagged_text(std::string text)
{ {
text_ = std::move(text); text_ = std::move(text);
@ -30,47 +54,41 @@ namespace psemek::ui
auto parse_result = tagged_text::parse(text_); auto parse_result = tagged_text::parse(text_);
std::unordered_map<std::string_view, int> tags_stack; std::unordered_map<std::string_view, std::vector<std::optional<std::string_view>>> tags_stack;
tags_stack["bold"] = 0;
tags_stack["uline"] = 0;
tags_stack["strike"] = 0;
for (auto const & token : parse_result.tokens) for (auto const & token : parse_result.tokens)
{ {
if (auto text = std::get_if<std::string_view>(&token)) if (auto text = std::get_if<std::string_view>(&token))
{ {
text_chunk chunk; text_chunk chunk;
if (tags_stack["bold"] > 0) if (!tags_stack["bold"].empty())
chunk.style.set(text_style_flag::bold); chunk.style.set(text_style_flag::bold);
if (tags_stack["uline"] > 0) if (!tags_stack["uline"].empty())
chunk.style.set(text_style_flag::underline); chunk.style.set(text_style_flag::underline);
if (tags_stack["strike"] > 0) if (!tags_stack["strike"].empty())
chunk.style.set(text_style_flag::strikethrough); chunk.style.set(text_style_flag::strikethrough);
if (!tags_stack["color"].empty())
chunk.color = *gfx::parse_color(*tags_stack["color"].back());
chunk.text = *text; chunk.text = *text;
chunks_.push_back(chunk); chunks_.push_back(chunk);
} }
else if (auto tag = std::get_if<tagged_text::opening_tag>(&token)) else if (auto tag = std::get_if<tagged_text::opening_tag>(&token))
{ {
if (tag->type == "bold") if (known_tags.contains(tag->type))
{ {
tags_stack["bold"] += 1; check_attribute(tag->type, tag->attribute);
} tags_stack[tag->type].push_back(tag->attribute);
else if (tag->type == "uline")
{
tags_stack["uline"] += 1;
}
else if (tag->type == "strike")
{
tags_stack["strike"] += 1;
} }
else else
throw std::runtime_error("unknown tag [" + std::string(tag->type) + "]"); throw std::runtime_error("unknown tag [" + std::string(tag->type) + "]");
} }
else if (auto tag = std::get_if<tagged_text::closing_tag>(&token)) else if (auto tag = std::get_if<tagged_text::closing_tag>(&token))
{ {
if (tags_stack[tag->type] == 0) if (!known_tags.contains(tag->type))
throw std::runtime_error("unknown tag [" + std::string(tag->type) + "]");
if (tags_stack[tag->type].empty())
throw std::runtime_error("mismatched opening & closing tags for [" + std::string(tag->type) + "]"); throw std::runtime_error("mismatched opening & closing tags for [" + std::string(tag->type) + "]");
tags_stack[tag->type] -= 1; tags_stack[tag->type].pop_back();
} }
} }
@ -165,13 +183,16 @@ namespace psemek::ui
{ {
auto const offset = geom::cast<float>(*st->text_shadow_offset); auto const offset = geom::cast<float>(*st->text_shadow_offset);
for (auto const & batch : cached_state_->batches) for (auto const & batch : cached_state_->batches)
{
if (batch.color[3] < 255) continue;
for (auto const & image : batch.images) 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}); 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 & batch : cached_state_->batches)
for (auto const & image : batch.images) 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}); p.draw_image(image.position, gfx::texture_view_2d{batch.texture, image.texcoords}, {batch.color, painter::color_mode::multiply});
} }
void label::on_state_changed() void label::on_state_changed()
@ -209,6 +230,7 @@ namespace psemek::ui
{ {
std::size_t end; std::size_t end;
text_style style; text_style style;
gfx::color_rgba color;
}; };
std::vector<glyph> glyphs; std::vector<glyph> glyphs;
@ -231,10 +253,12 @@ namespace psemek::ui
auto chunk_glyphs = font->shape(text->text, opts, pen); auto chunk_glyphs = font->shape(text->text, opts, pen);
auto color = text->color ? *text->color : *st->text_color;
glyphs.reserve(glyphs.size() + chunk_glyphs.size()); glyphs.reserve(glyphs.size() + chunk_glyphs.size());
glyphs.insert(glyphs.end(), chunk_glyphs.begin(), chunk_glyphs.end()); glyphs.insert(glyphs.end(), chunk_glyphs.begin(), chunk_glyphs.end());
glyph_chunks.push_back({glyphs.size(), merged_text_style}); glyph_chunks.push_back({glyphs.size(), merged_text_style, color});
} }
} }
} }
@ -400,6 +424,7 @@ namespace psemek::ui
auto font = ch.style.is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get(); auto font = ch.style.is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get();
batch.texture = &font->atlas(); batch.texture = &font->atlas();
batch.color = ch.color;
for (std::size_t i = begin; i < ch.end; ++i) for (std::size_t i = begin; i < ch.end; ++i)
{ {
@ -424,9 +449,6 @@ namespace psemek::ui
{ {
float const line_width = 1.f * (*st->text_scale); 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_begin = 0;
std::size_t ch = 0; std::size_t ch = 0;
for (std::size_t l = 0; l < lines.size(); ++l) for (std::size_t l = 0; l < lines.size(); ++l)
@ -434,19 +456,30 @@ namespace psemek::ui
float underline_y = std::round(line_offset[l][1] + font_height - line_width); 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); 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<std::pair<geom::interval<float>, gfx::color_rgba>> current_underline;
std::optional<geom::interval<float>> current_strikethrough; std::optional<std::pair<geom::interval<float>, gfx::color_rgba>> current_strikethrough;
auto batch = [&](gfx::color_rgba const & color) -> cached_state::batch & {
auto texture = single_white_pixel_texture().get();
if (state.batches.empty() || state.batches.back().texture != texture || state.batches.back().color != color)
{
auto & b = state.batches.emplace_back();
b.texture = texture;
b.color = color;
}
return state.batches.back();
};
auto flush_underline = [&]{ auto flush_underline = [&]{
auto & image = batch.images.emplace_back(); auto & image = batch(current_underline->second).images.emplace_back();
image.position[0] = *current_underline; image.position[0] = current_underline->first;
image.position[1] = {underline_y, underline_y + line_width}; image.position[1] = {underline_y, underline_y + line_width};
current_underline = std::nullopt; current_underline = std::nullopt;
}; };
auto flush_strikethrough = [&]{ auto flush_strikethrough = [&]{
auto & image = batch.images.emplace_back(); auto & image = batch(current_strikethrough->second).images.emplace_back();
image.position[0] = *current_strikethrough; image.position[0] = current_strikethrough->first;
image.position[1] = {strikethrough_y, strikethrough_y + line_width}; image.position[1] = {strikethrough_y, strikethrough_y + line_width};
current_strikethrough = std::nullopt; current_strikethrough = std::nullopt;
}; };
@ -469,11 +502,17 @@ namespace psemek::ui
if (underline && current_underline) if (underline && current_underline)
{ {
*current_underline |= x_range; if (current_underline->second != chunk.color)
{
flush_underline();
current_underline = {x_range, chunk.color};
}
else
current_underline->first |= x_range;
} }
else if (underline && !current_underline) else if (underline && !current_underline)
{ {
current_underline = x_range; current_underline = {x_range, chunk.color};
} }
else if (!underline && current_underline) else if (!underline && current_underline)
{ {
@ -482,11 +521,17 @@ namespace psemek::ui
if (strikethrough && current_strikethrough) if (strikethrough && current_strikethrough)
{ {
*current_strikethrough |= x_range; if (current_strikethrough->second != chunk.color)
{
flush_strikethrough();
current_strikethrough = {x_range, chunk.color};
}
else
current_strikethrough->first |= x_range;
} }
else if (strikethrough && !current_strikethrough) else if (strikethrough && !current_strikethrough)
{ {
current_strikethrough = x_range; current_strikethrough = {x_range, chunk.color};
} }
else if (!strikethrough && current_strikethrough) else if (!strikethrough && current_strikethrough)
{ {