diff --git a/libs/ui/include/psemek/ui/edit.hpp b/libs/ui/include/psemek/ui/edit.hpp new file mode 100644 index 00000000..cd10e35c --- /dev/null +++ b/libs/ui/include/psemek/ui/edit.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include +#include + +#include + +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; + virtual bool set_validator(validator_type validator); + static validator_type numeric_nonnegative(); + static validator_type numeric(); + + using callback_type = std::function; + 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 const & bbox) override; + + geom::box 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 glyphs; + geom::vector size{0.f, 0.f}; + }; + + mutable std::optional cached_state_; + + void on_state_changed(); + void reset_caret(); + void post_text_entered() const; + }; + +} diff --git a/libs/ui/include/psemek/ui/element_factory.hpp b/libs/ui/include/psemek/ui/element_factory.hpp index 1382c659..70193999 100644 --- a/libs/ui/include/psemek/ui/element_factory.hpp +++ b/libs/ui/include/psemek/ui/element_factory.hpp @@ -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 make_progress_bar(); virtual std::shared_ptr make_selector(); virtual std::shared_ptr make_table(); + virtual std::shared_ptr make_edit(); virtual ~element_factory() {} }; diff --git a/libs/ui/source/edit.cpp b/libs/ui/source/edit.cpp new file mode 100644 index 00000000..19024728 --- /dev/null +++ b/libs/ui/source/edit.cpp @@ -0,0 +1,360 @@ +#include + +#include +#include + +#include + +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(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 const & bbox) + { + shape_.box = bbox; + } + + geom::box edit::size_constraints() const + { + // TODO: min x-size + static float const inf = std::numeric_limits::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 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 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(*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(self.get())->text_); + }); + } + } + +} diff --git a/libs/ui/source/element_factory.cpp b/libs/ui/source/element_factory.cpp index 2ec00198..ec3b0a4f 100644 --- a/libs/ui/source/element_factory.cpp +++ b/libs/ui/source/element_factory.cpp @@ -133,4 +133,6 @@ namespace psemek::ui std::shared_ptr
element_factory::make_table() { return nullptr; } + std::shared_ptr element_factory::make_edit() { return nullptr; } + }