Implement ui::label link tag
This commit is contained in:
parent
9300dc6779
commit
f0cf916b85
4 changed files with 172 additions and 10 deletions
|
|
@ -62,9 +62,16 @@ namespace psemek::ui
|
||||||
virtual struct image_provider * set_image_provider(struct image_provider * provider);
|
virtual struct image_provider * set_image_provider(struct image_provider * provider);
|
||||||
virtual struct image_provider * image_provider() const { return image_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_; }
|
struct shape const & shape() const override { return shape_; }
|
||||||
void reshape(geom::box<float, 2> const & bbox) override;
|
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::box<float, 2> size_constraints() const override;
|
||||||
|
|
||||||
geom::interval<float> width_constraints(float height) const override;
|
geom::interval<float> width_constraints(float height) const override;
|
||||||
|
|
@ -89,6 +96,8 @@ namespace psemek::ui
|
||||||
|
|
||||||
struct image_provider * image_provider_ = nullptr;
|
struct image_provider * image_provider_ = nullptr;
|
||||||
|
|
||||||
|
link_callback link_callback_;
|
||||||
|
|
||||||
box_shape shape_;
|
box_shape shape_;
|
||||||
|
|
||||||
struct text_chunk
|
struct text_chunk
|
||||||
|
|
@ -96,12 +105,14 @@ 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;
|
std::optional<gfx::color_rgba> color;
|
||||||
|
std::optional<std::string_view> link;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct image_chunk
|
struct image_chunk
|
||||||
{
|
{
|
||||||
std::string_view id;
|
std::string_view id;
|
||||||
std::optional<gfx::color_rgba> color;
|
std::optional<gfx::color_rgba> color;
|
||||||
|
std::optional<std::string_view> link;
|
||||||
};
|
};
|
||||||
|
|
||||||
using chunk = std::variant<text_chunk, image_chunk>;
|
using chunk = std::variant<text_chunk, image_chunk>;
|
||||||
|
|
@ -127,10 +138,14 @@ namespace psemek::ui
|
||||||
|
|
||||||
std::vector<batch> batches;
|
std::vector<batch> batches;
|
||||||
geom::vector<float, 2> size{0.f, 0.f};
|
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_;
|
||||||
mutable std::optional<cached_state> cached_state_inf_;
|
mutable std::optional<cached_state> cached_state_inf_;
|
||||||
|
std::optional<std::string_view> selected_link_;
|
||||||
|
bool mouse_down_ = false;
|
||||||
|
|
||||||
void update_cached_state() const;
|
void update_cached_state() const;
|
||||||
cached_state cached_state_for(geom::box<float, 2> const & bbox) const;
|
cached_state cached_state_for(geom::box<float, 2> const & bbox) const;
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,17 @@ namespace psemek::ui
|
||||||
|
|
||||||
std::optional<gfx::color_rgba> text_color;
|
std::optional<gfx::color_rgba> text_color;
|
||||||
std::optional<int> text_scale;
|
std::optional<int> text_scale;
|
||||||
|
std::optional<ui::text_style> text_style;
|
||||||
std::optional<geom::vector<int, 2>> text_shadow_offset;
|
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> font;
|
||||||
std::shared_ptr<struct font> bold_font;
|
std::shared_ptr<struct font> bold_font;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include <psemek/ui/label.hpp>
|
#include <psemek/ui/label.hpp>
|
||||||
|
|
||||||
|
#include <psemek/geom/contains.hpp>
|
||||||
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
@ -17,7 +19,7 @@ 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{}, std::nullopt});
|
chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}, std::nullopt, std::nullopt});
|
||||||
|
|
||||||
on_state_changed();
|
on_state_changed();
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +30,7 @@ namespace psemek::ui
|
||||||
"uline",
|
"uline",
|
||||||
"strike",
|
"strike",
|
||||||
"color",
|
"color",
|
||||||
|
"link",
|
||||||
};
|
};
|
||||||
|
|
||||||
void check_attribute(std::string_view tag, std::optional<std::string_view> const & attribute)
|
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))
|
if (!gfx::parse_color(*attribute))
|
||||||
throw std::runtime_error("failed to parse color \"" + std::string(*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)
|
void label::set_tagged_text(std::string text)
|
||||||
{
|
{
|
||||||
text_ = std::move(text);
|
text_ = std::move(text);
|
||||||
|
selected_link_ = std::nullopt;
|
||||||
|
|
||||||
chunks_.clear();
|
chunks_.clear();
|
||||||
|
|
||||||
|
|
@ -69,6 +78,8 @@ namespace psemek::ui
|
||||||
chunk.style.set(text_style_flag::strikethrough);
|
chunk.style.set(text_style_flag::strikethrough);
|
||||||
if (!tags_stack["color"].empty())
|
if (!tags_stack["color"].empty())
|
||||||
chunk.color = *gfx::parse_color(*tags_stack["color"].back());
|
chunk.color = *gfx::parse_color(*tags_stack["color"].back());
|
||||||
|
if (!tags_stack["link"].empty())
|
||||||
|
chunk.link = tags_stack["link"].back();
|
||||||
chunk.text = *text;
|
chunk.text = *text;
|
||||||
chunks_.push_back(chunk);
|
chunks_.push_back(chunk);
|
||||||
}
|
}
|
||||||
|
|
@ -82,11 +93,15 @@ namespace psemek::ui
|
||||||
chunk.id = *tag->attribute;
|
chunk.id = *tag->attribute;
|
||||||
if (!tags_stack["color"].empty())
|
if (!tags_stack["color"].empty())
|
||||||
chunk.color = *gfx::parse_color(*tags_stack["color"].back());
|
chunk.color = *gfx::parse_color(*tags_stack["color"].back());
|
||||||
|
if (!tags_stack["link"].empty())
|
||||||
|
chunk.link = tags_stack["link"].back();
|
||||||
chunks_.push_back(chunk);
|
chunks_.push_back(chunk);
|
||||||
}
|
}
|
||||||
else if (known_tags.contains(tag->type))
|
else if (known_tags.contains(tag->type))
|
||||||
{
|
{
|
||||||
check_attribute(tag->type, tag->attribute);
|
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);
|
tags_stack[tag->type].push_back(tag->attribute);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -136,6 +151,11 @@ namespace psemek::ui
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void label::on_link_click(link_callback callback)
|
||||||
|
{
|
||||||
|
link_callback_ = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
void label::set_overflow(overflow_mode value)
|
void label::set_overflow(overflow_mode value)
|
||||||
{
|
{
|
||||||
overflow_ = value;
|
overflow_ = value;
|
||||||
|
|
@ -148,6 +168,63 @@ namespace psemek::ui
|
||||||
cached_state_.reset();
|
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
|
geom::box<float, 2> label::size_constraints() const
|
||||||
{
|
{
|
||||||
static float const inf = std::numeric_limits<float>::infinity();
|
static float const inf = std::numeric_limits<float>::infinity();
|
||||||
|
|
@ -256,6 +333,7 @@ namespace psemek::ui
|
||||||
text_style style;
|
text_style style;
|
||||||
gfx::color_rgba color;
|
gfx::color_rgba color;
|
||||||
gfx::texture_view_2d image;
|
gfx::texture_view_2d image;
|
||||||
|
std::optional<std::string_view> link;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<item> items;
|
std::vector<item> items;
|
||||||
|
|
@ -270,21 +348,50 @@ namespace psemek::ui
|
||||||
{
|
{
|
||||||
if (auto text = std::get_if<text_chunk>(&chunk))
|
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 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);
|
auto glyphs = font->shape(text->text, opts, pen);
|
||||||
|
|
||||||
items.reserve(items.size() + glyphs.size());
|
items.reserve(items.size() + glyphs.size());
|
||||||
for (auto const & g : glyphs)
|
for (auto const & g : glyphs)
|
||||||
items.push_back(item{g.position, g.character});
|
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))
|
else if (auto image = std::get_if<image_chunk>(&chunk))
|
||||||
{
|
{
|
||||||
|
|
@ -306,7 +413,7 @@ namespace psemek::ui
|
||||||
pen[0] = position[0].max;
|
pen[0] = position[0].max;
|
||||||
|
|
||||||
items.push_back({position, {}});
|
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;
|
state.size = text_size;
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,14 @@ namespace psemek::ui
|
||||||
merge(dst.outer_margin, src.outer_margin);
|
merge(dst.outer_margin, src.outer_margin);
|
||||||
merge(dst.text_color, src.text_color);
|
merge(dst.text_color, src.text_color);
|
||||||
merge(dst.text_scale, src.text_scale);
|
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_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.font, src.font);
|
||||||
merge(dst.bold_font, src.bold_font);
|
merge(dst.bold_font, src.bold_font);
|
||||||
}
|
}
|
||||||
|
|
@ -125,9 +131,17 @@ namespace psemek::ui
|
||||||
|
|
||||||
s.text_color = {255, 255, 255, 255};
|
s.text_color = {255, 255, 255, 255};
|
||||||
s.text_scale = 1;
|
s.text_scale = 1;
|
||||||
|
s.text_style = text_style{};
|
||||||
s.text_shadow_offset = {1, 1};
|
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;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue