Compare commits

...

7 Commits
1.7.2 ... 1.7.3

Author SHA1 Message Date
dec05eba
2d3abace0e Fix build 2025-08-07 20:47:14 +02:00
dec05eba
47c02fc6c8 1.7.3 2025-08-07 20:22:20 +02:00
dec05eba
5f8c366b43 Show video codec error messages from gsr
let gsr choose video codec automatically for us when using auto (prefer h264, then hevc and then av1),
fallback to software encoding in gsr ui if none of them are available.
2025-08-07 20:21:22 +02:00
dec05eba
f4ed622510 Entry: add more delimiter for moving 2025-08-07 02:51:55 +02:00
dec05eba
f1ee19d014 Mask stream keys, add button to unmask it 2025-08-07 02:00:35 +02:00
dec05eba
67a8040e57 Entry: use text32 (utf32) instead of text (utf8). This simplifies text editing and other features such as text masking (password) 2025-08-07 00:13:59 +02:00
dec05eba
ff00be30df Entry: implement moving care by word with ctrl+arrow keys 2025-08-06 14:54:25 +02:00
15 changed files with 255 additions and 150 deletions

2
TODO
View File

@@ -25,8 +25,6 @@ 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.
Add global setting. In that setting there should be an option to enable/disable gsr-ui from system startup (the systemd service).
Add profiles and hotkey to switch between profiles (show notification when switching profile).

BIN
images/masked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

BIN
images/unmasked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -44,6 +44,8 @@ namespace gsr {
mgl::Texture save_texture;
mgl::Texture screenshot_texture;
mgl::Texture trash_texture;
mgl::Texture masked_texture;
mgl::Texture unmasked_texture;
mgl::Texture ps4_home_texture;
mgl::Texture ps4_options_texture;

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,10 +15,20 @@ 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:
enum class Direction {
LEFT,
RIGHT
};
Entry(mgl::Font *font, const char *text, float max_width);
Entry(const Entry&) = delete;
Entry& operator=(const Entry&) = delete;
@@ -28,11 +38,11 @@ namespace gsr {
mgl::vec2f get_size() override;
EntryValidateHandlerResult set_text(std::string str);
const std::string& get_text() const;
EntryValidateHandlerResult set_text(const std::string &str);
std::string get_text() const;
// Also updates the cursor position
void replace_text(size_t index, size_t size, const std::string &replacement);
void set_masked(bool masked);
bool is_masked() 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.
@@ -40,24 +50,28 @@ namespace gsr {
std::function<void(const std::string &text)> on_changed;
private:
EntryValidateHandlerResult set_text_internal(std::string str);
// 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::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;
mgl::Text32 masked_text;
float max_width;
bool selected = false;
bool selecting_text = false;
bool selecting_with_keyboard = false;
bool show_selection = false;
bool masked = false;
Caret caret;
Caret selection_start_caret;
float text_overflow = 0.0f;

View File

@@ -118,9 +118,11 @@ namespace gsr {
std::unique_ptr<ComboBox> create_streaming_service_box();
std::unique_ptr<List> create_streaming_service_section();
std::unique_ptr<List> create_stream_key_section();
std::unique_ptr<List> create_stream_custom_url();
std::unique_ptr<List> create_stream_custom_key();
std::unique_ptr<List> create_stream_custom_section();
std::unique_ptr<ComboBox> create_stream_container_box();
std::unique_ptr<List> create_stream_container_section();
std::unique_ptr<List> create_stream_container();
void add_stream_widgets();
void load_audio_tracks(const RecordOptions &record_options);
@@ -173,8 +175,7 @@ namespace gsr {
ComboBox *container_box_ptr = nullptr;
ComboBox *streaming_service_box_ptr = nullptr;
List *stream_key_list_ptr = nullptr;
List *stream_url_list_ptr = nullptr;
List *container_list_ptr = nullptr;
List *custom_stream_list_ptr = nullptr;
CheckBox *save_replay_in_game_folder_ptr = nullptr;
CheckBox *restart_replay_on_save = nullptr;
Label *estimated_file_size_ptr = nullptr;

View File

@@ -45,6 +45,8 @@ namespace gsr {
void set_visible(bool visible);
Widget* get_parent_widget();
void *userdata = nullptr;
protected:
void set_widget_as_selected_in_parent();

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.7.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.7.3', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -62,7 +62,7 @@ datadir = get_option('datadir')
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.5"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.6"', language: ['c', 'cpp'])
executable(
meson.project_name(),

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.7.2"
version = "1.7.3"
platforms = ["posix"]
[lang.cpp]

View File

@@ -1951,7 +1951,15 @@ namespace gsr {
void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
if(exit_code == 50) {
show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 51) {
show_notification("Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 52) {
show_notification("Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 53) {
show_notification("Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 54) {
show_notification("Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 60) {
show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else {
@@ -2468,7 +2476,7 @@ namespace gsr {
kill(gpu_screen_recorder_process, SIGRTMIN+5);
}
static const char* switch_video_codec_to_usable_hardware_encoder(const GsrInfo &gsr_info) {
static const char* get_first_usable_hardware_video_codec_name(const GsrInfo &gsr_info) {
if(gsr_info.supported_video_codecs.h264)
return "h264";
else if(gsr_info.supported_video_codecs.hevc)
@@ -2501,8 +2509,7 @@ namespace gsr {
*video_codec = "h264";
*encoder = "cpu";
} else if(strcmp(*video_codec, "auto") == 0) {
*video_codec = switch_video_codec_to_usable_hardware_encoder(gsr_info);
if(!*video_codec) {
if(!get_first_usable_hardware_video_codec_name(gsr_info)) {
*video_codec = "h264";
*encoder = "cpu";
}

View File

@@ -120,6 +120,12 @@ namespace gsr {
if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->masked_texture.load_from_file((resources_path + "images/masked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->unmasked_texture.load_from_file((resources_path + "images/unmasked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;

View File

@@ -6,6 +6,7 @@
#include <mglpp/system/FloatRect.hpp>
#include <mglpp/system/Utf8.hpp>
#include <optional>
#include <string.h>
namespace gsr {
static const float padding_top_scale = 0.004629f;
@@ -22,8 +23,13 @@ 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),
masked_text(std::u32string(), *font),
max_width(max_width)
{
this->text.set_color(get_color_theme().text_color);
this->masked_text.set_color(get_color_theme().text_color);
set_text(text);
}
@@ -31,6 +37,8 @@ namespace gsr {
if(!visible)
return true;
mgl::Text32 &active_text = masked ? masked_text : text;
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
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);
@@ -38,9 +46,8 @@ 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.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
caret.index = caret_index_mouse.index;
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
selection_start_caret = caret;
show_selection = true;
} else {
@@ -50,89 +57,72 @@ 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.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
caret.index = caret_index_mouse.index;
caret.offset_x = caret_index_mouse.pos.x - active_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)active_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 = active_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 = active_text.find_character_pos(caret.index).x - active_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) {
} else if(event.key.code == mgl::Keyboard::Left) {
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;
}
else
move_caret_word(Direction::LEFT, event.key.control ? 999999 : 1);
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) {
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;
}
else
move_caret_word(Direction::RIGHT, event.key.control ? 999999 : 1);
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.index = 0;
caret.offset_x = 0.0f;
if(!selecting_with_keyboard) {
@@ -140,10 +130,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 = active_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 = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
if(!selecting_with_keyboard) {
selection_start_caret = caret;
@@ -164,14 +153,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;
}
@@ -189,13 +178,15 @@ namespace gsr {
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
mgl::Text32 &active_text = masked ? masked_text : text;
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 mgl::vec2f caret_size = mgl::vec2f(caret_width, active_text.get_bounds().size.y).floor();
const float overflow_left = (caret.offset_x + padding_left) - (padding_left + text_overflow);
if(overflow_left < 0.0f)
@@ -205,18 +196,18 @@ namespace gsr {
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());
active_text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - active_text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
const auto text_bounds = text.get_bounds();
const auto text_bounds = active_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));
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
}
} else {
text.set_position(text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
text_overflow = 0.0f;
}
@@ -234,7 +225,7 @@ namespace gsr {
});
window.set_scissor(scissor);
window.draw(text);
window.draw(active_text);
if(show_selection)
draw_caret_selection(window, draw_pos, caret_size);
@@ -254,11 +245,16 @@ namespace gsr {
}
void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
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.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), 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::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());
mgl::Color caret_select_color = get_color_theme().tint_color;
caret_select_color.a = 100;
caret_selection_rect.set_color(caret_select_color);
@@ -274,13 +270,43 @@ namespace gsr {
return { max_width, text.get_bounds().size.y + padding_top + padding_bottom };
}
EntryValidateHandlerResult Entry::set_text(std::string str) {
EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
void Entry::move_caret_word(Direction direction, size_t max_codepoints) {
mgl::Text32 &active_text = masked ? masked_text : text;
const int dir_step = direction == Direction::LEFT ? -1 : 1;
const int num_delimiter_chars = 15;
const char delimiter_chars[num_delimiter_chars + 1] = " \t\n/.,:;\\[](){}";
const char32_t *text_str = active_text.get_string().data();
int num_non_delimiter_chars_found = 0;
for(size_t i = 0; i < max_codepoints; ++i) {
const uint32_t codepoint = text_str[caret.index];
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)
break;
} else {
++num_non_delimiter_chars_found;
}
if(caret.index + dir_step < 0 || caret.index + dir_step > (int)active_text.get_string().size())
break;
caret.index += dir_step;
}
// TODO: Move right by some characters instead of calculating every character to caret index
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
}
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());
mgl::Text32 &active_text = masked ? masked_text : text;
caret.index = active_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 = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
@@ -290,40 +316,58 @@ 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));
if(masked)
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
// 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::set_masked(bool masked) {
if(masked == this->masked)
return;
this->masked = masked;
if(masked)
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
else
masked_text.set_string(std::u32string());
mgl::Text32 &active_text = masked ? masked_text : text;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
}
bool Entry::is_masked() const {
return masked;
}
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) {
@@ -333,8 +377,9 @@ namespace gsr {
return;
}
mgl::Text32 &active_text = masked ? masked_text : text;
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
@@ -342,17 +387,14 @@ 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();
mgl::Font *font = text.get_font();
CaretIndexPos Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
mgl::Text32 &active_text = masked ? masked_text : text;
const std::u32string &str = active_text.get_string();
mgl::Font *font = active_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, {active_text.get_position().x, active_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') {
@@ -368,8 +410,6 @@ namespace gsr {
break;
result.pos.x += glyph_width;
result.byte_index += clen;
result.utf8_index += 1;
}
return result;
@@ -379,7 +419,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;
@@ -406,7 +446,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;

View File

@@ -4,6 +4,7 @@
#include "../../include/gui/PageStack.hpp"
#include "../../include/gui/FileChooser.hpp"
#include "../../include/gui/Subsection.hpp"
#include "../../include/gui/Image.hpp"
#include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
@@ -965,42 +966,75 @@ namespace gsr {
return streaming_service_list;
}
static std::unique_ptr<Button> create_mask_toggle_button(Entry *entry_to_toggle, mgl::vec2f size) {
auto button = std::make_unique<Button>(&get_theme().body_font, "", size, mgl::Color(0, 0, 0, 0));
Button *button_ptr = button.get();
button->set_icon(&get_theme().masked_texture);
button->on_click = [entry_to_toggle, button_ptr]() {
const bool is_masked = entry_to_toggle->is_masked();
button_ptr->set_icon(is_masked ? &get_theme().unmasked_texture : &get_theme().masked_texture);
entry_to_toggle->set_masked(!is_masked);
};
return button;
}
static Entry* add_stream_key_entry_to_list(List *stream_key_list) {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
key_entry->set_masked(true);
Entry *key_entry_ptr = key_entry.get();
const float mask_icon_size = key_entry_ptr->get_size().y * 0.9f;
list->add_widget(std::move(key_entry));
list->add_widget(create_mask_toggle_button(key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
stream_key_list->add_widget(std::move(list));
return key_entry_ptr;
}
std::unique_ptr<List> SettingsPage::create_stream_key_section() {
auto stream_key_list = std::make_unique<List>(List::Orientation::VERTICAL);
stream_key_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
auto twitch_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
twitch_stream_key_entry_ptr = twitch_stream_key_entry.get();
stream_key_list->add_widget(std::move(twitch_stream_key_entry));
auto youtube_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
youtube_stream_key_entry_ptr = youtube_stream_key_entry.get();
stream_key_list->add_widget(std::move(youtube_stream_key_entry));
auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
rumble_stream_key_entry_ptr = rumble_stream_key_entry.get();
stream_key_list->add_widget(std::move(rumble_stream_key_entry));
twitch_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
youtube_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
rumble_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
stream_key_list_ptr = stream_key_list.get();
return stream_key_list;
}
std::unique_ptr<List> SettingsPage::create_stream_custom_section() {
auto stream_url_list = std::make_unique<List>(List::Orientation::VERTICAL);
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream URL:", get_color_theme().text_color));
std::unique_ptr<List> SettingsPage::create_stream_custom_url() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
auto stream_url_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
stream_url_entry_ptr = stream_url_entry.get();
stream_url_list->add_widget(std::move(stream_url_entry));
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream URL:", get_color_theme().text_color));
list->add_widget(std::move(stream_url_entry));
return list;
}
std::unique_ptr<List> SettingsPage::create_stream_custom_key() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
stream_key_entry->set_masked(true);
stream_key_entry_ptr = stream_key_entry.get();
stream_url_list->add_widget(std::move(stream_key_entry));
const float mask_icon_size = stream_key_entry_ptr->get_size().y * 0.9f;
list->add_widget(std::move(stream_key_entry));
list->add_widget(create_mask_toggle_button(stream_key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
return list;
}
stream_url_list_ptr = stream_url_list.get();
return stream_url_list;
std::unique_ptr<List> SettingsPage::create_stream_custom_section() {
auto custom_stream_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto stream_url_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
stream_url_list->add_widget(create_stream_custom_url());
stream_url_list->add_widget(create_stream_container());
custom_stream_list->add_widget(std::move(stream_url_list));
custom_stream_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
custom_stream_list->add_widget(create_stream_custom_key());
custom_stream_list_ptr = custom_stream_list.get();
return custom_stream_list;
}
std::unique_ptr<ComboBox> SettingsPage::create_stream_container_box() {
@@ -1013,11 +1047,10 @@ namespace gsr {
return container_box;
}
std::unique_ptr<List> SettingsPage::create_stream_container_section() {
std::unique_ptr<List> SettingsPage::create_stream_container() {
auto container_list = std::make_unique<List>(List::Orientation::VERTICAL);
container_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Container:", get_color_theme().text_color));
container_list->add_widget(create_stream_container_box());
container_list_ptr = container_list.get();
return container_list;
}
@@ -1026,7 +1059,6 @@ namespace gsr {
streaming_info_list->add_widget(create_streaming_service_section());
streaming_info_list->add_widget(create_stream_key_section());
streaming_info_list->add_widget(create_stream_custom_section());
streaming_info_list->add_widget(create_stream_container_section());
settings_list_ptr->add_widget(std::make_unique<Subsection>("Streaming info", std::move(streaming_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -1051,11 +1083,10 @@ namespace gsr {
const bool rumble_option = id == "rumble";
const bool custom_option = id == "custom";
stream_key_list_ptr->set_visible(!custom_option);
stream_url_list_ptr->set_visible(custom_option);
container_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->set_visible(twitch_option);
youtube_stream_key_entry_ptr->set_visible(youtube_option);
rumble_stream_key_entry_ptr->set_visible(rumble_option);
custom_stream_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->get_parent_widget()->set_visible(twitch_option);
youtube_stream_key_entry_ptr->get_parent_widget()->set_visible(youtube_option);
rumble_stream_key_entry_ptr->get_parent_widget()->set_visible(rumble_option);
return true;
};
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");

View File

@@ -64,6 +64,10 @@ namespace gsr {
this->visible = visible;
}
Widget* Widget::get_parent_widget() {
return parent_widget;
}
void add_widget_to_remove(std::unique_ptr<Widget> widget) {
widgets_to_remove.push_back(std::move(widget));
}