Implement ui::label link tag

This commit is contained in:
Nikita Lisitsa 2022-05-22 19:28:14 +03:00
parent 9300dc6779
commit f0cf916b85
4 changed files with 172 additions and 10 deletions

View file

@ -62,9 +62,16 @@ namespace psemek::ui
virtual struct image_provider * set_image_provider(struct image_provider * provider);
virtual struct image_provider * image_provider() const { return image_provider_; }
using link_callback = std::function<void(std::string_view)>;
virtual void on_link_click(link_callback callback);
struct shape const & shape() const override { return shape_; }
void reshape(geom::box<float, 2> const & bbox) override;
bool on_event(ui::mouse_move const & e) override;
bool on_event(ui::mouse_click const & e) override;
geom::box<float, 2> size_constraints() const override;
geom::interval<float> width_constraints(float height) const override;
@ -89,6 +96,8 @@ namespace psemek::ui
struct image_provider * image_provider_ = nullptr;
link_callback link_callback_;
box_shape shape_;
struct text_chunk
@ -96,12 +105,14 @@ namespace psemek::ui
std::string_view text;
text_style style; // to be OR'd with base style
std::optional<gfx::color_rgba> color;
std::optional<std::string_view> link;
};
struct image_chunk
{
std::string_view id;
std::optional<gfx::color_rgba> color;
std::optional<std::string_view> link;
};
using chunk = std::variant<text_chunk, image_chunk>;
@ -127,10 +138,14 @@ namespace psemek::ui
std::vector<batch> batches;
geom::vector<float, 2> size{0.f, 0.f};
std::vector<std::pair<geom::box<float, 2>, std::string_view>> link_bboxes;
};
mutable std::optional<cached_state> cached_state_;
mutable std::optional<cached_state> cached_state_inf_;
std::optional<std::string_view> selected_link_;
bool mouse_down_ = false;
void update_cached_state() const;
cached_state cached_state_for(geom::box<float, 2> const & bbox) const;

View file

@ -58,9 +58,17 @@ namespace psemek::ui
std::optional<gfx::color_rgba> text_color;
std::optional<int> text_scale;
std::optional<ui::text_style> text_style;
std::optional<geom::vector<int, 2>> text_shadow_offset;
std::optional<ui::text_style> text_style;
std::optional<gfx::color_rgba> link_color;
std::optional<ui::text_style> link_style;
std::optional<gfx::color_rgba> link_hover_color;
std::optional<ui::text_style> link_hover_style;
std::optional<gfx::color_rgba> link_click_color;
std::optional<ui::text_style> link_click_style;
std::shared_ptr<struct font> font;
std::shared_ptr<struct font> bold_font;

View file

@ -1,5 +1,7 @@
#include <psemek/ui/label.hpp>
#include <psemek/geom/contains.hpp>
#include <stdexcept>
#include <cctype>
#include <unordered_map>
@ -17,7 +19,7 @@ namespace psemek::ui
text_ = std::move(text);
chunks_.clear();
chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}, std::nullopt});
chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}, std::nullopt, std::nullopt});
on_state_changed();
}
@ -28,6 +30,7 @@ namespace psemek::ui
"uline",
"strike",
"color",
"link",
};
void check_attribute(std::string_view tag, std::optional<std::string_view> const & attribute)
@ -44,11 +47,17 @@ namespace psemek::ui
if (!gfx::parse_color(*attribute))
throw std::runtime_error("failed to parse color \"" + std::string(*attribute) + "\"");
}
else if (tag == "link")
{
if (!attribute)
throw std::runtime_error("tag [link] requires an attribute");
}
}
void label::set_tagged_text(std::string text)
{
text_ = std::move(text);
selected_link_ = std::nullopt;
chunks_.clear();
@ -69,6 +78,8 @@ namespace psemek::ui
chunk.style.set(text_style_flag::strikethrough);
if (!tags_stack["color"].empty())
chunk.color = *gfx::parse_color(*tags_stack["color"].back());
if (!tags_stack["link"].empty())
chunk.link = tags_stack["link"].back();
chunk.text = *text;
chunks_.push_back(chunk);
}
@ -82,11 +93,15 @@ namespace psemek::ui
chunk.id = *tag->attribute;
if (!tags_stack["color"].empty())
chunk.color = *gfx::parse_color(*tags_stack["color"].back());
if (!tags_stack["link"].empty())
chunk.link = tags_stack["link"].back();
chunks_.push_back(chunk);
}
else if (known_tags.contains(tag->type))
{
check_attribute(tag->type, tag->attribute);
if (tag->type == "link" && !tags_stack["link"].empty())
throw std::runtime_error("tag [link] cannot be nested");
tags_stack[tag->type].push_back(tag->attribute);
}
else
@ -136,6 +151,11 @@ namespace psemek::ui
return provider;
}
void label::on_link_click(link_callback callback)
{
link_callback_ = std::move(callback);
}
void label::set_overflow(overflow_mode value)
{
overflow_ = value;
@ -148,6 +168,63 @@ namespace psemek::ui
cached_state_.reset();
}
bool label::on_event(ui::mouse_move const & e)
{
if (cached_state_)
{
std::optional<std::string_view> new_selected_link;
auto const p = geom::cast<float>(e.position);
for (auto const & b : cached_state_->link_bboxes)
{
if (geom::contains(b.first, p))
{
new_selected_link = b.second;
break;
}
}
if (new_selected_link != selected_link_)
{
selected_link_ = new_selected_link;
mouse_down_ = false;
on_state_changed();
}
}
return false;
}
bool label::on_event(ui::mouse_click const & e)
{
if (e.button == mouse_button::left)
{
if (e.down)
{
if (selected_link_)
{
if (!mouse_down_)
{
mouse_down_ = true;
if (link_callback_)
post([cb = link_callback_, text = *selected_link_]{ cb(text); });
on_state_changed();
return true;
}
}
}
else
{
if (mouse_down_)
{
mouse_down_ = false;
on_state_changed();
}
}
}
return false;
}
geom::box<float, 2> label::size_constraints() const
{
static float const inf = std::numeric_limits<float>::infinity();
@ -256,6 +333,7 @@ namespace psemek::ui
text_style style;
gfx::color_rgba color;
gfx::texture_view_2d image;
std::optional<std::string_view> link;
};
std::vector<item> items;
@ -270,21 +348,50 @@ namespace psemek::ui
{
if (auto text = std::get_if<text_chunk>(&chunk))
{
auto const merged_text_style = *st->text_style | text->style;
auto text_style = *st->text_style | text->style;
bool const bold = merged_text_style.is_set(text_style_flag::bold);
if (text->link)
{
if (selected_link_ && *text->link == *selected_link_)
{
if (mouse_down_)
text_style |= *st->link_click_style;
else
text_style |= *st->link_hover_style;
}
else
text_style |= *st->link_style;
}
gfx::color_rgba color;
if (text->color)
color = *text->color;
else if (text->link)
{
if (selected_link_ && *text->link == *selected_link_)
{
if (mouse_down_)
color = *st->link_click_color;
else
color = *st->link_hover_color;
}
else
color = *st->link_color;
}
else
color = *st->text_color;
bool const bold = text_style.is_set(text_style_flag::bold);
auto font = bold ? st->bold_font.get() : st->font.get();
auto color = text->color ? *text->color : *st->text_color;
auto glyphs = font->shape(text->text, opts, pen);
items.reserve(items.size() + glyphs.size());
for (auto const & g : glyphs)
items.push_back(item{g.position, g.character});
item_chunks.push_back({items.size(), merged_text_style, color, {}});
item_chunks.push_back({items.size(), text_style, color, {}, text->link});
}
else if (auto image = std::get_if<image_chunk>(&chunk))
{
@ -306,7 +413,7 @@ namespace psemek::ui
pen[0] = position[0].max;
items.push_back({position, {}});
item_chunks.push_back({items.size(), {}, color, texture});
item_chunks.push_back({items.size(), {}, color, texture, image->link});
}
}
}
@ -590,6 +697,24 @@ namespace psemek::ui
}
}
// Generate link bboxes
{
std::size_t begin = 0;
for (auto const & ch : item_chunks)
{
if (ch.link)
{
geom::box<float, 2> bbox;
for (std::size_t i = begin; i < ch.end; ++i)
bbox |= items[i].position;
state.link_bboxes.push_back({bbox, *ch.link});
}
begin = ch.end;
}
}
state.size = text_size;
return state;

View file

@ -71,8 +71,14 @@ namespace psemek::ui
merge(dst.outer_margin, src.outer_margin);
merge(dst.text_color, src.text_color);
merge(dst.text_scale, src.text_scale);
merge(dst.text_shadow_offset, src.text_shadow_offset);
merge(dst.text_style, src.text_style);
merge(dst.text_shadow_offset, src.text_shadow_offset);
merge(dst.link_color, src.link_color);
merge(dst.link_style, src.link_style);
merge(dst.link_hover_color, src.link_hover_color);
merge(dst.link_hover_style, src.link_hover_style);
merge(dst.link_click_color, src.link_click_color);
merge(dst.link_click_style, src.link_click_style);
merge(dst.font, src.font);
merge(dst.bold_font, src.bold_font);
}
@ -125,9 +131,17 @@ namespace psemek::ui
s.text_color = {255, 255, 255, 255};
s.text_scale = 1;
s.text_style = text_style{};
s.text_shadow_offset = {1, 1};
s.text_style = text_style{};
s.link_color = {0, 0, 255, 255};
s.link_style = text_style{};
s.link_hover_color = {127, 127, 255, 255};
s.link_hover_style = text_style{};
s.link_click_color = {0, 0, 127, 255};
s.link_click_style = text_style{};
return s;
}