Entry: use text32 (utf32) instead of text (utf8). This simplifies text editing and other features such as text masking (password)

This commit is contained in:
dec05eba
2025-08-07 00:13:59 +02:00
parent ff00be30df
commit 67a8040e57
4 changed files with 82 additions and 101 deletions

4
TODO
View File

@@ -26,6 +26,10 @@ Have different modes. Overlay, window and side menu. Overlay can be used on x11,
Show navigation breadcrumbs for settings and deeper navigation (such as selecting a directory to save videos).
Add option to hide stream key like a password input.
This could be implemented in Entry by having a second mgl::Text field that gets updated in set_text and that is displayed instead when the entry is masked.
For caret drawing overflow check in ::draw use that text field is the entry is masked and use that text field in caret navigation.
If masking is disabled then switch to caret index in the real text by using the same utf8_index (since the text is the same length)
but recalculate byte_index from utf8_index with utf8_index_to_byte_index.
Add global setting. In that setting there should be an option to enable/disable gsr-ui from system startup (the systemd service).

View File

@@ -4,7 +4,7 @@
#include <functional>
#include <mglpp/graphics/Color.hpp>
#include <mglpp/graphics/Text.hpp>
#include <mglpp/graphics/Text32.hpp>
#include <mglpp/graphics/Rectangle.hpp>
namespace gsr {
@@ -15,7 +15,12 @@ namespace gsr {
ALLOW,
REPLACED
};
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::string &str)>;
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::u32string &str)>;
struct CaretIndexPos {
int index;
mgl::vec2f pos;
};
class Entry : public Widget {
public:
@@ -33,11 +38,8 @@ namespace gsr {
mgl::vec2f get_size() override;
EntryValidateHandlerResult set_text(std::string str);
const std::string& get_text() const;
// Also updates the cursor position
void replace_text(size_t index, size_t size, const std::string &replacement);
EntryValidateHandlerResult set_text(const std::string &str);
std::string get_text() const;
// Return false to specify that the string should not be accepted. This reverts the string back to its previous value.
// The input can be changed by changing the input parameter and returning true.
@@ -45,20 +47,21 @@ namespace gsr {
std::function<void(const std::string &text)> on_changed;
private:
// Also updates the cursor position
void replace_text(size_t index, size_t size, const std::u32string &replacement);
void move_caret_word(Direction direction, size_t max_codepoints);
EntryValidateHandlerResult set_text_internal(std::string str);
EntryValidateHandlerResult set_text_internal(std::u32string str);
void draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
void draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
mgl_index_codepoint_pair find_closest_caret_index_by_position(mgl::vec2f position);
CaretIndexPos find_closest_caret_index_by_position(mgl::vec2f position);
private:
struct Caret {
float offset_x = 0.0f;
int utf8_index = 0;
int byte_index = 0;
int index = 0;
};
mgl::Rectangle background;
mgl::Text text;
mgl::Text32 text;
float max_width;
bool selected = false;
bool selecting_text = false;

View File

@@ -23,7 +23,7 @@ namespace gsr {
}
}
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text("", *font), max_width(max_width) {
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text(std::u32string(), *font), max_width(max_width) {
this->text.set_color(get_color_theme().text_color);
set_text(text);
}
@@ -39,8 +39,7 @@ namespace gsr {
selecting_text = true;
const auto caret_index_mouse = find_closest_caret_index_by_position(mouse_pos);
caret.byte_index = caret_index_mouse.byte_index;
caret.utf8_index = caret_index_mouse.utf8_index;
caret.index = caret_index_mouse.index;
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
selection_start_caret = caret;
show_selection = true;
@@ -51,54 +50,48 @@ namespace gsr {
}
} else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) {
selecting_text = false;
if(caret.byte_index == selection_start_caret.byte_index)
if(caret.index == selection_start_caret.index)
show_selection = false;
} else if(event.type == mgl::Event::MouseMoved && selected) {
if(selecting_text) {
const auto caret_index_mouse = find_closest_caret_index_by_position(mgl::vec2f(event.mouse_move.x, event.mouse_move.y));
caret.byte_index = caret_index_mouse.byte_index;
caret.utf8_index = caret_index_mouse.utf8_index;
caret.index = caret_index_mouse.index;
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
return false;
}
} else if(event.type == mgl::Event::KeyPressed && selected) {
int selection_start_byte = caret.byte_index;
int selection_end_byte = caret.byte_index;
int selection_start_byte = caret.index;
int selection_end_byte = caret.index;
if(show_selection) {
selection_start_byte = std::min(caret.byte_index, selection_start_caret.byte_index);
selection_end_byte = std::max(caret.byte_index, selection_start_caret.byte_index);
selection_start_byte = std::min(caret.index, selection_start_caret.index);
selection_end_byte = std::max(caret.index, selection_start_caret.index);
}
if(event.key.code == mgl::Keyboard::Backspace) {
if(selection_start_byte == selection_end_byte && caret.byte_index > 0)
selection_start_byte = mgl::utf8_get_start_of_codepoint((const unsigned char*)text.get_string().c_str(), text.get_string().size(), caret.byte_index - 1);
if(selection_start_byte == selection_end_byte && caret.index > 0)
selection_start_byte -= 1;
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, "");
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
} else if(event.key.code == mgl::Keyboard::Delete) {
if(selection_start_byte == selection_end_byte && caret.byte_index < (int)text.get_string().size()) {
size_t codepoint_length = 1;
mgl::utf8_get_codepoint_length(((const unsigned char*)text.get_string().c_str())[caret.byte_index], &codepoint_length);
selection_end_byte = selection_start_byte + codepoint_length;
}
if(selection_start_byte == selection_end_byte && caret.index < (int)text.get_string().size())
selection_end_byte += 1;
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, "");
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
} else if(event.key.code == mgl::Keyboard::C && event.key.control) {
const size_t selection_num_bytes = selection_end_byte - selection_start_byte;
if(selection_num_bytes > 0)
window.set_clipboard(text.get_string().substr(selection_start_byte, selection_num_bytes));
window.set_clipboard(mgl::utf32_to_utf8(text.get_string().substr(selection_start_byte, selection_num_bytes)));
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
std::string clipboard_string = window.get_clipboard_string();
string_replace_all(clipboard_string, '\n', ' ');
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::move(clipboard_string));
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32(clipboard_string));
} else if(event.key.code == mgl::Keyboard::A && event.key.control) {
selection_start_caret.byte_index = 0;
selection_start_caret.utf8_index = 0;
selection_start_caret.index = 0;
selection_start_caret.offset_x = 0.0f;
caret.byte_index = text.get_string().size();
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
caret.index = text.get_string().size();
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = text.find_character_pos(caret.index).x - this->text.get_position().x;
show_selection = true;
} else if(event.key.code == mgl::Keyboard::Left) {
@@ -122,8 +115,7 @@ namespace gsr {
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::Home) {
caret.byte_index = 0;
caret.utf8_index = 0;
caret.index = 0;
caret.offset_x = 0.0f;
if(!selecting_with_keyboard) {
@@ -131,10 +123,9 @@ namespace gsr {
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::End) {
caret.byte_index = text.get_string().size();
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
caret.index = text.get_string().size();
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = text.find_character_pos(caret.index).x - this->text.get_position().x;
if(!selecting_with_keyboard) {
selection_start_caret = caret;
@@ -155,14 +146,14 @@ namespace gsr {
return false;
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32 && event.text.codepoint != 127) {
int selection_start_byte = caret.byte_index;
int selection_end_byte = caret.byte_index;
int selection_start_byte = caret.index;
int selection_end_byte = caret.index;
if(show_selection) {
selection_start_byte = std::min(caret.byte_index, selection_start_caret.byte_index);
selection_end_byte = std::max(caret.byte_index, selection_start_caret.byte_index);
selection_start_byte = std::min(caret.index, selection_start_caret.index);
selection_end_byte = std::max(caret.index, selection_start_caret.index);
}
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::string(event.text.str, event.text.size));
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32((const unsigned char*)event.text.str, event.text.size));
return false;
}
@@ -245,13 +236,13 @@ namespace gsr {
}
void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
if(selection_start_caret.byte_index == caret.byte_index)
if(selection_start_caret.index == caret.index)
return;
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
const int offset = caret.byte_index < selection_start_caret.byte_index ? caret_width : 0;
const int offset = caret.index < selection_start_caret.index ? caret_width : 0;
mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x) - offset, caret_size.y).floor());
caret_selection_rect.set_position((draw_pos + mgl::vec2f(padding_left + std::min(caret.offset_x, selection_start_caret.offset_x) - text_overflow + offset, padding_top)).floor());
@@ -274,45 +265,37 @@ namespace gsr {
const int dir_step = direction == Direction::LEFT ? -1 : 1;
const int num_delimiter_chars = 7;
const char delimiter_chars[num_delimiter_chars + 1] = " \t\n/.,;";
const unsigned char *text_str = (const unsigned char*)text.get_string().data();
const char32_t *text_str = text.get_string().data();
int num_non_delimiter_chars_found = 0;
for(size_t i = 0; i < max_codepoints; ++i) {
const int caret_byte_index_before = caret.byte_index;
caret.byte_index = mgl::utf8_index_to_byte_index(text_str, text.get_string().size(), std::max(0, caret.utf8_index + dir_step));
if(caret.byte_index == caret_byte_index_before)
break;
const uint32_t codepoint = text_str[caret.index];
const size_t codepoint_start = std::min(caret_byte_index_before, caret.byte_index);
const size_t codepoint_end = std::max(caret_byte_index_before, caret.byte_index);
uint32_t decoded_codepoint = ' ';
size_t codepoint_length = 1;
mgl::utf8_decode(text_str + codepoint_start, codepoint_end - codepoint_start, &decoded_codepoint, &codepoint_length);
const bool is_delimiter_char = !!memchr(delimiter_chars, decoded_codepoint, num_delimiter_chars);
const bool is_delimiter_char = codepoint < 127 && !!memchr(delimiter_chars, codepoint, num_delimiter_chars);
if(is_delimiter_char) {
if(num_non_delimiter_chars_found > 0) {
caret.byte_index = caret_byte_index_before;
if(num_non_delimiter_chars_found > 0)
break;
}
} else {
++num_non_delimiter_chars_found;
}
caret.utf8_index += dir_step;
if(caret.index + dir_step < 0 || caret.index + dir_step > (int)text.get_string().size())
break;
caret.index += dir_step;
}
// TODO: Move right by one character instead of calculating every character to caret index
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = text.find_character_pos(caret.index).x - this->text.get_position().x;
}
EntryValidateHandlerResult Entry::set_text(std::string str) {
EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
EntryValidateHandlerResult Entry::set_text(const std::string &str) {
EntryValidateHandlerResult validate_result = set_text_internal(mgl::utf8_to_utf32(str));
if(validate_result == EntryValidateHandlerResult::ALLOW) {
caret.byte_index = text.get_string().size();
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
caret.index = text.get_string().size();
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = text.find_character_pos(caret.index).x - this->text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
@@ -322,40 +305,37 @@ namespace gsr {
return validate_result;
}
EntryValidateHandlerResult Entry::set_text_internal(std::string str) {
EntryValidateHandlerResult Entry::set_text_internal(std::u32string str) {
EntryValidateHandlerResult validate_result = EntryValidateHandlerResult::ALLOW;
if(validate_handler)
validate_result = validate_handler(*this, str);
if(validate_result == EntryValidateHandlerResult::ALLOW) {
text.set_string(std::move(str));
// TODO: Call callback with utf32 instead?
if(on_changed)
on_changed(text.get_string());
on_changed(mgl::utf32_to_utf8(text.get_string()));
}
return validate_result;
}
const std::string& Entry::get_text() const {
return text.get_string();
std::string Entry::get_text() const {
return mgl::utf32_to_utf8(text.get_string());
}
void Entry::replace_text(size_t index, size_t size, const std::string &replacement) {
void Entry::replace_text(size_t index, size_t size, const std::u32string &replacement) {
if(index + size > text.get_string().size())
return;
const auto prev_caret = caret;
if((int)index >= caret.byte_index) {
caret.utf8_index += mgl::utf8_get_character_count((const unsigned char*)replacement.c_str(), replacement.size());
caret.byte_index += replacement.size();
} else {
caret.utf8_index -= mgl::utf8_get_character_count((const unsigned char*)(text.get_string().c_str() + caret.byte_index - size), size);
caret.utf8_index += mgl::utf8_get_character_count((const unsigned char*)replacement.c_str(), replacement.size());
caret.byte_index = caret.byte_index - size + replacement.size();
}
if((int)index >= caret.index)
caret.index += replacement.size();
else
caret.index = caret.index - size + replacement.size();
std::string str = text.get_string();
std::u32string str = text.get_string();
str.replace(index, size, replacement);
const EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
if(validate_result == EntryValidateHandlerResult::DENY) {
@@ -366,7 +346,7 @@ namespace gsr {
}
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = text.find_character_pos(caret.index).x - this->text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
@@ -374,17 +354,13 @@ namespace gsr {
show_selection = false;
}
mgl_index_codepoint_pair Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
const std::string &str = text.get_string();
CaretIndexPos Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
const std::u32string &str = text.get_string();
mgl::Font *font = text.get_font();
mgl_index_codepoint_pair result = {0, 0, {text.get_position().x, text.get_position().y}};
for(; result.byte_index < str.size();) {
uint32_t codepoint = ' ';
size_t clen = 1;
if(!mgl::utf8_decode((const unsigned char*)&str[result.byte_index], str.size() - result.byte_index, &codepoint, &clen))
clen = 1;
CaretIndexPos result = {0, {text.get_position().x, text.get_position().y}};
for(result.index = 0; result.index < (int)str.size(); ++result.index) {
const uint32_t codepoint = str[result.index];
float glyph_width = 0.0f;
if(codepoint == '\t') {
@@ -400,8 +376,6 @@ namespace gsr {
break;
result.pos.x += glyph_width;
result.byte_index += clen;
result.utf8_index += 1;
}
return result;
@@ -411,7 +385,7 @@ namespace gsr {
return c >= '0' && c <= '9';
}
static std::optional<int> to_integer(const std::string &str) {
static std::optional<int> to_integer(const std::u32string &str) {
if(str.empty())
return std::nullopt;
@@ -438,7 +412,7 @@ namespace gsr {
}
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) {
return [min, max](Entry &entry, const std::string &str) {
return [min, max](Entry &entry, const std::u32string &str) {
if(str.empty())
return EntryValidateHandlerResult::ALLOW;