Improve entry with cutting off text, vertical scroll, text selection, caret movement, copy, etc

This commit is contained in:
dec05eba
2025-08-06 02:03:48 +02:00
parent 8ed1fe4799
commit a9e118ea8f
6 changed files with 369 additions and 42 deletions

1
TODO
View File

@@ -87,6 +87,7 @@ Dont put widget position to int position when scrolling. This makes the UI jitte
Show warning if another instance of gpu screen recorder is already running when starting recording?
Keyboard leds get turned off when stopping gsr-global-hotkeys (for example numlock). The numlock key has to be pressed twice again to make it look correct to match its state.
Fix this by writing 0 or 1 to /sys/class/leds/input2::numlock/brightness.
Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change ExecStart to do flatpak run ... || gsr-ui, but make it run as a shell command first with /bin/sh -c "".

View File

@@ -5,9 +5,17 @@
#include <mglpp/graphics/Color.hpp>
#include <mglpp/graphics/Text.hpp>
#include <mglpp/graphics/Rectangle.hpp>
namespace gsr {
using EntryValidateHandler = std::function<bool(std::string &str)>;
class Entry;
enum class EntryValidateHandlerResult {
DENY,
ALLOW,
REPLACED
};
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::string &str)>;
class Entry : public Widget {
public:
@@ -20,19 +28,39 @@ namespace gsr {
mgl::vec2f get_size() override;
void set_text(std::string str);
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);
// 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.
EntryValidateHandler validate_handler;
std::function<void(const std::string &text)> on_changed;
private:
EntryValidateHandlerResult set_text_internal(std::string 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);
private:
struct Caret {
float offset_x = 0.0f;
int utf8_index = 0;
int byte_index = 0;
};
mgl::Rectangle background;
mgl::Text text;
float max_width;
bool selected = false;
float caret_offset_x = 0.0f;
bool selecting_text = false;
bool selecting_with_keyboard = false;
bool show_selection = false;
Caret caret;
Caret selection_start_caret;
float text_overflow = 0.0f;
};
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max);

View File

@@ -2,6 +2,7 @@
#include <mglpp/system/vec.hpp>
#include <mglpp/graphics/Color.hpp>
#include <mglpp/window/Window.hpp>
namespace mgl {
class Window;
@@ -14,4 +15,5 @@ namespace gsr {
void set_frame_delta_seconds(double frame_delta);
mgl::vec2f scale_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to);
mgl::vec2f clamp_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to);
mgl::Scissor scissor_get_sub_area(mgl::Scissor parent, mgl::Scissor child);
}

View File

@@ -1,7 +1,6 @@
#include "../../include/gui/Entry.hpp"
#include "../../include/gui/Utils.hpp"
#include "../../include/Theme.hpp"
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
#include <mglpp/system/FloatRect.hpp>
@@ -16,6 +15,13 @@ namespace gsr {
static const float border_scale = 0.0015f;
static const float caret_width_scale = 0.001f;
static void string_replace_all(std::string &str, char old_char, char new_char) {
for(char &c : str) {
if(c == old_char)
c = new_char;
}
}
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text("", *font), max_width(max_width) {
this->text.set_color(get_color_theme().text_color);
set_text(text);
@@ -26,24 +32,137 @@ namespace gsr {
return true;
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
selected = mgl::FloatRect(position + offset, get_size()).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });
} else if(event.type == mgl::Event::KeyPressed && selected) {
if(event.key.code == mgl::Keyboard::Backspace && !text.get_string().empty()) {
std::string str = text.get_string();
const size_t prev_index = mgl::utf8_get_start_of_codepoint((const unsigned char*)str.c_str(), str.size(), str.size());
str.erase(prev_index, std::string::npos);
set_text(std::move(str));
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
std::string clipboard_text = window.get_clipboard_string();
std::string str = text.get_string();
str += clipboard_text;
set_text(std::move(str));
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
selected = mgl::FloatRect(position + offset, get_size()).contains(mouse_pos);
if(selected) {
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.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
selection_start_caret = caret;
show_selection = true;
}
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32) {
std::string str = text.get_string();
str.append(event.text.str, event.text.size);
set_text(std::move(str));
} 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)
show_selection = false;
} else if(event.type == mgl::Event::KeyPressed && selected) {
int selection_start_byte = caret.byte_index;
int selection_end_byte = caret.byte_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);
}
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);
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, "");
} 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;
}
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, "");
} 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));
} 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));
} 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.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());
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
show_selection = true;
} else if(event.key.code == mgl::Keyboard::Left && caret.byte_index > 0) {
if(!selecting_with_keyboard && show_selection) {
show_selection = false;
} else {
caret.byte_index = mgl::utf8_get_start_of_codepoint((const unsigned char*)text.get_string().data(), text.get_string().size(), caret.byte_index - 1);
caret.utf8_index -= 1;
// TODO: Move left 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;
}
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::Right) {
if(!selecting_with_keyboard && show_selection) {
show_selection = false;
} else {
const int caret_byte_index_before = caret.byte_index;
caret.byte_index = mgl::utf8_index_to_byte_index((const unsigned char*)text.get_string().data(), text.get_string().size(), caret.utf8_index + 1);
if(caret.byte_index != caret_byte_index_before)
caret.utf8_index += 1;
// 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;
}
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::Home) {
caret.byte_index = 0;
caret.utf8_index = 0;
caret.offset_x = 0.0f;
if(!selecting_with_keyboard) {
selection_start_caret = caret;
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());
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::LShift || event.key.code == mgl::Keyboard::RShift) {
if(!show_selection)
selection_start_caret = caret;
selecting_with_keyboard = true;
show_selection = true;
}
return false;
} else if(event.type == mgl::Event::KeyReleased && selected) {
if(event.key.code == mgl::Keyboard::LShift || event.key.code == mgl::Keyboard::RShift) {
selecting_with_keyboard = false;
}
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;
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);
}
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::string(event.text.str, event.text.size));
return false;
}
return true;
}
@@ -54,26 +173,91 @@ namespace gsr {
const mgl::vec2f draw_pos = position + offset;
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
mgl::Rectangle background(get_size());
background.set_size(get_size());
background.set_position(draw_pos.floor());
background.set_color(selected ? mgl::Color(0, 0, 0, 255) : mgl::Color(0, 0, 0, 120));
window.draw(background);
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
const mgl::vec2f caret_size = mgl::vec2f(caret_width, text.get_bounds().size.y).floor();
const float overflow_left = (caret.offset_x + padding_left) - (padding_left + text_overflow);
if(overflow_left < 0.0f)
text_overflow += overflow_left;
const float overflow_right = (caret.offset_x + padding_left) - (background.get_size().x - padding_right);
if(overflow_right - text_overflow > 0.0f)
text_overflow = overflow_right;
text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
const auto text_bounds = text.get_bounds();
const bool text_larger_than_background = text_bounds.size.x > (background.get_size().x - padding_left - padding_right);
const float text_overflow_right = (text_bounds.position.x + text_bounds.size.x) - (background.get_position().x + background.get_size().x - padding_right);
if(text_larger_than_background) {
if(text_overflow_right < 0.0f) {
text_overflow += text_overflow_right;
text.set_position(text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
}
} else {
text.set_position(text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
text_overflow = 0.0f;
}
if(selected) {
const int border_size = std::max(1.0f, border_scale * get_theme().window_height);
draw_rectangle_outline(window, draw_pos.floor(), get_size().floor(), get_color_theme().tint_color, border_size);
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
mgl::Rectangle caret({(float)caret_width, text.get_bounds().size.y});
caret.set_position((draw_pos + mgl::vec2f(padding_left + caret_offset_x, padding_top)).floor());
caret.set_color(mgl::Color(255, 255, 255));
window.draw(caret);
draw_caret(window, draw_pos, caret_size);
}
text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - text.get_bounds().size.y * 0.5f)).floor());
const mgl::Scissor parent_scissor = window.get_scissor();
const mgl::Scissor scissor = scissor_get_sub_area(parent_scissor,
mgl::Scissor{
(background.get_position() + mgl::vec2f(padding_left, padding_top)).to_vec2i(),
(background.get_size() - mgl::vec2f(padding_left + padding_right, padding_top + padding_bottom)).to_vec2i()
});
window.set_scissor(scissor);
window.draw(text);
if(selecting_text) {
const auto caret_index_mouse = find_closest_caret_index_by_position(window.get_mouse_position().to_vec2f());
caret.byte_index = caret_index_mouse.byte_index;
caret.utf8_index = caret_index_mouse.utf8_index;
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
}
if(show_selection)
draw_caret_selection(window, draw_pos, caret_size);
window.set_scissor(parent_scissor);
}
void Entry::draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
mgl::Rectangle caret_rect(caret_size);
mgl::vec2f caret_draw_pos = draw_pos + mgl::vec2f(padding_left + caret.offset_x - text_overflow, padding_top);
caret_rect.set_position(caret_draw_pos.floor());
caret_rect.set_color(mgl::Color(255, 255, 255));
window.draw(caret_rect);
}
void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x), 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, padding_top)).floor());
mgl::Color caret_select_color = get_color_theme().tint_color;
caret_select_color.a = 100;
caret_selection_rect.set_color(caret_select_color);
window.draw(caret_selection_rect);
}
mgl::vec2f Entry::get_size() {
@@ -85,19 +269,107 @@ namespace gsr {
return { max_width, text.get_bounds().size.y + padding_top + padding_bottom };
}
void Entry::set_text(std::string str) {
if(!validate_handler || validate_handler(str)) {
EntryValidateHandlerResult Entry::set_text(std::string str) {
EntryValidateHandlerResult validate_result = set_text_internal(std::move(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());
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
selecting_with_keyboard = false;
show_selection = false;
}
return validate_result;
}
EntryValidateHandlerResult Entry::set_text_internal(std::string 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));
caret_offset_x = text.find_character_pos(99999).x - this->text.get_position().x;
if(on_changed)
on_changed(text.get_string());
}
return validate_result;
}
const std::string& Entry::get_text() const {
return text.get_string();
}
void Entry::replace_text(size_t index, size_t size, const std::string &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();
}
std::string str = text.get_string();
str.replace(index, size, replacement);
const EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
if(validate_result == EntryValidateHandlerResult::DENY) {
caret = prev_caret;
return;
} else if(validate_result == EntryValidateHandlerResult::REPLACED) {
return;
}
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
selecting_with_keyboard = false;
show_selection = false;
}
mgl_index_codepoint_pair Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
const std::string &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;
float glyph_width = 0.0f;
if(codepoint == '\t') {
const auto glyph = font->get_glyph(' ');
const int tab_width = 4;
glyph_width = glyph.advance * tab_width;
} else {
const auto glyph = font->get_glyph(codepoint);
glyph_width = glyph.advance;
}
if(result.pos.x + glyph_width * 0.5f >= position.x)
break;
result.pos.x += glyph_width;
result.byte_index += clen;
result.utf8_index += 1;
}
return result;
}
static bool is_number(uint8_t c) {
return c >= '0' && c <= '9';
}
@@ -114,7 +386,7 @@ namespace gsr {
int number = 0;
for(; i < str.size(); ++i) {
if(!is_number(str[i]))
return false;
return std::nullopt;
const int new_number = number * 10 + (str[i] - '0');
if(new_number < number)
@@ -129,19 +401,23 @@ namespace gsr {
}
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) {
return [min, max](std::string &str) {
return [min, max](Entry &entry, const std::string &str) {
if(str.empty())
return true;
return EntryValidateHandlerResult::ALLOW;
std::optional<int> number = to_integer(str);
const std::optional<int> number = to_integer(str);
if(!number)
return false;
return EntryValidateHandlerResult::DENY;
if(number.value() < min)
str = std::to_string(min);
else if(number.value() > max)
str = std::to_string(max);
return true;
if(number.value() < min) {
entry.set_text(std::to_string(min));
return EntryValidateHandlerResult::REPLACED;
} else if(number.value() > max) {
entry.set_text(std::to_string(max));
return EntryValidateHandlerResult::REPLACED;
}
return EntryValidateHandlerResult::ALLOW;
};
}
}

View File

@@ -5,6 +5,18 @@
namespace gsr {
static double frame_delta_seconds = 1.0;
static mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
return { std::min(a.x, b.x), std::min(a.y, b.y) };
}
static mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b) {
return { std::max(a.x, b.x), std::max(a.y, b.y) };
}
static mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
return min_vec2i(max, max_vec2i(value, min));
}
// TODO: Use vertices to make it one draw call
void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size) {
pos = pos.floor();
@@ -74,4 +86,12 @@ namespace gsr {
else
return from;
}
mgl::Scissor scissor_get_sub_area(mgl::Scissor parent, mgl::Scissor child) {
const mgl::vec2i pos = clamp_vec2i(child.position, parent.position, parent.position + parent.size);
return mgl::Scissor{
pos,
min_vec2i(child.size, parent.position + parent.size - pos)
};
}
}