psemek/libs/ui/source/edit.cpp

530 lines
11 KiB
C++

#include <psemek/ui/edit.hpp>
#include <psemek/util/to_string.hpp>
#include <psemek/util/unicode.hpp>
#include <psemek/sdl2/cursor.hpp>
namespace psemek::ui
{
bool edit::set_text(std::string_view text, bool signal)
{
return set_text(util::from_utf8(std::string(text)), signal);
}
bool edit::set_text(std::u32string text, bool signal)
{
if (validator_ && !validator_(text))
return false;
text_ = std::move(text);
caret_ = std::min(caret_, text_.size());
on_state_changed();
post_text_changed();
if (signal)
post_text_entered();
return true;
}
void edit::set_font(font_type f)
{
if (font_ != f)
{
font_ = f;
on_state_changed();
}
}
void edit::set_halign(halignment value)
{
if (halign_ != value)
{
halign_ = value;
on_state_changed();
}
}
void edit::set_valign(valignment value)
{
if (valign_ != value)
{
valign_ = value;
on_state_changed();
}
}
bool edit::set_validator(validator_type validator)
{
if (validator && !validator(text_))
return false;
validator_ = std::move(validator);
return true;
}
edit::validator_type edit::numeric(int max_length)
{
return [max_length](std::u32string_view const str) -> bool {
auto begin = str.begin();
auto end = str.end();
if (begin != end && *begin == '-')
++begin;
return std::all_of(begin, end, [](char32_t c){ return '0' <= c && c <= '9'; }) && (end - begin) <= max_length;
};
}
edit::validator_type edit::numeric_nonnegative(int max_length)
{
return [max_length](std::u32string_view const str) -> bool {
return std::all_of(str.begin(), str.end(), [](char32_t c){ return '0' <= c && c <= '9'; }) && str.length() <= max_length;
};
}
void edit::on_text_entered(callback_type callback, bool signal)
{
on_text_entered_ = std::move(callback);
if (signal)
post_text_entered();
}
void edit::on_start_input(std::function<void()> callback)
{
on_start_input_ = std::move(callback);
}
void edit::on_text_changed(callback_type callback)
{
on_text_changed_ = std::move(callback);
}
bool edit::on_event(mouse_move const & e)
{
auto m = geom::cast<float>(e.position);
mouse_x_ = m[0];
bool new_mouseover = shape_.contains(m);
if (!mouseover_ && new_mouseover)
sdl2::set_cursor(sdl2::cursor_type::beam);
else if (mouseover_ && !new_mouseover)
sdl2::set_cursor(sdl2::cursor_type::arrow);
mouseover_ = new_mouseover;
return false;
}
bool edit::on_event(mouse_click const & e)
{
if (e.button == mouse_button::left && e.down)
{
if (mouseover_)
{
if (!editing_)
{
editing_ = true;
old_text_ = text_;
start_text_input();
reset_caret();
post_start_input();
}
if (cached_state_ && mouse_x_)
{
float x_offset = 0.f;
switch (halign_)
{
case halignment::left:
x_offset = text_box_[0].min;
break;
case halignment::center:
x_offset = text_box_[0].center() - cached_state_->size[0] * 0.5f;
break;
case halignment::right:
x_offset = text_box_[0].max - cached_state_->size[0];
break;
}
auto it = std::lower_bound(cached_state_->images.begin(), cached_state_->images.end(), *mouse_x_, [&](auto const & i, float x){
return i.position[0].center() + x_offset < x;
});
caret_ = it - cached_state_->images.begin();
caret_blink_timer_ = 0.f;
caret_visible_ = true;
}
return true;
}
else
{
if (editing_)
{
editing_ = false;
stop_text_input();
reset_caret();
post_text_entered();
}
}
}
if (e.button == mouse_button::right && e.down)
{
if (editing_)
{
editing_ = false;
text_ = old_text_;
cached_state_.reset();
stop_text_input();
reset_caret();
caret_ = std::min(caret_, text_.size());
post_text_entered();
return true;
}
}
return false;
}
bool edit::on_event(key_press const & e)
{
if (e.key == SDLK_LCTRL)
ctrl_down_ = e.down;
if (e.down && editing_)
{
if (e.key == SDLK_LEFT)
{
if (ctrl_down_)
{
std::size_t start = caret_;
while (start > 0 && std::isspace(text_[start - 1])) --start;
while (start > 0 && !std::isspace(text_[start - 1])) --start;
caret_ = start;
reset_caret();
}
else
{
if (caret_ > 0)
{
--caret_;
reset_caret();
}
}
}
else if (e.key == SDLK_RIGHT)
{
if (ctrl_down_)
{
std::size_t end = caret_;
while (end < text_.size() && !std::isspace(text_[end])) ++end;
while (end < text_.size() && std::isspace(text_[end])) ++end;
caret_ = end;
reset_caret();
}
else
{
if (caret_ < text_.size())
{
++caret_;
reset_caret();
}
}
}
else if (e.key == SDLK_HOME)
{
caret_ = 0;
reset_caret();
}
else if (e.key == SDLK_END)
{
caret_ = text_.size();
reset_caret();
}
else if (e.key == SDLK_RETURN)
{
if (editing_)
{
editing_ = false;
stop_text_input();
reset_caret();
post_text_entered();
}
}
else if (e.key == SDLK_ESCAPE)
{
if (editing_)
{
editing_ = false;
text_ = old_text_;
cached_state_.reset();
stop_text_input();
reset_caret();
caret_ = std::min(caret_, text_.size());
post_text_changed();
post_text_entered();
}
}
else if (e.key == SDLK_BACKSPACE)
{
if (ctrl_down_)
{
std::size_t start = caret_;
while (start > 0 && std::isspace(text_[start - 1])) --start;
while (start > 0 && !std::isspace(text_[start - 1])) --start;
auto new_text = text_;
new_text.erase(start, caret_ - start);
if (set_text(std::move(new_text), false))
{
caret_ = start;
reset_caret();
}
}
else
{
if (caret_ > 0)
{
--caret_;
auto new_text = text_;
new_text.erase(new_text.begin() + caret_);
if (!set_text(std::move(new_text), false))
++caret_;
reset_caret();
}
}
}
else if (e.key == SDLK_DELETE)
{
if (ctrl_down_)
{
std::size_t end = caret_;
while (end < text_.size() && !std::isspace(text_[end])) ++end;
while (end < text_.size() && std::isspace(text_[end])) ++end;
auto new_text = text_;
new_text.erase(caret_, end - caret_);
if (set_text(std::move(new_text), false))
{
reset_caret();
}
}
else
{
if (caret_ < text_.size())
{
auto new_text = text_;
new_text.erase(new_text.begin() + caret_);
set_text(std::move(new_text), false);
reset_caret();
}
}
}
return true;
}
return false;
}
bool edit::on_event(text_input const & e)
{
if (editing_)
{
auto new_text = text_;
auto range = util::utf8_range(e.text);
new_text.insert(new_text.begin() + caret_, range.begin(), range.end());
if (set_text(std::move(new_text), false))
caret_ += range.size();
reset_caret();
return true;
}
return false;
}
bool edit::focused() const
{
return editing_;
}
bool edit::editing() const
{
return editing_;
}
void edit::reshape(geom::box<float, 2> const & bbox)
{
shape_.box = bbox;
text_box_ = bbox;
}
geom::box<float, 2> edit::size_constraints() const
{
// TODO: min x-size
static float const inf = std::numeric_limits<float>::infinity();
auto st = merged_own_style();
return {{{0.f, inf}, {1.f * st->font->size()[1] * (*st->scale), inf}}};
}
void edit::update(float dt)
{
if (editing_)
{
caret_blink_timer_ += dt;
if (caret_blink_timer_ >= caret_blink_period_)
{
caret_blink_timer_ -= caret_blink_period_;
caret_visible_ = !caret_visible_;
}
}
}
void edit::draw(painter & p) const
{
auto st = merged_own_style();
if (!cached_state_)
{
auto font = (font_ == font_type::bold) ? st->bold_font.get() : st->font.get();
cached_state state;
state.texture = &font->atlas();
shape_options options;
options.scale = *st->text_scale;
auto glyphs = font->shape(text_, options);
geom::box<float, 2> bbox;
for (auto const & g : glyphs)
bbox |= g.position;
state.size[0] = glyphs.empty() ? 0.f : bbox[0].length();
state.size[1] = font->size()[1] * (*st->text_scale);
for (auto const & g : glyphs)
{
auto tc = font->texcoords(g.character);
if (!tc) continue;
state.images.push_back({*tc, g.position});
}
cached_state_ = std::move(state);
}
geom::vector<float, 2> offset{0.f, 0.f};
switch (halign_)
{
case halignment::left:
offset[0] = text_box_[0].min;
break;
case halignment::center:
offset[0] = text_box_[0].center() - cached_state_->size[0] * 0.5f;
break;
case halignment::right:
offset[0] = text_box_[0].max - cached_state_->size[0];
break;
}
switch (valign_)
{
case valignment::top:
offset[1] = text_box_[1].min;
break;
case valignment::center:
offset[1] = text_box_[1].center() - cached_state_->size[1] * 0.5f;
break;
case valignment::bottom:
offset[1] = text_box_[1].max - cached_state_->size[1];
break;
}
offset[0] = std::round(offset[0]);
offset[1] = std::round(offset[1]);
p.begin_stencil();
p.draw_rect(text_box_, {0, 0, 0, 255});
p.commit_stencil();
if (*st->text_shadow_offset != geom::vector{0, 0} && (*st->shadow_color)[3] != 0)
{
auto shoffset = offset + geom::cast<float>(*st->text_shadow_offset);
for (auto const & i : cached_state_->images)
p.draw_image(i.position + shoffset, {cached_state_->texture, i.texcoords}, {*st->shadow_color, painter::color_mode::multiply});
}
for (auto const & i : cached_state_->images)
p.draw_image(i.position + offset, {cached_state_->texture, i.texcoords}, {*st->text_color, painter::color_mode::multiply});
p.end_stencil();
if (caret_visible_)
{
float x;
if (cached_state_->images.empty())
x = 0.f;
else if (caret_ == 0)
x = cached_state_->images.front().position[0].min;
else if (caret_ >= cached_state_->images.size())
x = cached_state_->images.back().position[0].max;
else
x = (cached_state_->images[caret_ - 1].position[0].max + cached_state_->images[caret_].position[0].min) / 2.f;
x += offset[0];
float y = offset[1];
p.draw_rect({{{x - (*st->text_scale) * 0.5f, x + (*st->text_scale) * 0.5f}, {y, y + cached_state_->size[1]}}}, *st->text_color);
}
}
void edit::set_text_shape(geom::box<float, 2> const & box)
{
text_box_ = box;
}
void edit::on_state_changed()
{
cached_state_ = std::nullopt;
post_reshape();
reset_caret();
}
void edit::reset_caret()
{
caret_visible_ = editing_;
caret_blink_timer_ = 0.f;
}
void edit::post_text_entered() const
{
if (on_text_entered_)
{
post([weak_self = weak_from_this(), cb = on_text_entered_]{
if (auto self = weak_self.lock())
cb(static_cast<edit const *>(self.get())->text_);
});
}
}
void edit::post_start_input() const
{
if (on_start_input_)
post(on_start_input_);
}
void edit::post_text_changed() const
{
if (on_text_changed_)
{
post([weak_self = weak_from_this(), cb = on_text_changed_]{
if (auto self = weak_self.lock())
cb(static_cast<edit const *>(self.get())->text_);
});
}
}
}