Add ui::edit text input element
This commit is contained in:
parent
7848d5a8b3
commit
f7568535b6
4 changed files with 479 additions and 0 deletions
115
libs/ui/include/psemek/ui/edit.hpp
Normal file
115
libs/ui/include/psemek/ui/edit.hpp
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/ui/element.hpp>
|
||||
#include <psemek/ui/box_shape.hpp>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace psemek::ui
|
||||
{
|
||||
|
||||
struct edit
|
||||
: element
|
||||
{
|
||||
enum font_type
|
||||
{
|
||||
normal,
|
||||
bold,
|
||||
};
|
||||
|
||||
enum class halignment
|
||||
{
|
||||
left,
|
||||
center,
|
||||
right,
|
||||
};
|
||||
|
||||
enum class valignment
|
||||
{
|
||||
top,
|
||||
center,
|
||||
bottom,
|
||||
};
|
||||
|
||||
virtual bool set_text(std::string_view text, bool signal = true);
|
||||
virtual bool set_text(std::u32string text, bool signal = true);
|
||||
virtual std::u32string_view text() const { return text_; }
|
||||
|
||||
virtual void set_font(font_type f);
|
||||
virtual font_type font() const { return font_; }
|
||||
|
||||
virtual void set_halign(halignment value);
|
||||
virtual halignment halign() const { return halign_; }
|
||||
|
||||
virtual void set_valign(valignment value);
|
||||
virtual valignment valign() const { return valign_; }
|
||||
|
||||
using validator_type = std::function<bool(std::u32string_view)>;
|
||||
virtual bool set_validator(validator_type validator);
|
||||
static validator_type numeric_nonnegative();
|
||||
static validator_type numeric();
|
||||
|
||||
using callback_type = std::function<void(std::u32string_view)>;
|
||||
virtual void on_text_entered(callback_type callback, bool signal = true);
|
||||
|
||||
bool on_event(mouse_move const & e) override;
|
||||
bool on_event(mouse_click const & e) override;
|
||||
bool on_event(key_press const & e) override;
|
||||
bool on_event(text_input const & e) override;
|
||||
|
||||
struct shape const & shape() const override { return shape_; }
|
||||
void reshape(geom::box<float, 2> const & bbox) override;
|
||||
|
||||
geom::box<float, 2> size_constraints() const override;
|
||||
|
||||
void own_style_updated() override;
|
||||
|
||||
void update(float dt) override;
|
||||
|
||||
void draw(painter & p) const override;
|
||||
|
||||
protected:
|
||||
|
||||
enum class state_t
|
||||
{
|
||||
normal,
|
||||
mouseover,
|
||||
editing,
|
||||
};
|
||||
|
||||
state_t state() const { return state_; }
|
||||
|
||||
private:
|
||||
std::u32string text_;
|
||||
font_type font_;
|
||||
halignment halign_ = halignment::left;
|
||||
valignment valign_ = valignment::top;
|
||||
|
||||
std::size_t caret_ = 0;
|
||||
float caret_blink_period_ = 0.5f;
|
||||
float caret_blink_timer_ = 0.f;
|
||||
bool caret_visible_ = true;
|
||||
|
||||
box_shape shape_;
|
||||
|
||||
state_t state_ = state_t::normal;
|
||||
|
||||
validator_type validator_;
|
||||
|
||||
callback_type on_text_entered_;
|
||||
|
||||
struct cached_state
|
||||
{
|
||||
struct font const * font = nullptr;
|
||||
std::vector<glyph> glyphs;
|
||||
geom::vector<float, 2> size{0.f, 0.f};
|
||||
};
|
||||
|
||||
mutable std::optional<cached_state> cached_state_;
|
||||
|
||||
void on_state_changed();
|
||||
void reset_caret();
|
||||
void post_text_entered() const;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ namespace psemek::ui
|
|||
struct progress_bar;
|
||||
struct selector;
|
||||
struct table;
|
||||
struct edit;
|
||||
|
||||
struct element_factory
|
||||
{
|
||||
|
|
@ -54,6 +55,7 @@ namespace psemek::ui
|
|||
virtual std::shared_ptr<progress_bar> make_progress_bar();
|
||||
virtual std::shared_ptr<selector> make_selector();
|
||||
virtual std::shared_ptr<table> make_table();
|
||||
virtual std::shared_ptr<edit> make_edit();
|
||||
|
||||
virtual ~element_factory() {}
|
||||
};
|
||||
|
|
|
|||
360
libs/ui/source/edit.cpp
Normal file
360
libs/ui/source/edit.cpp
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
#include <psemek/ui/edit.hpp>
|
||||
|
||||
#include <psemek/util/to_string.hpp>
|
||||
#include <psemek/util/unicode.hpp>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
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();
|
||||
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()
|
||||
{
|
||||
return [](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, [](char c){ return '0' <= c && c <= '9'; });
|
||||
};
|
||||
}
|
||||
|
||||
edit::validator_type edit::numeric_nonnegative()
|
||||
{
|
||||
return [](std::u32string_view const str) -> bool {
|
||||
return std::all_of(str.begin(), str.end(), [](char c){ return '0' <= c && c <= '9'; });
|
||||
};
|
||||
}
|
||||
|
||||
void edit::on_text_entered(callback_type callback, bool signal)
|
||||
{
|
||||
on_text_entered_ = std::move(callback);
|
||||
if (signal)
|
||||
post_text_entered();
|
||||
}
|
||||
|
||||
bool edit::on_event(mouse_move const & e)
|
||||
{
|
||||
bool const mouseover = shape_.contains(geom::cast<float>(e.position));
|
||||
|
||||
switch (state_)
|
||||
{
|
||||
case state_t::normal:
|
||||
if (mouseover)
|
||||
state_ = state_t::mouseover;
|
||||
break;
|
||||
case state_t::mouseover:
|
||||
if (!mouseover)
|
||||
state_ = state_t::normal;
|
||||
break;
|
||||
case state_t::editing:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool edit::on_event(mouse_click const & e)
|
||||
{
|
||||
if (e.button == mouse_button::left && e.down)
|
||||
{
|
||||
switch (state_)
|
||||
{
|
||||
case state_t::normal:
|
||||
return false;
|
||||
case state_t::mouseover:
|
||||
state_ = state_t::editing;
|
||||
if (!in_text_input())
|
||||
start_text_input();
|
||||
reset_caret();
|
||||
return true;
|
||||
case state_t::editing:
|
||||
state_ = state_t::normal;
|
||||
if (in_text_input())
|
||||
stop_text_input();
|
||||
reset_caret();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool edit::on_event(key_press const & e)
|
||||
{
|
||||
if (e.down && state_ == state_t::editing)
|
||||
{
|
||||
if (e.key == SDLK_LEFT)
|
||||
{
|
||||
if (caret_ > 0)
|
||||
{
|
||||
--caret_;
|
||||
reset_caret();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (e.key == SDLK_RIGHT)
|
||||
{
|
||||
if (caret_ < text_.size())
|
||||
{
|
||||
++caret_;
|
||||
reset_caret();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (e.key == SDLK_HOME)
|
||||
{
|
||||
caret_ = 0;
|
||||
return true;
|
||||
}
|
||||
else if (e.key == SDLK_END)
|
||||
{
|
||||
caret_ = text_.size();
|
||||
return true;
|
||||
}
|
||||
else if (e.key == SDLK_RETURN || e.key == SDLK_ESCAPE)
|
||||
{
|
||||
state_ = state_t::normal;
|
||||
if (in_text_input())
|
||||
stop_text_input();
|
||||
reset_caret();
|
||||
post_text_entered();
|
||||
return true;
|
||||
}
|
||||
else if (e.key == SDLK_BACKSPACE)
|
||||
{
|
||||
if (caret_ > 0)
|
||||
{
|
||||
--caret_;
|
||||
auto new_text = text_;
|
||||
new_text.erase(new_text.begin() + caret_);
|
||||
if (!set_text(std::move(new_text), false))
|
||||
++caret_;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (e.key == SDLK_DELETE)
|
||||
{
|
||||
if (caret_ < text_.size())
|
||||
{
|
||||
auto new_text = text_;
|
||||
new_text.erase(new_text.begin() + caret_);
|
||||
set_text(std::move(new_text), false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool edit::on_event(text_input const & e)
|
||||
{
|
||||
if (state_ == state_t::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();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void edit::reshape(geom::box<float, 2> const & bbox)
|
||||
{
|
||||
shape_.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::own_style_updated()
|
||||
{
|
||||
element::own_style_updated();
|
||||
on_state_changed();
|
||||
}
|
||||
|
||||
void edit::update(float dt)
|
||||
{
|
||||
if (state_ == state_t::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_)
|
||||
{
|
||||
cached_state state;
|
||||
state.font = (font_ == font_type::bold) ? st->bold_font.get() : st->font.get();
|
||||
|
||||
shape_options options;
|
||||
options.scale = *st->text_scale;
|
||||
state.glyphs = state.font->shape(text_, options);
|
||||
|
||||
geom::box<float, 2> bbox;
|
||||
for (auto const & g : state.glyphs)
|
||||
bbox |= g.position;
|
||||
|
||||
state.size[0] = state.glyphs.empty() ? 0.f : bbox[0].length();
|
||||
state.size[1] = state.font->size()[1] * (*st->text_scale);
|
||||
|
||||
cached_state_ = std::move(state);
|
||||
}
|
||||
|
||||
geom::vector<float, 2> offset{0.f, 0.f};
|
||||
|
||||
switch (halign_)
|
||||
{
|
||||
case halignment::left:
|
||||
offset[0] = shape_.box[0].min;
|
||||
break;
|
||||
case halignment::center:
|
||||
offset[0] = shape_.box[0].center() - cached_state_->size[0] * 0.5f;
|
||||
break;
|
||||
case halignment::right:
|
||||
offset[0] = shape_.box[0].max - cached_state_->size[0];
|
||||
break;
|
||||
}
|
||||
|
||||
switch (valign_)
|
||||
{
|
||||
case valignment::top:
|
||||
offset[1] = shape_.box[1].min;
|
||||
break;
|
||||
case valignment::center:
|
||||
offset[1] = shape_.box[1].center() - cached_state_->size[1] * 0.5f;
|
||||
break;
|
||||
case valignment::bottom:
|
||||
offset[1] = shape_.box[1].max - cached_state_->size[1];
|
||||
break;
|
||||
}
|
||||
|
||||
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 & g : cached_state_->glyphs)
|
||||
p.draw_glyph(*cached_state_->font, g.character, g.position + shoffset, *st->shadow_color);
|
||||
}
|
||||
|
||||
for (auto const & g : cached_state_->glyphs)
|
||||
p.draw_glyph(*cached_state_->font, g.character, g.position + offset, *st->text_color);
|
||||
|
||||
if (caret_visible_)
|
||||
{
|
||||
float x;
|
||||
if (cached_state_->glyphs.empty())
|
||||
x = 0.f;
|
||||
else if (caret_ == 0)
|
||||
x = cached_state_->glyphs.front().position[0].min;
|
||||
else if (caret_ >= cached_state_->glyphs.size())
|
||||
x = cached_state_->glyphs.back().position[0].max;
|
||||
else
|
||||
x = (cached_state_->glyphs[caret_ - 1].position[0].max + cached_state_->glyphs[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::on_state_changed()
|
||||
{
|
||||
cached_state_ = std::nullopt;
|
||||
post_reshape();
|
||||
reset_caret();
|
||||
}
|
||||
|
||||
void edit::reset_caret()
|
||||
{
|
||||
caret_visible_ = (state_ == state_t::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(dynamic_cast<edit const *>(self.get())->text_);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -133,4 +133,6 @@ namespace psemek::ui
|
|||
|
||||
std::shared_ptr<table> element_factory::make_table() { return nullptr; }
|
||||
|
||||
std::shared_ptr<edit> element_factory::make_edit() { return nullptr; }
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue