diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d267196..794ea2fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. -# 1 +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -41,8 +41,6 @@ find_package(Freetype REQUIRED) file(GLOB_RECURSE HEADER_FILES "include/*.hpp") file(GLOB_RECURSE SOURCE_FILES "src/*.cpp") -# exclude_files_by_regex(SOURCE_FILES /* regex */) - add_library( ekg STATIC ${SOURCE_FILES} @@ -66,6 +64,7 @@ target_compile_options( target_compile_definitions( ekg PRIVATE EKG_VERSION="${EKG_VERSION}" + EKG_EOF_SYSTEM="${PLATFORM_EOF_SYSTEM}" ) set_target_properties( diff --git a/cmake/properties.cmake b/cmake/properties.cmake index c228367e..e668ee68 100644 --- a/cmake/properties.cmake +++ b/cmake/properties.cmake @@ -37,15 +37,19 @@ endif() if(WIN32 OR EKG_FORCE_WINDOWS) set(LIBRARY_OUTPUT_PATH "../lib/windows/") - set(PLATFORM "windows") + set(PLATFORM "windows") + set(PLATFORM_EOF_SYSTEM "\\r\\n") elseif(ANDROID OR EKG_FORCE_ANDROID) set(LIBRARY_OUTPUT_PATH "${ANDROID_ABI}/") - set(PLATFORM "${ANDROID_ABI}") + set(PLATFORM "${ANDROID_ABI}") + set(PLATFORM_EOF_SYSTEM "\\n") elseif(EKG_EMSCRIPTEN_BUILD_TYPE) set(LIBRARY_OUTPUT_PATH "../lib/linux-wasm/") - set(PLATFORM "linux-wasm") + set(PLATFORM "linux-wasm") + set(PLATFORM_EOF_SYSTEM "\\n") elseif(LINUX OR EKG_FORCE_LINUX) - # WSL is not detected as Linux-based OS, same you use a Linux kernel-based distribution. + # WSL is not detected as Linux-kernel based OS; force is necessary >< set(LIBRARY_OUTPUT_PATH "../lib/linux/") - set(PLATFORM "linux") + set(PLATFORM "linux") + set(PLATFORM_EOF_SYSTEM "\\n") endif() diff --git a/include/ekg/core/pools.hpp b/include/ekg/core/pools.hpp index 84a2c7c7..af9461b4 100644 --- a/include/ekg/core/pools.hpp +++ b/include/ekg/core/pools.hpp @@ -50,6 +50,9 @@ #include "ekg/ui/popup/popup.hpp" #include "ekg/ui/popup/widget.hpp" +#include "ekg/ui/textbox/textbox.hpp" +#include "ekg/ui/textbox/widget.hpp" + namespace ekg::core { void registry(ekg::property_t &property); } @@ -80,6 +83,7 @@ namespace ekg::core { ekg_core_widget_call_impl(ekg::scrollbar_t, widget_descriptor_at, todo); \ ekg_core_widget_call_impl(ekg::slider_t, widget_descriptor_at, todo); \ ekg_core_widget_call_impl(ekg::popup_t, widget_descriptor_at, todo); \ + ekg_core_widget_call_impl(ekg::textbox_t, widget_descriptor_at, todo); \ } #define ekg_core_unique_widget_call(descriptor_t, widget_descriptor_type, widget_descriptor_at, todo) \ @@ -155,6 +159,9 @@ namespace ekg { ekg::pool popup_property {}; ekg::pool popup {}; + + ekg::pool textbox_property {}; + ekg::pool textbox {}; } pools; template @@ -200,6 +207,10 @@ namespace ekg { return ekg::io::any_static_cast( &ekg::pools.popup_property.query(at) ); + case ekg::type::textbox: + return ekg::io::any_static_cast( + &ekg::pools.textbox_property.query(at) + ); } case ekg::type::button: return ekg::io::any_static_cast( @@ -225,6 +236,10 @@ namespace ekg { return ekg::io::any_static_cast( &ekg::pools.popup.query(at) ); + case ekg::type::textbox: + return ekg::io::any_static_cast( + &ekg::pools.textbox.query(at) + ); } return t::not_found; @@ -322,6 +337,20 @@ namespace ekg { ); } + case ekg::type::textbox: { + ekg_registry_widget_impl( + ekg::textbox_t, + ekg::pools.textbox, + ekg::pools.textbox_property, + false, + { + property.is_childnizate = false; + property.is_children_docknizable = false; + widget.color_scheme = global_theme.textbox_color_scheme; + } + ); + } + case ekg::type::stack: { ekg::stack_t &stack { ekg::pools.stack.push_back( diff --git a/include/ekg/draw/shape/shape.hpp b/include/ekg/draw/shape/shape.hpp index 9a1aad6d..2cbc60ac 100644 --- a/include/ekg/draw/shape/shape.hpp +++ b/include/ekg/draw/shape/shape.hpp @@ -36,14 +36,14 @@ namespace ekg::draw { void rect( const ekg::rect_t &rect, const ekg::rgba_t &color, - ekg::pixel_thickness_t line_thicnkess, + ekg::pixel_thickness_t line_thickness, ekg::at_t &sampler_at ); void rect( float x, float y, float w, float h, const ekg::rgba_t &color, - ekg::pixel_thickness_t line_thicnkess, + ekg::pixel_thickness_t line_thickness, ekg::at_t &sampler_at ); diff --git a/include/ekg/draw/typography/font.hpp b/include/ekg/draw/typography/font.hpp index 4d0a6cfb..7be937f4 100644 --- a/include/ekg/draw/typography/font.hpp +++ b/include/ekg/draw/typography/font.hpp @@ -50,11 +50,13 @@ namespace ekg::draw { uint32_t font_size {}; float text_height {}; float non_swizzlable_range {}; + float space_wsize {}; FT_Bool ft_bool_kerning {}; bool font_size_changed {}; bool was_initialized {}; bool is_any_functional_font_face_loaded {}; + bool is_monospaced {}; public: void init(); void quit(); diff --git a/include/ekg/handler/input.hpp b/include/ekg/handler/input.hpp index 80606034..f7d8579c 100644 --- a/include/ekg/handler/input.hpp +++ b/include/ekg/handler/input.hpp @@ -71,8 +71,11 @@ namespace ekg { bool has_motion {}; bool was_wheel {}; bool was_typed {}; + std::string_view typed {}; }; + using input_bind_function_t = ekg::result (*)(std::string_view, bool); + struct input_key_t { public: int32_t key {}; @@ -89,10 +92,11 @@ namespace ekg { namespace ekg { ekg::input_info_t &input(); - bool fire(std::string_view tag); + bool fired(std::string_view tag); bool input(std::string_view input); void bind(std::string_view tag, std::string_view input); void bind(std::string_view tag, std::vector inputs); + void listener(ekg::input_bind_function_t input_bind_listener_function); } #endif diff --git a/include/ekg/handler/input/handler.hpp b/include/ekg/handler/input/handler.hpp index d7e6adef..818bdde9 100644 --- a/include/ekg/handler/input/handler.hpp +++ b/include/ekg/handler/input/handler.hpp @@ -29,6 +29,13 @@ namespace ekg::handler { ekg::timing_t double_interact {}; ekg::timing_t last_time_wheel_was_fired {}; + ekg::timing_t cooldown_wheel {}; + + // prevent too many unoptmized use of string allocations + std::string key_name {}; + std::string string_builder {}; + + ekg::input_bind_function_t input_bind_listener_function {}; public: ekg::input_info_t input {}; protected: @@ -81,6 +88,10 @@ namespace ekg::handler { bool get_input_state( std::string_view tag ); + + void set_input_bind_listener( + ekg::input_bind_function_t input_bind_listener_function + ); }; } diff --git a/include/ekg/handler/theme.hpp b/include/ekg/handler/theme.hpp index 8756af0a..59923dc0 100644 --- a/include/ekg/handler/theme.hpp +++ b/include/ekg/handler/theme.hpp @@ -30,6 +30,7 @@ #include "ekg/ui/scrollbar/scrollbar.hpp" #include "ekg/ui/slider/slider.hpp" #include "ekg/ui/popup/popup.hpp" +#include "ekg/ui/textbox/textbox.hpp" namespace ekg { struct theme_t { @@ -46,6 +47,7 @@ namespace ekg { ekg::scrollbar_color_scheme_t scrollbar_color_scheme {}; ekg::slider_color_scheme_t slider_color_scheme {}; ekg::popup_color_scheme_t popup_color_scheme {}; + ekg::textbox_color_scheme_t textbox_color_scheme {}; }; ekg::theme_t &theme(std::string tag = ""); diff --git a/include/ekg/io/descriptor.hpp b/include/ekg/io/descriptor.hpp index 32fbe07d..03e07b85 100644 --- a/include/ekg/io/descriptor.hpp +++ b/include/ekg/io/descriptor.hpp @@ -39,6 +39,7 @@ namespace ekg { slider = 8, label = 9, popup = 10, + textbox = 11 }; } diff --git a/include/ekg/io/memory.hpp b/include/ekg/io/memory.hpp index 4fff11c6..d548b9c3 100644 --- a/include/ekg/io/memory.hpp +++ b/include/ekg/io/memory.hpp @@ -84,9 +84,8 @@ namespace ekg { **/ namespace ekg { /** - * Broken heart hash.......... **/ - constexpr ekg::id_t not_found {2942656639}; + constexpr ekg::id_t not_found {283233071866}; struct at_t { public: @@ -130,7 +129,7 @@ namespace ekg { descriptor.at.unique_id = this->highest_unique_id++; descriptor.at.flags = t::type; - descriptor.at.index = index; + descriptor.at.index = index; return descriptor; } @@ -149,6 +148,7 @@ namespace ekg { for (size_t it {}; it < size; it++) { t &descriptor {this->loaded.at(it)}; descriptor.at.index = it; + if (descriptor.at.unique_id == at.unique_id) { at.index = it; return descriptor; diff --git a/include/ekg/io/utf.hpp b/include/ekg/io/utf.hpp index 4ceb9353..a6b6c4a7 100644 --- a/include/ekg/io/utf.hpp +++ b/include/ekg/io/utf.hpp @@ -29,8 +29,136 @@ #include #include #include +#include +#include namespace ekg { + /** + * Check utf8 sequence and add to `it` index; checking for the first byte of a char utf8 encoded: + * + * * - If first char fit less than 127; 1 byte utf8 char. + * * - If first char fit equals 192~224; 2 byte utf8 char. + * * - If first char fit equals 224~240; 3 byte utf8 char. + * * - If first char fit equals 240~248; 4 byte utf8 char. + * + * By the amount of bytes per-char it is added to `it` e.g: + * * - If 224~240; then `it + 2` (`it` already counts as 1 byte) + * + * @param `uc8` the current char to be checked. + * @param `c32` the post utf32 char converted. + * @param `utf8_str` the utf8 encoded text. + * @param `it` the current `utf8_str` index. + * + **/ + void utf8_sequence( + uint8_t &uc8, + char32_t &c32, + std::string_view utf8_str, + size_t &it + ); + + /** + * Find for an aligned utf position by byte position: + * + * * - If no utf position was found then `utf_pos` keeps unchanged. + * * - If an invalid byte position is passed, e.g a position inside a + * utf sequence, it return atuomatically `false`. + * + * @param `string` string for find utf position + * @param `byte_pos` byte pos used for find utf position + * @param `utf_pos` utf position relative to byte position + * + * @return `true` if a valid utf position was found else `false` if was not found + **/ + bool utf8_find_utf_pos_by_byte_pos( + std::string &string, + size_t byte_pos, + size_t &utf_pos + ); + + /** + * Find for a byte pos from an aligned utf position: + * * - If no byte position was found then `utf_byte` keeps unchanged. + * + * @param `string` string for find utf position + * @param `utf_pos` utf pos used for find byte pos + * @param `byte_pos` byte pos relative to utf position + * + * @return `true` if a valid byte position was found else `false` if was not found + **/ + bool utf8_find_byte_pos_by_utf_pos( + std::string &string, + size_t utf_pos, + size_t &byte_pos + ); + + /** + * Align utf position and byte-position by the next byte-position: + * * - If next byte pos is greater than unaligned byte pos, then move to left; if not move to right. + * + * May output be corrupted if current unaligned byte-pos is an invalid utf position. + * + * @param `string` string for be used in alignment + * @param `unaligned_byte_pos` byte pos to be aligned + * @param `unaligned_utf_pos` utf pos to be aligned + * @param `next_byte_pos` next byte pos to align byte-pos and utf-pos + * + * @return true if could align, else false if could not + **/ + bool utf8_align_utf_pos_by_byte_pos( + std::string &string, + size_t &unaligned_byte_pos, + size_t &unaligned_utf_pos, + size_t next_byte_pos + ); + + /** + * Get the first nearest group matched byte-position: + * * - For left dock the first matched group position is assigned; + * * - For right dock the last matched group position is assigned. + * + * @param `begin` const string iterator to begin regex matching + * @param `end` const string iterator to end regex matching + * @param `nearest_byte_pos` first group matched bidirectional byte-position + * + * @return true if matched else false if not matched + **/ + bool utf8_nearest_regex_group_matched_position( + const std::string::const_iterator &begin, + const std::string::const_iterator &end, + size_t &nearest_byte_pos, + std::regex &pattern, + const ekg::dock &dock + ); + + /** + * Check if current index is last index by the last utf char size. + * + * @param `byte_index` the current index in byte to be checked + * @param `last_char_bytes` the utf size (1 | 2 | 3 | 4 bytes per utf char) + * @param `string_bytes_size` the string size in bytes + * + * @return true if is last index else false if not last index + **/ + bool utf8_is_last_index( + size_t byte_index, + size_t last_char_bytes, + size_t string_bytes_size + ); + + /** + * Check for last char utf8 bytes sequence. + * + * @param `utf8_str` the utf8 valid string to find + * @param `bytes` the last char utf8 bytes output + * + * @return true if found-valid else false if not found + **/ + bool utf8_check_last_char_byte_sequence( + std::string &utf8_str, + size_t &bytes + ); + /** * Returns a UTF string by `char32` converting * the UTF-32 unique char into a sequence of UTF-8 @@ -51,8 +179,7 @@ namespace ekg { uint64_t size ); - /** - * Returns the `string` length considering UTF chars. + /** * Returns the `string` length considering UTF chars. */ uint64_t utf8_length( std::string_view string @@ -78,24 +205,9 @@ namespace ekg { std::string_view string ); - /** - * Fast splitter specialized in `\n` or `\r\n` (non OS unix-based). - * UTF to sinalize the string unicode-like suggested by EKG. - */ - void utf8_split_new_line( - std::string_view string, - std::vector &utf8_split_new_lined - ); - - /** - * Return true if `string` contains `find_char`, - * then it must allocate and insert elements to - * `p_string_split_list` ptr. - */ - bool utf8_split( - std::vector &string_split_list, - const std::string &string, - char find_char + size_t utf8_split_endings( + std::string_view line, + std::vector &splitted ); template @@ -115,6 +227,77 @@ namespace ekg { ) ); } + + void utf8_concat( + std::string &string, + const ekg::vec4_t &stride, + std::string &concated + ); +} + +namespace ekg::io { + typedef std::vector chunk_t; +} + +namespace ekg { + class text { + protected: + std::list loaded_chunks {}; + size_t lines_per_chunk_limit {100000}; + size_t total_lines {}; + size_t total_chars {}; + size_t prev_lines {}; + size_t prev_total_chars {}; + + bool was_audited {}; + bool should_count {}; + protected: + void swizzle( + std::list::iterator chunk_it, + size_t line_index, + std::vector &to_swizzle, + bool skip_first_line + ); + public: + void push_back(std::string_view line); + size_t set(size_t index, std::string_view line, ekg::io::chunk_t &split_endings); + size_t set(size_t index, std::string_view line); + + std::string read( + ekg::vec2_t &begin, + ekg::vec2_t &end + ); + + void insert( + size_t index, + const ekg::io::chunk_t &to_insert_chunk + ); + + void insert( + size_t index, + std::string_view line + ); + + void erase( + size_t begin, + size_t end + ); + + std::list &chunks_data(); + size_t length_of_chunks(); + + std::string at(size_t index); + size_t length_of_lines(bool force = false); + size_t length_of_chars(); + + bool audited(); + void unset_audited(); + void gc(); + void sanitize(); + + void set_lines_per_chunk_limit(size_t limit); + size_t get_lines_per_chunk_limit(); + }; } #endif diff --git a/include/ekg/math/floating_point.hpp b/include/ekg/math/floating_point.hpp index fddebb48..86bbe81c 100644 --- a/include/ekg/math/floating_point.hpp +++ b/include/ekg/math/floating_point.hpp @@ -28,8 +28,6 @@ #include namespace ekg { - constexpr float pi {3.1415927f}; - constexpr bool fequalsf(float compare, float compared) { return ( fabsf(compare - compared) diff --git a/include/ekg/math/geometry.hpp b/include/ekg/math/geometry.hpp index d0d0b358..9d147c0c 100644 --- a/include/ekg/math/geometry.hpp +++ b/include/ekg/math/geometry.hpp @@ -32,6 +32,7 @@ namespace ekg { typedef int32_t pixel_thickness_t; typedef float pixel_t; + constexpr float pi {3.1415927f}; constexpr float one_pixel {1.0000000f}; constexpr float half_pixel {0.5000000f}; @@ -94,6 +95,14 @@ namespace ekg { this->y / div_by }; } + + bool operator == (const ekg::vec2_t &vec) { + return this->x == vec.x && this->y == vec.y; + } + + bool operator != (const ekg::vec2_t &vec) { + return !(*this == vec); + } }; template @@ -411,12 +420,12 @@ namespace ekg { ekg::rect_t rect, float square ) { - const t zero {}; + const t zero {}; return ekg::rect_t { ekg::clamp_min(rect.x, zero), ekg::clamp_min(rect.y, zero), - ekg::clamp_max(rect.w, square), - ekg::clamp_max(rect.h, square) + ekg::clamp_min(rect.w, square), + ekg::clamp_min(rect.h, square) }; } @@ -619,7 +628,15 @@ namespace ekg { } template - using rgba_t =ekg::vec4_t; + using rgba_t = ekg::vec4_t; + + template + constexpr t arithmetic_normalize( + t x, + t len + ) { + return x / len; + } void ortho( float *p_mat4x4, diff --git a/include/ekg/platform/base.hpp b/include/ekg/platform/base.hpp index e72518e8..17d9adf8 100644 --- a/include/ekg/platform/base.hpp +++ b/include/ekg/platform/base.hpp @@ -25,6 +25,7 @@ #define EKG_PLATFORM_HPP #include "ekg/io/event.hpp" +#include namespace ekg::platform { class base { @@ -40,8 +41,8 @@ namespace ekg::platform { virtual void update() {}; virtual void get_key_name(ekg::input_key_t &key, std::string &name) {}; virtual void get_special_key(ekg::input_key_t &key, ekg::special_key &espcial_key) {}; - virtual const char *get_clipboard_text() { return nullptr; }; - virtual void set_clipboard_text(const char *p_text) {}; + virtual std::string_view get_clipboard_text() { return ""; }; + virtual void set_clipboard_text(std::string_view text) {}; virtual bool has_clipboard_text() { return false; }; }; } diff --git a/include/ekg/platform/sdl/sdl2.hpp b/include/ekg/platform/sdl/sdl2.hpp index bc63ad34..5e72891b 100644 --- a/include/ekg/platform/sdl/sdl2.hpp +++ b/include/ekg/platform/sdl/sdl2.hpp @@ -50,8 +50,8 @@ namespace ekg { void update() override; void get_key_name(ekg::input_key_t &key, std::string &name) override; void get_special_key(ekg::input_key_t &key, ekg::special_key &special_key) override; - const char *get_clipboard_text() override; - void set_clipboard_text(const char *p_text) override; + std::string_view get_clipboard_text() override; + void set_clipboard_text(std::string_view text) override; bool has_clipboard_text() override; }; diff --git a/include/ekg/ui/textbox/textbox.hpp b/include/ekg/ui/textbox/textbox.hpp new file mode 100644 index 00000000..d737559f --- /dev/null +++ b/include/ekg/ui/textbox/textbox.hpp @@ -0,0 +1,160 @@ +/** + * MIT License + * + * Copyright (c) 2022-2025 Rina Wilk / vokegpu@gmail.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef EKG_UI_TEXTBOX_HPP +#define EKG_UI_TEXTBOX_HPP + +#include "ekg/ui/scrollbar/scrollbar.hpp" +#include "ekg/io/descriptor.hpp" +#include "ekg/io/utf.hpp" +#include "ekg/io/font.hpp" +#include "ekg/ui/property.hpp" +#include + +namespace ekg { + struct textbox_color_scheme_t { + public: + ekg::rgba_t background {}; + ekg::rgba_t outline {}; + ekg::rgba_t text_foreground {}; + ekg::rgba_t text_select_foreground {}; + ekg::rgba_t text_cursor_foreground {}; + ekg::rgba_t text_select_outline {}; + ekg::pixel_thickness_t cursor_thickness {2}; + bool caret_cursor {}; + ekg::pixel_thickness_t gutter_margin {2}; + }; + + struct textbox_t { + public: + enum operation : size_t { + modifier_left, + modifier_right + }; + static constexpr size_t operation_enum_size {ekg::textbox_t::operation::modifier_right+1}; + + struct select_draw_layer_t { + public: + bool is_ab_equals {}; + bool is_always_static {}; + ekg::rect_t rect {}; + }; + + struct cursor_t { + public: + ekg::dock direction {}; + size_t highest_char_index {}; + ekg::vec2_t a {}; + ekg::vec2_t b {}; + ekg::vec2_t delta {}; + ekg::rect_t rect {}; + bool is_ignored {}; + bool is_deleted {}; + public: + bool operator == (const ekg::vec2_t &index) { + return index.x == this->a.x && index.y == this->a.y && index.x == this->b.x && index.y == this->b.y; + } + + bool operator != (const ekg::vec2_t &index) { + return !(*this == index); + } + + bool operator > (const ekg::vec2_t &index) { + return (index.x > this->a.x && index.y == this->a.y) || (index.y > this->a.y); + } + + bool operator >= (const ekg::vec2_t &index) { + return (index.x >= this->a.x && index.y == this->a.y) || (index.y > this->a.y); + } + + bool operator < (const ekg::vec2_t &index) { + return (index.x < this->b.x && index.y == this->b.y) || (index.y < this->b.y); + } + + bool operator <= (const ekg::vec2_t &index) { + return (index.x <= this->b.x && index.y == this->b.y) || (index.y < this->b.y); + } + + bool operator == (const ekg::textbox_t::cursor_t &cursor) { + return (this->a.x == cursor.a.x && this->a.y == cursor.a.y) && (this->b.x == cursor.b.x && this->b.y == cursor.b.y); + } + + bool operator != (const ekg::textbox_t::cursor_t &cursor) { + return !(*this == cursor); + } + }; + + struct widget_t { + public: + ekg::rect_t rect_text_size {}; + ekg::property_t scrollbar_property {}; + ekg::scrollbar_t scrollbar {}; + std::vector cursors {}; + std::vector layers_select {}; + + size_t last_layers_select_size {}; + size_t view_line_index {UINT64_MAX}; + std::list::iterator view_chunk_it {}; + size_t view_chunk_line_index {UINT64_MAX}; + + ekg::vec2_t picked_left {UINT64_MAX, UINT64_MAX}; + ekg::vec2_t picked_right {UINT64_MAX, UINT64_MAX}; + size_t current_cursor_index {UINT64_MAX}; + + ekg::timing_t cursor_timing {}; + bool set_cursor_static {}; + bool unset_cursor_static {}; + }; + + enum class text_input_mode : uint8_t { + none, + text, + ending, + backspace, + del + }; + + static constexpr ekg::type type {ekg::type::textbox}; + static ekg::textbox_t not_found; + public: + ekg::at_t property_at {}; + public: + std::string tag {}; + ekg::text text {}; + ekg::font font_size {ekg::font::medium}; + ekg::flags_t dock {}; + ekg::rect_t rect {.w = 100.0f}; + ekg::textbox_color_scheme_t color_scheme {}; + ekg::textbox_t::widget_t widget {}; + ekg::at_array_t layers {}; + + std::array regex_operations { + std::regex("(^)|([^\\s:&\\(\\);]+)|(:+)|(&+)|(\\(\\))|(\\()|(\\))|(;+)|([.]+)|(,+)"), + std::regex("[^\\s:&\\(\\);)]([\\s:])|(:+)|(&+)|(\\(\\))|(\\()|(\\))|(;+)|(\\.+)|(,+)|($)") + }; + public: + ekg_descriptor(ekg::textbox_t); + }; +} + +#endif diff --git a/include/ekg/ui/textbox/widget.hpp b/include/ekg/ui/textbox/widget.hpp new file mode 100644 index 00000000..6033f975 --- /dev/null +++ b/include/ekg/ui/textbox/widget.hpp @@ -0,0 +1,114 @@ +/** + * MIT License + * + * Copyright (c) 2022-2025 Rina Wilk / vokegpu@gmail.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef EKG_UI_TEXTBOX_WIDGET_HPP +#define EKG_UI_TEXTBOX_WIDGET_HPP + +#include "ekg/ui/property.hpp" +#include "textbox.hpp" + +namespace ekg::ui { + enum class textbox_operation { + insert_inline, + insert_multiline, + insert_line, + erase_inline, + erase_multiline + }; + + void refresh_cursors_pos( + ekg::textbox_t &textbox, + ekg::textbox_t::cursor_t &origin, + const ekg::vec2_t &displacement_a, + const ekg::vec2_t &displacement_b, + const ekg::ui::textbox_operation &operation + ); + + bool find_cursor( + ekg::textbox_t &textbox, + ekg::vec2_t &index, + ekg::textbox_t::cursor_t &cursor_out + ); + + bool find_index_by_interact( + ekg::property_t &property, + ekg::textbox_t &textbox, + ekg::vec2_t &interact, + ekg::vec2_t &index + ); + + void handle_cursor_interact( + ekg::property_t &property, + ekg::textbox_t &textbox, + bool picked_index, + ekg::vec2_t &pick_index, + ekg::input_info_t &input + ); + + void handle_erase( + ekg::textbox_t &textbox, + ekg::textbox_t::cursor_t &cursor + ); + + void refresh_scroll_positions( + ekg::textbox_t &textbox + ); + + void handle_insert( + ekg::textbox_t &textbox, + ekg::textbox_t::cursor_t &cursor, + std::string_view typed + ); + + void reload( + ekg::property_t &property, + ekg::textbox_t &textbox + ); + + void event( + ekg::property_t &property, + ekg::textbox_t &textbox, + const ekg::io::stage &stage + ); + + void high_frequency( + ekg::property_t &property, + ekg::textbox_t &textbox + ); + + void pass( + ekg::property_t &property, + ekg::textbox_t &textbox + ); + + void buffering( + ekg::property_t &property, + ekg::textbox_t &textbox + ); + + void unmap( + ekg::textbox_t &textbox + ); +} + +#endif diff --git a/src/core/runtime.cpp b/src/core/runtime.cpp index 6917921c..9a7399d7 100644 --- a/src/core/runtime.cpp +++ b/src/core/runtime.cpp @@ -302,11 +302,12 @@ void ekg::core::poll_event() { hovered = ( !( - ekg::p_core->p_platform_base->event.type == ekg::io::event_type::key_down - || - ekg::p_core->p_platform_base->event.type == ekg::io::event_type::key_up - || - ekg::p_core->p_platform_base->event.type == ekg::io::event_type::text_input + false + //ekg::p_core->p_platform_base->event.type == ekg::io::event_type::key_down + //|| + //ekg::p_core->p_platform_base->event.type == ekg::io::event_type::key_up + //|| + //ekg::p_core->p_platform_base->event.type == ekg::io::event_type::text_input ) && property.states.is_hovering @@ -321,11 +322,18 @@ void ekg::core::poll_event() { ekg::query(focused_at) }; - focused_property != ekg::property_t::not_found - && (focused_property.states.is_hovering = false); - - focused_at = at; - first_absolute = false; + if (first_absolute) { + first_absolute = ( + focused_property != ekg::property_t::not_found + && property.abs_parent_at == focused_property.abs_parent_at + ); + } + + if (!first_absolute) { + focused_property != ekg::property_t::not_found + && (focused_property.states.is_hovering = false); + focused_at = at; + } } if (property.states.is_absolute && !first_absolute) { diff --git a/src/draw/typography/font.cpp b/src/draw/typography/font.cpp index fc8ee67d..ec04cb48 100644 --- a/src/draw/typography/font.cpp +++ b/src/draw/typography/font.cpp @@ -1,5 +1,3 @@ -#include "ekg/draw/typography/font.hpp" - /** * MIT License * @@ -401,6 +399,12 @@ void ekg::draw::font::reload() { ); } + this->is_monospaced = FT_IS_FIXED_WIDTH(text_font_face.ft_face); + this->space_wsize = this->text_height / 3.0f; + + FT_Vector space_char_metrics {}; + FT_Get_Kerning(text_font_face.ft_face, 32, 32, 0, &space_char_metrics); + this->text_height = static_cast(this->font_size); this->offset_text_height = this->text_height / 6; @@ -423,11 +427,11 @@ void ekg::draw::font::blit( const ekg::rgba_t &color ) { if ( - !this->is_any_functional_font_face_loaded - || - color.w < 0.1f - || - text.empty() + !this->is_any_functional_font_face_loaded + || + color.w < 0.1f + || + text.empty() ) { return; } @@ -435,7 +439,9 @@ void ekg::draw::font::blit( x = static_cast(static_cast(x)); y = static_cast(static_cast(y - this->offset_text_height)); - ekg::gpu::data_t &data {ekg::p_core->draw_allocator.bind_current_data()}; + ekg::gpu::data_t &data { + ekg::p_core->draw_allocator.bind_current_data() + }; data.buffer[0] = x; data.buffer[1] = y; diff --git a/src/ekg.cpp b/src/ekg.cpp index 03b9ea72..b677c30d 100644 --- a/src/ekg.cpp +++ b/src/ekg.cpp @@ -138,12 +138,14 @@ void ekg::render() { ekg::gui.ui.redraw = false; ekg::p_core->draw_allocator.invoke(); + size_t a {}; for (ekg::at_t &at : ekg::p_core->stack) { ekg::property_t &property { ekg::query(at) }; + if (property == ekg::property_t::not_found) { continue; } @@ -165,7 +167,7 @@ void ekg::render() { * * I do not know why this is hapenning, likely wtf, if I remove this * scrolling is horrible, I am not sure why this is hapning, I tried make the make redraw always, - * rendering everything, no allocators asserts. Nothing. I tried do a stupid `meow` where t receives a descriptor, + * rendering everything, no allocators asscerts. Nothing. I tried do a stupid `meow` where t receives a descriptor, * but nothing too. I do not know why this happens but it is very insanely weird. Post does not affect the performance * a lot, may we consider to let for some long time. While no solution was found. * @@ -183,6 +185,7 @@ void ekg::render() { } ekg::ui::buffering(property, descriptor); + //property.widget.should_buffering = false; ); } diff --git a/src/handler/input.cpp b/src/handler/input.cpp index 69b02f25..fbc5f310 100644 --- a/src/handler/input.cpp +++ b/src/handler/input.cpp @@ -1,3 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2022-2025 Rina Wilk / vokegpu@gmail.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ #include "ekg/handler/input.hpp" #include "ekg/core/runtime.hpp" @@ -5,7 +28,7 @@ ekg::input_info_t &ekg::input() { return ekg::p_core->handler_input.input; } -bool ekg::fire(std::string_view tag) { +bool ekg::fired(std::string_view tag) { return ekg::p_core->handler_input.get_input_bind_state(tag); } @@ -22,3 +45,7 @@ void ekg::bind(std::string_view tag, std::vector inputs) { ekg::bind(tag, inputs); } } + +void ekg::listener(ekg::input_bind_function_t input_bind_listener_function) { + ekg::p_core->handler_input.set_input_bind_listener(input_bind_listener_function); +} diff --git a/src/handler/input/handler.cpp b/src/handler/input/handler.cpp index 4b102a3f..adbdc97e 100644 --- a/src/handler/input/handler.cpp +++ b/src/handler/input/handler.cpp @@ -128,26 +128,31 @@ void ekg::handler::input::init() { this->insert_input_bind("textbox-focus", "mouse-1"); this->insert_input_bind("textbox-focus", "finger-click"); - this->insert_input_bind("textbox-action-break", "return"); - this->insert_input_bind("textbox-action-break", "keypad enter"); this->insert_input_bind("textbox-action-select-all", "lctrl+a"); this->insert_input_bind("textbox-action-select-all", "rctrl+a"); this->insert_input_bind("textbox-action-select-all-inline", "mouse-1"); - this->insert_input_bind("textbox-action-select", "lshift"); + + this->insert_input_bind("textbox-action-select", "rshift"); this->insert_input_bind("textbox-action-select", "rshift"); this->insert_input_bind("textbox-action-select-word", "mouse-1-double"); this->insert_input_bind("textbox-action-select-word", "finger-hold"); - this->insert_input_bind("textbox-action-delete-left", "abs-backspace"); - this->insert_input_bind("textbox-action-delete-right", "abs-delete"); + this->insert_input_bind("textbox-action-erase-left", "abs-backspace"); + this->insert_input_bind("textbox-action-erase-right", "abs-delete"); this->insert_input_bind("textbox-action-break-line", "return"); this->insert_input_bind("textbox-action-break-line", "keypad enter"); this->insert_input_bind("textbox-action-break-line", "lshift+return"); this->insert_input_bind("textbox-action-break-line", "rshift+return"); this->insert_input_bind("textbox-action-tab", "tab"); + + this->insert_input_bind("textbox-action-cursor", "abs-mouse-1"); + this->insert_input_bind("textbox-action-multicursor", "lctrl+mouse-1"); + this->insert_input_bind("textbox-action-multicursor", "rctrl+mouse-1"); + this->insert_input_bind("textbox-action-select", "lshift"); + this->insert_input_bind("textbox-action-select", "rshift"); this->insert_input_bind("textbox-action-modifier", "lctrl"); this->insert_input_bind("textbox-action-modifier", "rctrl"); @@ -256,45 +261,49 @@ void ekg::handler::input::poll_event() { case ekg::io::event_type::text_input: { this->input.was_pressed = true; this->input.was_typed = true; + this->input.typed = platform_event.text_input; break; } case ekg::io::event_type::key_down: { this->input.was_pressed = true; - this->input.was_typed = true; - std::string key_name {}; - std::string string_builder {}; + this->string_builder.clear(); ekg::p_core->p_platform_base->get_key_name( platform_event.key, - key_name + this->key_name ); ekg::special_key special_key {ekg::special_key::unknown}; ekg::p_core->p_platform_base->get_special_key(platform_event.key, special_key); if (special_key != ekg::special_key::unknown) { - this->special_keys[static_cast(special_key)][0] = key_name[0]; - string_builder += key_name; + this->special_keys[static_cast(special_key)][0] = this->key_name[0]; - this->set_input_state(string_builder, true); + this->string_builder += "abs-"; + this->string_builder += this->key_name; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); + + this->string_builder.clear(); + this->string_builder += this->key_name; + this->set_input_state(this->string_builder, true); this->is_special_keys_released = true; } else { - std::transform(key_name.begin(), key_name.end(), key_name.begin(), ::tolower); - string_builder += "abs-"; - string_builder += key_name; - - this->set_input_state(string_builder, true); - this->input_released_list.push_back(string_builder); - - string_builder.clear(); - this->complete_with_units(string_builder, key_name); - this->set_input_state(string_builder, true); - this->input_released_list.push_back(string_builder); - - if (string_builder != key_name && !this->contains_unit(string_builder)) { - this->special_keys_unit_pressed.push_back(string_builder); + std::transform(this->key_name.begin(), this->key_name.end(), this->key_name.begin(), ::tolower); + this->string_builder += "abs-"; + this->string_builder += this->key_name; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); + + this->string_builder.clear(); + this->complete_with_units(this->string_builder, this->key_name); + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); + + if (this->string_builder != this->key_name && !this->contains_unit(this->string_builder)) { + this->special_keys_unit_pressed.push_back(this->string_builder); } } @@ -303,12 +312,10 @@ void ekg::handler::input::poll_event() { case ekg::io::event_type::key_up: { this->input.was_released = true; - std::string key_name {}; - std::string string_builder {}; ekg::p_core->p_platform_base->get_key_name( platform_event.key, - key_name + this->key_name ); ekg::special_key special_key {ekg::special_key::unknown}; @@ -316,55 +323,72 @@ void ekg::handler::input::poll_event() { if (special_key != ekg::special_key::unknown) { this->special_keys[static_cast(special_key)][0] = '\0'; - string_builder += key_name; - this->set_input_state(string_builder, false); + this->string_builder = "abs-"; + this->string_builder += this->key_name; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); + + this->string_builder = this->key_name; + this->set_input_state(this->string_builder, false); this->is_special_keys_released = true; - string_builder += "-up"; - this->set_input_state(string_builder, true); + this->string_builder += "-up"; + this->set_input_state(this->string_builder, true); } else { - std::transform(key_name.begin(), key_name.end(), key_name.begin(), ::tolower); - string_builder += "abs-"; - string_builder += key_name; - string_builder += "-up"; - - this->set_input_state(string_builder, true); - this->input_released_list.push_back(string_builder); - - string_builder.clear(); - this->complete_with_units(string_builder, key_name); - string_builder += "-up"; - - this->set_input_state(string_builder, true); - this->input_released_list.push_back(string_builder); + std::transform(this->key_name.begin(), this->key_name.end(), this->key_name.begin(), ::tolower); + this->string_builder = "abs-"; + this->string_builder += this->key_name; + this->string_builder += "-up"; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); + + this->string_builder.clear(); + this->complete_with_units(this->string_builder, this->key_name); + this->string_builder += "-up"; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); } break; } case ekg::io::event_type::mouse_button_down: { - std::string string_builder {}; - std::string key_name {"mouse"}; + this->input.was_pressed = true; + this->key_name = "mouse"; - this->set_input_state(key_name, true); - this->input_released_list.push_back(key_name); + this->string_builder = "abs-"; + this->string_builder += this->key_name; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); - key_name = "mouse-"; - key_name += std::to_string(platform_event.mouse_button); + this->set_input_state(this->key_name, true); + this->input_released_list.push_back(this->key_name); - this->input.was_pressed = true; - this->complete_with_units(string_builder, key_name); - this->set_input_state(string_builder, true); - this->input_released_list.push_back(string_builder); + this->key_name = "mouse-"; + this->key_name += std::to_string(platform_event.mouse_button); + + this->string_builder = "abs-"; + this->string_builder += this->key_name; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); + + this->string_builder.clear(); + this->complete_with_units(this->string_builder, this->key_name); + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); bool double_click_factor {ekg::reach(this->double_interact, 500)}; if (!double_click_factor) { - string_builder += "-double"; - this->set_input_state(string_builder, true); + this->string_builder += "-double"; + this->set_input_state(this->string_builder, true); + + this->string_builder = "abs-" + this->string_builder; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); - this->double_click_mouse_buttons_pressed.push_back(string_builder); - this->input_released_list.push_back(string_builder); + this->double_click_mouse_buttons_pressed.push_back(this->string_builder); + this->input_released_list.push_back(this->string_builder); } if (double_click_factor) { @@ -375,21 +399,32 @@ void ekg::handler::input::poll_event() { } case ekg::io::event_type::mouse_button_up: { - std::string string_builder {}; - std::string key_name {"mouse-up"}; - this->input.was_released = true; - this->set_input_state(key_name, true); - this->input_released_list.push_back(key_name); + this->key_name = "mouse-up"; + + this->string_builder = "abs-"; + this->string_builder += this->key_name; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); + + this->set_input_state(this->key_name, true); + this->input_released_list.push_back(this->key_name); - key_name = "mouse-"; - key_name += std::to_string(platform_event.mouse_button); + this->key_name = "mouse-"; + this->key_name += std::to_string(platform_event.mouse_button); - this->complete_with_units(string_builder, key_name); - string_builder += "-up"; + this->string_builder = "abs-"; + this->string_builder += this->key_name; + this->string_builder += "-up"; + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); - this->set_input_state(string_builder, true); - this->input_released_list.push_back(string_builder); + this->string_builder.clear(); + this->complete_with_units(this->string_builder, this->key_name); + this->string_builder += "-up"; + + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); break; } @@ -401,10 +436,10 @@ void ekg::handler::input::poll_event() { } case ekg::io::event_type::mouse_wheel: { - std::string string_builder {}; - this->complete_with_units(string_builder, "mouse-wheel"); - this->set_input_state(string_builder, true); - this->input_released_list.push_back(string_builder); + this->string_builder.clear(); + this->complete_with_units(this->string_builder, "mouse-wheel"); + this->set_input_state(this->string_builder, true); + this->input_released_list.push_back(this->string_builder); this->input.was_wheel = true; this->set_input_state("mouse-wheel-up", platform_event.mouse_wheel_y > 0); @@ -428,9 +463,37 @@ void ekg::handler::input::poll_event() { wheel_precise_interval = wheel_precise_interval + (static_cast(wheel_precise_interval > 0.99) * 0.5f); wheel_precise_interval = ekg::clamp_min(wheel_precise_interval, 0.2f); - this->input.interact.z = platform_event.mouse_wheel_precise_x * wheel_precise_interval; - this->input.interact.w = platform_event.mouse_wheel_precise_y * wheel_precise_interval; - + bool cooldown {ekg::reach(this->cooldown_wheel, 350)}; + ekg::reset(this->cooldown_wheel); + + if ( + !cooldown + && + ( + (platform_event.mouse_wheel_precise_x < 0 && this->input.interact.z < 0) + || + (platform_event.mouse_wheel_precise_x > 0 && this->input.interact.z > 0) + ) + ) { + this->input.interact.z += platform_event.mouse_wheel_precise_x * wheel_precise_interval * 0.5f; + } else { + this->input.interact.z = platform_event.mouse_wheel_precise_x * wheel_precise_interval; + } + + if ( + !cooldown + && + ( + (platform_event.mouse_wheel_precise_y < 0 && this->input.interact.w < 0) + || + (platform_event.mouse_wheel_precise_y > 0 && this->input.interact.w > 0) + ) + ) { + this->input.interact.w += platform_event.mouse_wheel_precise_y * wheel_precise_interval * 0.5f; + } else { + this->input.interact.w = platform_event.mouse_wheel_precise_y * wheel_precise_interval; + } + ekg::reset(this->last_time_wheel_was_fired); break; } @@ -521,7 +584,7 @@ void ekg::handler::input::update() { #endif ekg::reset_if_reach(this->input.ui_timing, 1000); - ekg::timing_t::second = this->input.ui_timing.elapsed_ticks; + ekg::timing_t::second = this->input.ui_timing.current_ticks; } void ekg::handler::input::insert_input_bind( @@ -607,6 +670,10 @@ void ekg::handler::input::set_input_state( ) { this->input_map[key.data()] = state; + if (!this->input_bind_listener_function) { + this->input_bind_listener_function(key, state); + } + for (bool *p_address : this->input_bindings_map[key.data()]) { if (!p_address) { continue; @@ -669,3 +736,9 @@ bool ekg::handler::input::get_input_bind_state( ) { return this->input_bind_map[tag.data()].state; } + +void ekg::handler::input::set_input_bind_listener( + ekg::input_bind_function_t input_bind_listener_function +) { + this->input_bind_listener_function = input_bind_listener_function; +} diff --git a/src/handler/theme/handler.cpp b/src/handler/theme/handler.cpp index 26c6deeb..b458724b 100644 --- a/src/handler/theme/handler.cpp +++ b/src/handler/theme/handler.cpp @@ -30,7 +30,7 @@ void ekg::handler::theme::init() { ekg::theme_t light_pinky_theme { .tag = "light-pinky", .author = "Rina Wilk", - .description = "wasted love of my life, no love no more love no more" + .description = "amo vc amo vc" }; light_pinky_theme.layout_offset = 2.0f; @@ -76,13 +76,20 @@ void ekg::handler::theme::init() { light_pinky_theme.popup_color_scheme.outline = {50, 50, 50, 100}; light_pinky_theme.popup_color_scheme.popup_mode = true; + light_pinky_theme.textbox_color_scheme.background = {242, 242, 242, 255}; + light_pinky_theme.textbox_color_scheme.outline = {190, 190, 190, 100}; + light_pinky_theme.textbox_color_scheme.text_foreground = {141, 141, 141, 255}; + light_pinky_theme.textbox_color_scheme.text_cursor_foreground = {141, 141, 141, 255}; + light_pinky_theme.textbox_color_scheme.text_select_foreground = {245, 169, 184, 50}; + light_pinky_theme.textbox_color_scheme.text_select_outline = {245, 169, 184, 100}; + this->registry(light_pinky_theme.tag) = light_pinky_theme; this->set_current_theme(light_pinky_theme.tag); ekg::theme_t black_light_pinky_theme { .tag = "black-light-pinky", .author = "Rina Wilk", - .description = "loved a shitty person God save me" + .description = "misses" }; black_light_pinky_theme.layout_offset = 2.0f; @@ -127,8 +134,74 @@ void ekg::handler::theme::init() { black_light_pinky_theme.popup_color_scheme = black_light_pinky_theme.frame_color_scheme; black_light_pinky_theme.popup_color_scheme.popup_mode = true; + black_light_pinky_theme.textbox_color_scheme.background = {242, 242, 242, 255}; + black_light_pinky_theme.textbox_color_scheme.outline = {190, 190, 190, 100}; + black_light_pinky_theme.textbox_color_scheme.text_foreground = {141, 141, 141, 255}; + black_light_pinky_theme.textbox_color_scheme.text_cursor_foreground = {141, 141, 141, 255}; + black_light_pinky_theme.textbox_color_scheme.text_select_foreground = {245, 169, 184, 50}; + black_light_pinky_theme.textbox_color_scheme.text_select_outline = {245, 169, 184, 100}; + this->registry(black_light_pinky_theme.tag) = black_light_pinky_theme; //this->set_current_theme(black_light_pinky_theme.tag); + + ekg::theme_t black_pinky { + .tag = "black-pinky", + .author = "weasted", + .description = "colors for show colors" + }; + + black_pinky.layout_offset = 2.0f; + black_pinky.layout_margin_thickness = 2; + black_pinky.frame_color_scheme.background = {242, 242, 242, 255}; + black_pinky.frame_color_scheme.highlight = {242, 242, 242, 0}; + black_pinky.frame_color_scheme.outline = {190, 190, 190, 0}; + black_pinky.frame_color_scheme.active = {242, 242, 242, 0}; + black_pinky.frame_color_scheme.focused_background = {242, 242, 242, 0}; + black_pinky.frame_color_scheme.focused_outline = {242, 242, 242, 0}; + black_pinky.frame_color_scheme.warning_outline = {242, 242, 0, 100}; + black_pinky.frame_color_scheme.actions_margin_pixel_thickness = 18; + + black_pinky.button_color_scheme.text_foreground = {141, 141, 141, 255}; + black_pinky.button_color_scheme.background = {204, 204, 204, 50}; + black_pinky.button_color_scheme.active = {245, 169, 184, 100}; + black_pinky.button_color_scheme.outline = {202, 207, 222, 0}; + black_pinky.button_color_scheme.highlight = {245, 169, 184, 50}; + black_pinky.button_color_scheme.text_foreground = {141, 141, 141, 255}; + black_pinky.button_color_scheme.box_outline = black_pinky.button_color_scheme.outline; + black_pinky.button_color_scheme.box_active = {245, 169, 184, 200}; + black_pinky.button_color_scheme.box_highlight = {245, 169, 184, 50}; + black_pinky.button_color_scheme.box_background = {202, 207, 222, 100}; + + black_pinky.label_color_scheme.background = {204, 204, 204, 0}; + black_pinky.label_color_scheme.outline = {202, 207, 222, 0}; + black_pinky.label_color_scheme.text_foreground = {141, 141, 141, 255}; + + black_pinky.scrollbar_color_scheme.background = {204, 204, 204, 30}; + black_pinky.scrollbar_color_scheme.outline = {204, 204, 204, 0}; + black_pinky.scrollbar_color_scheme.bar_background = {245, 169, 184, 100}; + black_pinky.scrollbar_color_scheme.bar_highlight = {245, 169, 184, 50}; + black_pinky.scrollbar_color_scheme.bar_active = {245, 169, 184, 100}; + + black_pinky.slider_color_scheme.outline = {204, 204, 204, 0}; + black_pinky.slider_color_scheme.background = {204, 204, 204, 30}; + black_pinky.slider_color_scheme.bar_background = {204, 204, 204, 50}; + black_pinky.slider_color_scheme.bar_highlight = {245, 169, 184, 50}; + black_pinky.slider_color_scheme.bar_active = {245, 169, 184, 100}; + black_pinky.slider_color_scheme.text_foreground = {141, 141, 141, 255}; + + black_pinky.popup_color_scheme = black_pinky.frame_color_scheme; + black_pinky.popup_color_scheme.outline = {50, 50, 50, 100}; + black_pinky.popup_color_scheme.popup_mode = true; + + black_pinky.textbox_color_scheme.background = {0, 0, 0, 255}; + black_pinky.textbox_color_scheme.outline = {190, 190, 190, 100}; + black_pinky.textbox_color_scheme.text_foreground = {242, 242, 242, 255}; + black_pinky.textbox_color_scheme.text_cursor_foreground = {141, 141, 141, 255}; + black_pinky.textbox_color_scheme.text_select_foreground = {245, 169, 184, 50}; + black_pinky.textbox_color_scheme.text_select_outline = {245, 169, 184, 100}; + + this->registry(black_pinky.tag) = black_pinky; + this->set_current_theme(black_pinky.tag); } void ekg::handler::theme::quit() { diff --git a/src/io/utf.cpp b/src/io/utf.cpp index ad51b8eb..44fc7910 100644 --- a/src/io/utf.cpp +++ b/src/io/utf.cpp @@ -27,26 +27,269 @@ #include #include +bool ekg::utf8_is_last_index( + size_t byte_index, + size_t last_char_bytes, + size_t string_bytes_size +) { + return byte_index == (string_bytes_size - last_char_bytes); +} + +bool ekg::utf8_align_utf_pos_by_byte_pos( + std::string &string, + size_t &unaligned_byte_pos, + size_t &unaligned_utf_pos, + size_t next_byte_pos +) { + if (string.empty() || next_byte_pos == unaligned_byte_pos) { + return false; + } + + ekg::dock axis { + next_byte_pos > unaligned_byte_pos + ? ekg::dock::left : ekg::dock::right + }; + + size_t byte_pos {}; + size_t utf_pos_count {}; + size_t utf_sequence_size {}; + + switch (axis) { + case ekg::dock::left: + byte_pos = unaligned_byte_pos; + while (byte_pos < next_byte_pos) { + char &c8 {string.at(byte_pos)}; + utf_sequence_size = 1; + utf_sequence_size += 1 * ((c8 & 0xE0) == 0xC0); + utf_sequence_size += 2 * ((c8 & 0xF0) == 0xE0); + utf_sequence_size += 3 * ((c8 & 0xF8) == 0xF0); + byte_pos += utf_sequence_size; + unaligned_utf_pos++; + } + + unaligned_byte_pos = next_byte_pos; + + break; + case ekg::dock::right: + byte_pos = next_byte_pos; + while (byte_pos < unaligned_byte_pos) { + char &c8 {string.at(byte_pos)}; + utf_sequence_size = 1; + utf_sequence_size += 1 * ((c8 & 0xE0) == 0xC0); + utf_sequence_size += 2 * ((c8 & 0xF0) == 0xE0); + utf_sequence_size += 3 * ((c8 & 0xF8) == 0xF0); + byte_pos += utf_sequence_size; + unaligned_utf_pos--; + } + + unaligned_byte_pos = next_byte_pos; + + break; + default: + break; + } + + return true; +} + +bool ekg::utf8_nearest_regex_group_matched_position( + const std::string::const_iterator &begin, + const std::string::const_iterator &end, + size_t &nearest_byte_pos, + std::regex &pattern, + const ekg::dock &dock +) { + std::sregex_iterator sregex_it(begin, end, pattern); + std::sregex_iterator sregex_it_end {}; + + if (sregex_it == sregex_it_end) { + return false; + } + + std::smatch match {}; + size_t groups_size {}; + + bool any_matched {}; + bool is_right {dock == ekg::dock::right}; + + for (; sregex_it != sregex_it_end; sregex_it++) { + match = *sregex_it; + groups_size = match.size(); + for (size_t i {1}; i < groups_size; i++) { + if (match[i].matched) { + any_matched = true; + nearest_byte_pos = match.position(i); + if (is_right) return true; + } + } + } + + return any_matched; +} + +void ekg::utf8_sequence( + uint8_t &uc8, + char32_t &c32, + std::string_view utf8_str, + size_t &it +) { + size_t size {utf8_str.size()}; + + if (size >= 1 && uc8 <= 0x7F) { + c32 = static_cast(uc8); + } else if ((size >= 2 && it + 1 < size) && (uc8 & 0xE0) == 0xC0) { + c32 = uc8 & 0x1F; + c32 = (c32 << 6) | (utf8_str.at(++it) & 0x3F); + } else if ((size >= 3 && it + 2 < size) && (uc8 & 0xF0) == 0xE0) { + c32 = uc8 & 0x0F; + c32 = (c32 << 6) | (utf8_str.at(++it) & 0x3F); + c32 = (c32 << 6) | (utf8_str.at(++it) & 0x3F); + } else if ((size >= 4 && it + 3 < size) && (uc8 & 0xF8) == 0xF0) { + c32 = uc8 & 0x07; + c32 = (c32 << 6) | (utf8_str.at(++it) & 0x3F); + c32 = (c32 << 6) | (utf8_str.at(++it) & 0x3F); + c32 = (c32 << 6) | (utf8_str.at(++it) & 0x3F); + } +} + +bool ekg::utf8_check_last_char_byte_sequence( + std::string &utf8_str, + size_t &bytes +) { + if (utf8_str.empty()) { + return false; + } + + size_t byte {utf8_str.size()}; + + /** + * Reversing find by byte pos is efficient, + * since we want to reduce the index utf8 check. + * + * It cost many CPU iterations instructions for calculating + * the string size by utf8 sequence. + **/ + + return ( + byte + && + ( + ( + byte >= (bytes = 4) + && + (static_cast(utf8_str.at(byte - 4)) & 0xF8) == 0xF0 + ) + || + ( + byte >= (bytes = 3) + && + (static_cast(utf8_str.at(byte - 3)) & 0xF0) == 0xE0 + ) + || + ( + byte >= (bytes = 2) + && + (static_cast(utf8_str.at(byte - 2)) & 0xE0) == 0xC0) + || + ( + byte >= (bytes = 1) + && + (static_cast(utf8_str.at(byte - 1))) <= 0xE0 + ) + ) + ); +} + +bool ekg::utf8_find_utf_pos_by_byte_pos( + std::string &string, + size_t byte_pos, + size_t &utf_pos +) { + size_t utf_sequence_size {}; + size_t string_size {string.size()}; + size_t utf_pos_count {}; + for (size_t it {}; it < string_size; it++) { + if (it == byte_pos) { + utf_pos = utf_pos_count; + return true; + } + + char &c8 {string.at(it)}; + utf_sequence_size = 0; + utf_sequence_size += ((c8 & 0xE0) == 0xC0); + utf_sequence_size += 2 * ((c8 & 0xF0) == 0xE0); + utf_sequence_size += 3 * ((c8 & 0xF8) == 0xF0); + + if (it > byte_pos && it + utf_sequence_size < byte_pos) { + return false; + } + + it += utf_sequence_size; + utf_pos_count++; + + if (it == byte_pos) { + utf_pos = utf_pos_count; + return true; + } + } + + return false; +} + +bool ekg::utf8_find_byte_pos_by_utf_pos( + std::string &string, + size_t utf_pos, + size_t &byte_pos +) { + size_t utf_sequence_size {}; + size_t string_size {string.size()}; + size_t utf_pos_count {}; + + size_t it {}; + for (; it < string_size; it++) { + if (utf_pos_count == utf_pos) { + byte_pos = it; + return true; + } + + char &c8 {string.at(it)}; + utf_sequence_size = 0; + utf_sequence_size += ((c8 & 0xE0) == 0xC0); + utf_sequence_size += 2 * ((c8 & 0xF0) == 0xE0); + utf_sequence_size += 3 * ((c8 & 0xF8) == 0xF0); + + it += utf_sequence_size; + utf_pos_count++; + } + + if (utf_pos_count == utf_pos) { + byte_pos = it; + return true; + } + + return false; +} + uint64_t ekg::utf8_check_sequence( - uint8_t &char8, + uint8_t &c8, char32_t &char32, std::string &utf_string, std::string_view string, uint64_t index ) { - if (char8 <= 0x7F) { - utf_string = char8; - char32 = static_cast(char8); + if (c8 <= 0x7F) { + utf_string = c8; + char32 = static_cast(c8); return 0; - } else if ((char8 & 0xE0) == 0xC0) { + } else if ((c8 & 0xE0) == 0xC0) { utf_string = string.substr(index, 2); char32 = ekg::utf8_to_utf32(utf_string); return 1; - } else if ((char8 & 0xF0) == 0xE0) { + } else if ((c8 & 0xF0) == 0xE0) { utf_string = string.substr(index, 3); char32 = ekg::utf8_to_utf32(utf_string); return 2; - } else if ((char8 & 0xF8) == 0xF0) { + } else if ((c8 & 0xF8) == 0xF0) { utf_string = string.substr(index, 4); char32 = ekg::utf8_to_utf32(utf_string); return 3; @@ -85,19 +328,19 @@ char32_t ekg::utf8_to_utf32(std::string_view string) { char32_t char32 {}; uint64_t it {}; - uint8_t char8 {static_cast(string.at(0))}; + uint8_t c8 {static_cast(string.at(0))}; - if (char8 <= 0x7F) { - char32 = char8; - } else if (char8 <= 0xDF) { - char32 = char8 & 0x1F; + if (c8 <= 0x7F) { + char32 = c8; + } else if (c8 <= 0xDF) { + char32 = c8 & 0x1F; char32 = (char32 << 6) | (string.at(++it) & 0x3F); - } else if (char8 <= 0xEF) { - char32 = char8 & 0x0F; + } else if (c8 <= 0xEF) { + char32 = c8 & 0x0F; char32 = (char32 << 6) | (string.at(++it) & 0x3F); char32 = (char32 << 6) | (string.at(++it) & 0x3F); } else { - char32 = char8 & 0x07; + char32 = c8 & 0x07; char32 = (char32 << 6) | (string.at(++it) & 0x3F); char32 = (char32 << 6) | (string.at(++it) & 0x3F); char32 = (char32 << 6) | (string.at(++it) & 0x3F); @@ -112,21 +355,21 @@ uint64_t ekg::utf8_length(std::string_view utf_string) { } uint64_t string_size {}; - uint8_t char8 {}; + uint8_t c8 {}; for (uint64_t it {}; it < utf_string.size(); it++) { - char8 = static_cast(utf_string.at(it)); - if (char8 == '\n' || char8 == '\r') { + c8 = static_cast(utf_string.at(it)); + if (c8 == '\n' || c8 == '\r') { continue; - } else if (char8 <= 0x7F) { + } else if (c8 <= 0x7F) { string_size++; - } else if ((char8 & 0xE0) == 0xC0) { + } else if ((c8 & 0xE0) == 0xC0) { string_size++; it++; - } else if ((char8 & 0xF0) == 0xE0) { + } else if ((c8 & 0xF0) == 0xE0) { string_size++; it += 2; - } else if ((char8 & 0xF8) == 0xF0) { + } else if ((c8 & 0xF8) == 0xF0) { string_size++; it += 3; } @@ -149,7 +392,7 @@ std::string ekg::utf8_substr(std::string_view string, uint64_t offset, uint64_t uint64_t index {}; uint64_t utf_sequence_size {}; uint64_t begin {UINT64_MAX}; - uint8_t char8 {}; + uint8_t c8 {}; bool at_last_index {}; /* @@ -158,12 +401,12 @@ std::string ekg::utf8_substr(std::string_view string, uint64_t offset, uint64_t */ while (index < string_size) { - char8 = static_cast(string.at(index)); + c8 = static_cast(string.at(index)); utf_sequence_size = 1; - utf_sequence_size += ((char8 & 0xE0) == 0xC0); - utf_sequence_size += 2 * ((char8 & 0xF0) == 0xE0); - utf_sequence_size += 3 * ((char8 & 0xF8) == 0xF0); + utf_sequence_size += ((c8 & 0xE0) == 0xC0); + utf_sequence_size += 2 * ((c8 & 0xF0) == 0xE0); + utf_sequence_size += 3 * ((c8 & 0xF8) == 0xF0); at_last_index = index + utf_sequence_size == string_size; @@ -177,7 +420,7 @@ std::string ekg::utf8_substr(std::string_view string, uint64_t offset, uint64_t if ( /** * OBS: - * If the `offset` parameter is equals to the last UTF-8 string size, + * If the `offset` paramete is equals to the last UTF-8 string size, * then it continue without substring process. **/ (at_last_index && begin != UINT64_MAX && offset != utf_text_size) || @@ -191,54 +434,507 @@ std::string ekg::utf8_substr(std::string_view string, uint64_t offset, uint64_t return ""; } -void ekg::utf8_split_new_line(std::string_view string, std::vector &utf8_read) { - if (string.empty()) { - return; +size_t ekg::utf8_split_endings( + std::string_view line, + std::vector &splitted +) { + if (line.empty()) { + return 0; } - /* if the first element is empty, then the decode must start from it */ - if (utf8_read.size() == 1 && utf8_read.at(0).empty()) { - utf8_read.clear(); + size_t new_lines_count {}; + size_t str_size {line.size()}; + size_t start_pos {}; + size_t pos {}; + + while ((pos = line.find('\n', start_pos)) != std::string::npos) { + std::string &split {splitted.emplace_back()}; + split = line.substr(start_pos, pos - start_pos); + if (split.back() == '\r') split.pop_back(); + start_pos = pos + 1; + new_lines_count++; + } + + if (start_pos < str_size) { + splitted.emplace_back() = line.substr(start_pos, str_size); + new_lines_count++; } - uint64_t index {}; - uint64_t start_index {}; + return new_lines_count; +} - while ((index = string.find('\n', start_index)) != std::string_view::npos) { - utf8_read.emplace_back( - string.substr( - start_index, - (index - start_index) - (index > 0 && string.at(index - 1) == '\r') - ) +void ekg::utf8_concat( + std::string &string, + const ekg::vec4_t &stride, + std::string &concated +) { + std::string a { + ekg::utf8_substr(string, stride.x, stride.y) + }; + + std::string b { + ekg::utf8_substr(string, stride.z, stride.w) + }; + + concated.clear(); + if (!a.empty() || !b.empty()) { + concated = a + b; + } +} + +void ekg::text::swizzle( + std::list::iterator chunk_it, + size_t line_index, + std::vector &to_swizzle, + bool skip_first_line +) { + ekg_log_low_level("swizzle...") + + ekg::io::chunk_t &chunk {*chunk_it}; + + this->was_audited = true; + bool is_empty {to_swizzle.empty()}; + + if (skip_first_line) { + chunk.at(line_index) = is_empty ? "" : to_swizzle.at(0); + } + + if (is_empty) { + to_swizzle.emplace_back(); + } + + chunk.insert( + chunk.begin() + line_index + 1, + to_swizzle.begin() + skip_first_line, + to_swizzle.end() + ); + + if (chunk.size() <= this->lines_per_chunk_limit) { + return; + } + + std::vector to_swizzle_chunk { + chunk.begin() + this->lines_per_chunk_limit, + chunk.end() + }; + + chunk.erase( + chunk.begin() + this->lines_per_chunk_limit, + chunk.end() + ); + + size_t to_swizzle_chunk_size {to_swizzle_chunk.size()}; + + if (++chunk_it == this->loaded_chunks.end()) { + size_t newly_chunks { + to_swizzle_chunk_size / this->lines_per_chunk_limit + }; + + ekg_log_low_level("insert at end if necessary") + + for (size_t jt {}; jt < newly_chunks; jt++) { + chunk_it = this->loaded_chunks.insert( + chunk_it, + ekg::io::chunk_t { + to_swizzle_chunk.begin() + (this->lines_per_chunk_limit * (jt + 0)), + to_swizzle_chunk.begin() + (this->lines_per_chunk_limit * (jt + 1)) + } + ); + } + + size_t rest {to_swizzle_chunk_size - (this->lines_per_chunk_limit * newly_chunks)}; + if (rest > 0) { + chunk_it = this->loaded_chunks.insert( + chunk_it, + ekg::io::chunk_t { + to_swizzle_chunk.begin() + (this->lines_per_chunk_limit * (newly_chunks + 0)), + to_swizzle_chunk.end() + } + ); + } + + return; + } + + ekg::io::chunk_t &next_chunk { + *chunk_it + }; + + if (to_swizzle_chunk_size + next_chunk.size() > this->lines_per_chunk_limit) { + chunk_it = this->loaded_chunks.insert(chunk_it, {}); + ekg_log_low_level("insert if next chunk too large for this") + this->swizzle( + chunk_it, + 0, + to_swizzle_chunk, + false ); + return; + } + + next_chunk.insert( + next_chunk.begin(), + to_swizzle_chunk.begin(), + to_swizzle_chunk.end() + ); + + ekg_log_low_level("insert if next chunk allows") +} + +size_t ekg::text::set(size_t index, std::string_view line) { + ekg::io::chunk_t split_endings {}; + return this->set(index, line, split_endings); +} + +size_t ekg::text::set(size_t index, std::string_view line, ekg::io::chunk_t &split_endings) { + size_t current_lines {}; + size_t previous_lines {}; + size_t chunk_size {}; + + ekg::utf8_split_endings(line, split_endings); + + bool ok {}; + for (std::list::iterator it {this->loaded_chunks.begin()}; it != this->loaded_chunks.end(); it++) { + ekg::io::chunk_t &chunk {*it}; + + previous_lines = current_lines; + current_lines += (chunk_size = chunk.size()); + + if ( + !ok + && + index < current_lines + && + (index - previous_lines) < chunk_size + ) { + this->swizzle(it, (index - previous_lines), split_endings, true); + this->was_audited = true; + ok = true; + } + } + + if (!ok) throw std::out_of_range("ekg::text::set -> lines length: " + std::to_string(current_lines)); + this->total_lines = current_lines; + return split_endings.size(); +} + +std::string ekg::text::at(size_t index) { + size_t chunks_size {this->loaded_chunks.size()}; + size_t current_lines {}; + size_t previous_lines {}; + size_t chunk_size {}; + + for (std::list::iterator it {this->loaded_chunks.begin()}; it != this->loaded_chunks.end(); it++) { + ekg::io::chunk_t &chunk {*it}; + previous_lines = current_lines; + current_lines += (chunk_size = chunk.size()); + + if ( + index < current_lines + && + (index - previous_lines) < chunk_size + ) { + return chunk.at(index - previous_lines); + } + } + + throw std::out_of_range("ekg::text::at -> lines length: " + std::to_string(current_lines)); +} + +void ekg::text::insert( + size_t index, + const ekg::io::chunk_t &to_insert_chunk +) { + if (this->loaded_chunks.empty()) { + if (index == 0) { + this->loaded_chunks.emplace_back(); + this->insert(index, to_insert_chunk); + } + return; + } + + size_t previous_lines {}; + size_t chunk_size {}; + size_t current_lines {}; + size_t to_insert_chunk_size {to_insert_chunk.size()}; + + ekg::io::chunk_t splitted {}; + for (size_t it {}; it < to_insert_chunk_size; it++) { + const std::string &lines {to_insert_chunk.at(it)}; + ekg::utf8_split_endings(lines, splitted); + } + + size_t b {}; + + size_t total_of_chunks {this->loaded_chunks.size()}; + for (std::list::iterator it {this->loaded_chunks.begin()}; it != this->loaded_chunks.end(); it++) { + ekg::io::chunk_t &chunk {*it}; + + previous_lines = current_lines; + current_lines += (chunk_size = chunk.size()); + + if ( + index < current_lines + && + (index - previous_lines) < chunk_size + ) { + this->swizzle(it, (index - previous_lines), splitted, false); + this->was_audited = true; + this->should_count = true; + this->total_lines += splitted.size(); + return; + } - start_index = index + 1; + b++; } - if (!string.empty() && - !( - string = string.substr(start_index, string.find('\0', start_index)) - ).empty() + throw std::out_of_range("ekg::text::insert -> lines length: " + std::to_string(current_lines)); +} + +void ekg::text::insert( + size_t index, + std::string_view line +) { + ekg::io::chunk_t chunk {}; + chunk.push_back(std::string(line)); + this->insert(index, chunk); +} + +std::string ekg::text::read( + ekg::vec2_t &begin, + ekg::vec2_t &end +) { + size_t previous_lines {}; + size_t chunk_size {}; + size_t current_lines {}; + size_t total_of_chunks {this->loaded_chunks.size()}; + size_t line_index {}; + size_t cut_length {end.y - begin.y}; + size_t cut_count {}; + size_t lines {}; + + size_t i {}; + bool oka_begin {}; + bool oka_end {}; + + std::string builder {}; + for (std::list::iterator it {this->loaded_chunks.begin()}; it != this->loaded_chunks.end(); it++) { + ekg::io::chunk_t &chunk {*it}; + + previous_lines = current_lines; + current_lines += (chunk_size = chunk.size()); + + if ( + begin.y > current_lines ) { - // `meow\n\0`, so the decode wont emplace a empty string. - utf8_read.emplace_back(string); + continue; + } + + i = 0; + if (!oka_begin) { + i = begin.y - previous_lines; + oka_begin = true; + } + + if (end.y < previous_lines) { + return builder; + } + + previous_lines += i; + + for (; i < chunk_size; i++) { + std::string &line {chunk.at(i)}; + + if ( + previous_lines == begin.y + && + previous_lines == end.y + ) { + builder += ekg::utf8_substr(line, begin.x, end.x - begin.x); + return builder; + } + + if ( + previous_lines == begin.y + ) { + builder += ekg::utf8_substr(line, begin.x, UINT32_MAX) + EKG_EOF_SYSTEM; + } else if ( + previous_lines == end.y + ) { + builder += ekg::utf8_substr(line, 0, end.x); + return builder; + } else { + builder += line + EKG_EOF_SYSTEM; + } + + previous_lines++; + } } } -bool ekg::utf8_split( - std::vector &splitted, - const std::string &string, - char find_char +void ekg::text::erase( + size_t begin, + size_t end ) { - std::stringstream ss(string); - std::string find_string {}; + if (this->loaded_chunks.empty()) { + throw std::out_of_range("ekg::text::erase -> text empty"); + return; + } - bool found_flag {}; + if (begin > this->total_lines) { + throw std::out_of_range("ekg::text::erase: " + std::to_string(begin) + ", " + std::to_string(this->total_lines)); + return; + } - while (std::getline(ss, find_string, find_char)) { - splitted.push_back(find_string); - found_flag = true; + if (end < begin) { + throw std::out_of_range("ekg::text::erase: " + std::to_string(end) + " end is > than " + std::to_string(begin) + " begin"); + return; } - return found_flag; + this->total_lines -= end - begin; + + this->was_audited = true; + this->should_count = true; + + size_t previous_lines {}; + size_t chunk_size {}; + size_t lines {}; + size_t remains_lines {}; + + bool empty_chunk {}; + bool goto_next_chunk {}; + + for (std::list::iterator it {this->loaded_chunks.begin()}; it != this->loaded_chunks.end(); it++) { + ekg::io::chunk_t &chunk {*it}; + + previous_lines = lines; + lines += (chunk_size = chunk.size()); + + if (begin < lines) { + remains_lines = end - begin; + begin = begin - previous_lines; + + ekg_log_low_level(remains_lines << " x " << begin) + + while (remains_lines != 0 && it != this->loaded_chunks.end()) { + ekg::io::chunk_t &chunk {*it}; + chunk_size = chunk.size(); + + if (chunk_size != 0) { + lines = (remains_lines + begin) >= (chunk_size) ? (chunk_size - begin) : (remains_lines); + + ekg_log_low_level(remains_lines << " " << begin << " | " << lines) + + chunk.erase( + chunk.begin() + begin, // 5 + chunk.begin() + begin + remains_lines // 5 + 1 = 6 + ); + + ekg_log_low_level(remains_lines << " vv " << lines) + remains_lines *= lines != 0; + remains_lines -= lines; + + begin = 0; + } + + if (chunk.empty()) { + it = this->loaded_chunks.erase(it); + } + + it++; + } + + break; + } + } +} + +void ekg::text::push_back(std::string_view line) { + ekg::io::chunk_t splitted {}; + ekg::utf8_split_endings(line, splitted); + + this->should_count = true; + this->total_lines += splitted.size(); + + if (this->loaded_chunks.empty()) { + this->loaded_chunks.emplace_back().emplace_back(); + this->swizzle(this->loaded_chunks.begin(), 0, splitted, true); + return; + } + + std::list::iterator last_it {--this->loaded_chunks.end()}; + ekg::io::chunk_t &last_chunk { + *last_it + }; + + this->swizzle( + last_it, + last_chunk.size() - !last_chunk.empty(), + splitted, + false + ); +} + +std::list &ekg::text::chunks_data() { + return this->loaded_chunks; +} + +size_t ekg::text::length_of_chunks() { + return this->loaded_chunks.size(); +} + +size_t ekg::text::length_of_lines(bool force) { + if (force) { + this->total_lines = 0; + for (ekg::io::chunk_t &chunk : this->loaded_chunks) { + this->total_lines += chunk.size(); + } + } + + return this->total_lines; +} + +size_t ekg::text::length_of_chars() { + if (this->should_count) { + this->total_chars = 0; + for (ekg::io::chunk_t &chunk : this->loaded_chunks) { + for (std::string &lines : chunk) { + this->total_chars += ekg::utf8_length(lines); + } + } + + this->should_count = false; + } + + return this->total_chars; +} + +bool ekg::text::audited() { + this->was_audited = this->was_audited || (this->prev_lines != this->total_lines); + this->prev_lines = this->total_lines; + return this->was_audited; +} + +void ekg::text::unset_audited() { + this->was_audited = false; +} + +void ekg::text::set_lines_per_chunk_limit(size_t limit) { + this->lines_per_chunk_limit = limit; +} + +size_t ekg::text::get_lines_per_chunk_limit() { + return this->lines_per_chunk_limit; +} + +void ekg::text::sanitize() { + if (this->loaded_chunks.empty()) { + this->loaded_chunks.emplace_back(); + } + + ekg::io::chunk_t &chunk {*this->loaded_chunks.begin()}; + + if (chunk.empty()) { + chunk.emplace_back(); + } } diff --git a/src/platform/sdl/sdl2.cpp b/src/platform/sdl/sdl2.cpp index be4ad831..2b46ade3 100644 --- a/src/platform/sdl/sdl2.cpp +++ b/src/platform/sdl/sdl2.cpp @@ -46,15 +46,15 @@ ekg::sdl2::sdl2( SDL_SetWindowSize(this->p_sdl_win, w++, h++); } -void ekg::sdl2::set_clipboard_text(const char *p_text) { - SDL_SetClipboardText(p_text); +void ekg::sdl2::set_clipboard_text(std::string_view text) { + SDL_SetClipboardText(text.data()); } bool ekg::sdl2::has_clipboard_text() { return SDL_HasClipboardText(); } -const char *ekg::sdl2::get_clipboard_text() { +std::string_view ekg::sdl2::get_clipboard_text() { return SDL_GetClipboardText(); } diff --git a/src/ui/button/widget.cpp b/src/ui/button/widget.cpp index 298c624f..2503d923 100644 --- a/src/ui/button/widget.cpp +++ b/src/ui/button/widget.cpp @@ -184,7 +184,7 @@ void ekg::ui::event( && check.states.is_highlight && - ekg::fire("button-active") + ekg::fired("button-active") ) { ekg_action( check.actions, diff --git a/src/ui/frame/widget.cpp b/src/ui/frame/widget.cpp index 741b8b99..cb49928d 100644 --- a/src/ui/frame/widget.cpp +++ b/src/ui/frame/widget.cpp @@ -96,9 +96,9 @@ void ekg::ui::event( input.was_pressed && ( - (frame.drag != ekg::dock::none && ekg::fire("frame-drag")) + (frame.drag != ekg::dock::none && ekg::fired("frame-drag")) || - (frame.resize != ekg::dock::none && ekg::fire("frame-resize")) + (frame.resize != ekg::dock::none && ekg::fired("frame-resize")) ) ) { @@ -199,7 +199,7 @@ void ekg::ui::event( } } - ekg::clamp_rect_by_square( + new_rect = ekg::clamp_rect_by_square( new_rect, ekg::dpi.min_sizes ); diff --git a/src/ui/scrollbar/widget.cpp b/src/ui/scrollbar/widget.cpp index 0c261932..d5ae4dae 100644 --- a/src/ui/scrollbar/widget.cpp +++ b/src/ui/scrollbar/widget.cpp @@ -187,7 +187,6 @@ void ekg::ui::reload( parent.scroll.nearest_scroll_bar_thickness = scrollbar.color_scheme.bar_thickness; } - void ekg::ui::event( ekg::property_t &property, ekg::scrollbar_t &scrollbar, @@ -207,7 +206,7 @@ void ekg::ui::event( input.has_motion || input.was_wheel - ) { + ) { bool is_visible { ekg::rect_collide_vec2(property.widget.rect_scissor, interact) }; @@ -217,7 +216,7 @@ void ekg::ui::event( && (property.scroll.is_enabled.x || property.scroll.is_enabled.y) && - (ekg::fire("scrollbar-scroll") || ekg::fire("scrollbar-scroll-horizontal")) + (ekg::fired("scrollbar-scroll") || ekg::fired("scrollbar-scroll-horizontal")) ); ekg::rect_t h_bar {scrollbar.widget.rect_horizontal}; @@ -238,6 +237,7 @@ void ekg::ui::event( ekg::rect_precise_collide_vec2(v_bar, interact) ); + property.states.is_focused = is_visible; property.states.is_hovering = ( property.states.is_active || @@ -271,7 +271,7 @@ void ekg::ui::event( ); ekg::input_info_t &input {ekg::p_core->handler_input.input}; - bool is_scroll_fired {ekg::fire("scrollbar-scroll")}; + bool is_scroll_fired {ekg::fired("scrollbar-scroll")}; property.scroll.is_scrolling.x = false; property.scroll.is_scrolling.y = false; @@ -296,7 +296,7 @@ void ekg::ui::event( } #else bool is_scroll_horizontal_fired { - ekg::fire("scrollbar-scroll-horizontal") + ekg::fired("scrollbar-scroll-horizontal") }; if ( @@ -351,7 +351,7 @@ void ekg::ui::event( && input.was_pressed && - ekg::fire("scrollbar-drag") + ekg::fired("scrollbar-drag") ) { ekg::rect_t h_bar {scrollbar.widget.rect_horizontal}; h_bar.x += rect_parent.x; @@ -536,6 +536,22 @@ void ekg::ui::event( parent.widget.rect ); + if (stage == ekg::io::stage::pre) { + /* + property.states.is_hovering = ( + !property.states.is_active + && + ( + property.states.is_focused + || + scrollbar.widget.states_horizontal_bar.is_hovering + || + scrollbar.widget.states_vertical_bar.is_hovering + ) + ); + */ + } + if ( ( property.scroll.is_scrolling.x @@ -638,13 +654,6 @@ void ekg::ui::buffering( ekg::rect_t &rect_parent, ekg::rect_t &rect_parent_scissor ) { - ekg_draw_allocator_assert_scissor( - property.widget.rect_scissor, - rect_parent, - rect_parent_scissor, - ekg::always_parented - ); - scrollbar.widget.rect_horizontal.w = 0.0f; scrollbar.widget.rect_vertical.h = 0.0f; @@ -656,7 +665,7 @@ void ekg::ui::buffering( && !property.scroll.is_enabled.y ) { - ekg_draw_allocator_pass(); + return; } ekg::rect_t bar {}; @@ -835,8 +844,6 @@ void ekg::ui::buffering( ekg::draw::mode::outline, scrollbar.layers[ekg::layer::outline] ); - - ekg_draw_allocator_pass(); } void ekg::ui::buffering( @@ -847,12 +854,21 @@ void ekg::ui::buffering( ekg::query(property.parent_at) }; + ekg_draw_allocator_assert_scissor( + property.widget.rect_scissor, + property_parent.widget.rect, + property_parent.widget.rect_scissor, + ekg::always_parented + ); + ekg::ui::buffering( property, scrollbar, property_parent.widget.rect, property_parent.widget.rect_scissor ); + + ekg_draw_allocator_pass(); } void ekg::ui::unmap( diff --git a/src/ui/textbox/textbox.cpp b/src/ui/textbox/textbox.cpp new file mode 100644 index 00000000..5c55d706 --- /dev/null +++ b/src/ui/textbox/textbox.cpp @@ -0,0 +1,28 @@ +/** + * MIT License + * + * Copyright (c) 2022-2025 Rina Wilk / vokegpu@gmail.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright `notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "ekg/ui/textbox/textbox.hpp" + +ekg::textbox_t ekg::textbox_t::not_found { + .at = ekg::at_t::not_found +}; diff --git a/src/ui/textbox/widget.cpp b/src/ui/textbox/widget.cpp new file mode 100644 index 00000000..0c248296 --- /dev/null +++ b/src/ui/textbox/widget.cpp @@ -0,0 +1,1725 @@ +/** + * MIT License + * + * Copyright (c) 2022-2025 Rina Wilk / vokegpu@gmail.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "ekg/ui/scrollbar/widget.hpp" +#include "ekg/ui/textbox/widget.hpp" +#include "ekg/layout/docknize.hpp" +#include "ekg/io/log.hpp" +#include "ekg/ui/abstract.hpp" +#include "ekg/draw/shape/shape.hpp" +#include "ekg/draw/typography/font.hpp" +#include "ekg/core/context.hpp" +#include "ekg/core/runtime.hpp" +#include "ekg/core/pools.hpp" +#include "ekg/math/floating_point.hpp" + +void ekg::ui::refresh_cursors_pos( + ekg::textbox_t &textbox, + ekg::textbox_t::cursor_t &origin, + const ekg::vec2_t &displacement_a, + const ekg::vec2_t &displacement_b, + const ekg::ui::textbox_operation &operation +) { + for (ekg::textbox_t::cursor_t &cursor : textbox.widget.cursors) { + if (cursor.is_ignored || cursor.is_deleted || cursor.a.y < origin.a.y) continue; + + switch (operation) { + case ekg::ui::textbox_operation::insert_line: + if (cursor.a.y > origin.b.y) { + cursor.a.y += displacement_a.y; + cursor.b.y += displacement_b.y; + } + + if (cursor.a.y == origin.b.y && cursor.a.x > origin.b.x) { + cursor.a.x += displacement_a.x; + cursor.a.y += displacement_a.y; + } + + if (cursor.b.y == origin.b.y && cursor.b.x > origin.b.x) { + cursor.b.x += displacement_b.x; + cursor.b.y += displacement_b.y; + } + break; + case ekg::ui::textbox_operation::insert_inline: + if (cursor.a.y == origin.a.y && cursor.a.x > origin.a.x) { + cursor.a.x += displacement_a.x; + } + + if (cursor.b.y == origin.a.y && cursor.b.x > origin.a.x) { + cursor.b.x += displacement_b.x; + } + break; + case ekg::ui::textbox_operation::insert_multiline: + if (cursor.a.y > origin.a.y) { + cursor.a.y += displacement_a.y; + cursor.b.y += displacement_b.y; + } + + if (cursor.a.y == origin.a.y && cursor.a.x > origin.a.x) { + cursor.a.x = (origin.b.x > origin.a.x) + ? cursor.a.x + (origin.b.x - origin.a.x) + : cursor.a.x - (origin.a.x - origin.b.x); + cursor.a.y += displacement_a.y; + } + + if (cursor.b.y == origin.a.y && cursor.b.x > origin.a.x) { + cursor.b.x = (origin.b.x > origin.a.x) + ? cursor.b.x + (origin.b.x - origin.a.x) + : cursor.b.x - (origin.a.x - origin.b.x); + cursor.b.y += displacement_b.y; + } + + break; + case ekg::ui::textbox_operation::erase_inline: + if (cursor.a.y > origin.b.y) { + cursor.a.y -= displacement_a.y; + cursor.b.y -= displacement_b.y; + } + + if ( + cursor.a.y == origin.a.y + && + cursor.a.x > origin.a.x + ) { + cursor.a.x = cursor.a.x - displacement_a.x; + cursor.a.y -= displacement_a.y; + } + + if ( + cursor.b.y == origin.b.y + && + cursor.b.x > origin.b.x + ) { + cursor.b.x = cursor.b.x - displacement_b.x; + cursor.b.y -= displacement_b.y; + } + break; + case ekg::ui::textbox_operation::erase_multiline: + if (cursor.a.y > origin.b.y) { + cursor.a.y -= displacement_a.y; + cursor.b.y -= displacement_b.y; + } + + if ( + cursor.a.y == origin.b.y + && + cursor.a.x > origin.b.x + ) { + cursor.a.x = (origin.a.x < origin.b.x) ? + cursor.a.x - (origin.b.x - origin.a.x) + : + cursor.a.x + (origin.a.x - origin.b.x); + cursor.a.y -= displacement_a.y; + } + + if ( + cursor.b.y == origin.b.y + && + cursor.b.x > origin.b.x + ) { + cursor.b.x = (origin.a.x < origin.b.x) ? + cursor.b.x - (origin.b.x - origin.a.x) + : + cursor.b.x + (origin.a.x - origin.b.x); + cursor.b.y -= displacement_b.y; + } + break; + } + } +} + +void ekg::ui::refresh_scroll_positions( + ekg::textbox_t &textbox +) { + size_t highest_size {}; + for (ekg::io::chunk_t &chunk : textbox.text.chunks_data()) { + for (std::string &line : chunk) { + highest_size = ekg::max(line.size(), highest_size); + } + }; + + textbox.widget.scrollbar.rect.w = highest_size * (textbox.widget.rect_text_size.h * 0.6f); + textbox.widget.scrollbar.rect.h = textbox.text.length_of_lines() * textbox.widget.rect_text_size.h; +} + +bool ekg::ui::find_cursor( + ekg::textbox_t &textbox, + ekg::vec2_t &index, + ekg::textbox_t::cursor_t &cursor_out +) { + if (cursor_out != ekg::vec2_t(0, 0) && cursor_out >= index && cursor_out <= index) { + return true; + } + + for (ekg::textbox_t::cursor_t &cursor : textbox.widget.cursors) { + if (cursor >= index && cursor <= index) { + cursor_out = cursor; + return true; + } + } + + return false; +} + +bool ekg::ui::find_index_by_interact( + ekg::property_t &property, + ekg::textbox_t &textbox, + ekg::vec2_t &interact, + ekg::vec2_t &index +) { + ekg::rect_t &rect_abs { + ekg::ui::get_abs_rect(property, textbox.rect) + }; + + ekg::draw::font &draw_font { + ekg::draw::get_font_renderer(textbox.font_size) + }; + + ekg::io::font_face_t &text_font_face {draw_font.faces[ekg::io::font_face_type::text]}; + ekg::io::font_face_t &emojis_font_face {draw_font.faces[ekg::io::font_face_type::emojis]}; + ekg::io::font_face_t &kanjis_font_face {draw_font.faces[ekg::io::font_face_type::kanjis]}; + + FT_Face ft_face {}; + FT_Vector ft_vector_previous_char {}; + char32_t ft_uint_previous {}; + + size_t chunk_size {}; + size_t il {}; + size_t sum_chunk_sizes {}; + size_t text_total_lines {textbox.text.length_of_lines()}; + + ekg::vec2_t rendered {}; + ekg::rect_t rect {}; + + size_t text_len {}; + size_t utf8_text_len {}; + uint8_t uc8 {}; + char32_t c32 {}; + + std::list &chunks {textbox.text.chunks_data()}; + size_t chunks_size {chunks.size()}; + + index.y += textbox.widget.view_line_index; + textbox.widget.scrollbar.rect.h = text_total_lines * textbox.widget.rect_text_size.h; + + ekg::vec2_t pos { + 0.0f, + 0.0f + }; + + float visible_text_height {static_cast(textbox.widget.view_line_index * textbox.widget.rect_text_size.h)}; + pos.y = textbox.widget.scrollbar_property.scroll.position.y + visible_text_height; + pos.y = static_cast(static_cast(floorf(pos.y))); + + std::string empty {"\n"}; + bool is_empty {}; + + il = textbox.widget.view_chunk_line_index; + for (std::list::iterator ic {textbox.widget.view_chunk_it}; ic != chunks.end(); ic++) { + ekg::io::chunk_t &chunk {*ic}; + + chunk_size = chunk.size(); + for (;il < chunk_size; il++) { + std::string &chunk_line {chunk.at(il)}; + std::string &line {(is_empty = chunk_line.empty()) ? empty : chunk_line}; + + text_len = line.size(); + for (size_t it {}; it < text_len; it++) { + uc8 = static_cast(line.at(it)); + + ekg::utf8_sequence( + uc8, + c32, + line, + it + ); + + if (draw_font.ft_bool_kerning && ft_uint_previous) { + switch (c32 < 256 || !emojis_font_face.was_loaded) { + case true: { + ft_face = text_font_face.ft_face; + break; + } + + default: { + ft_face = emojis_font_face.ft_face; + break; + } + } + + FT_Get_Kerning(ft_face, ft_uint_previous, c32, 0, &ft_vector_previous_char); + pos.x += static_cast(ft_vector_previous_char.x >> 6); + } + + ekg::io::glyph_t &glyph {draw_font.mapped_glyph[c32]}; + + rect.x = pos.x + textbox.widget.scrollbar_property.scroll.position.x + rect_abs.x; + rect.y = pos.y + rect_abs.y; + rect.w = glyph.wsize / 2; + rect.h = textbox.widget.rect_text_size.h; + + if (ekg::rect_collide_vec2(rect, interact)) { + return true; + } + + rect.w = glyph.wsize; + if (!is_empty && ekg::rect_collide_vec2(rect, interact)) { + index.x++; + return true; + } + + index.x++; + pos.x += rect.w; + + rect.w = rect_abs.w; + if (it+1 == text_len && ekg::rect_collide_vec2(rect, interact)) { + index.x *= !is_empty; // if line is empty then last utf8 index is 0 + return true; + } + } + + index.x = 0; + pos.x = textbox.color_scheme.gutter_margin; + + index.y++; + pos.y += textbox.widget.rect_text_size.h; + + rendered.y += textbox.widget.rect_text_size.h; + if (rendered.y >= rect_abs.h + textbox.widget.rect_text_size.h) { + return false; + } + } + + il = 0; + } + + return false; +} + +void ekg::ui::handle_cursor_interact( + ekg::property_t &property, + ekg::textbox_t &textbox, + bool picked_index, + ekg::vec2_t &pick_index, + ekg::input_info_t &input +) { + if (input.was_released && !input.was_typed) { + if ( + textbox.widget.current_cursor_index != UINT64_MAX + && + textbox.widget.current_cursor_index < textbox.widget.cursors.size() + ) { + ekg::textbox_t::cursor_t cursor { + textbox.widget.cursors[textbox.widget.current_cursor_index] + }; + + std::vector new_cursors {}; + for (size_t i {}; i < textbox.widget.cursors.size(); i++) { + ekg::textbox_t::cursor_t cur {textbox.widget.cursors[i]}; + if (cursor == cur) { + new_cursors.push_back(cur); + continue; + } + + if (cursor >= cur.a && cursor <= cur.b) { + continue; + } + + new_cursors.push_back(cur); + } + + textbox.widget.cursors = new_cursors; + } + + textbox.widget.current_cursor_index = UINT64_MAX; + } + + if (!picked_index) { + return; + } + + bool should_create_a_cursor {}; + ekg::vec2_t first_pick_pos {}; + + if (input.was_pressed && ekg::fired("textbox-action-cursor")) { + textbox.widget.picked_left = pick_index; + textbox.widget.picked_right = pick_index; + first_pick_pos = pick_index; + textbox.widget.set_cursor_static = true; + should_create_a_cursor = true; + } + + if ( + textbox.widget.current_cursor_index != UINT64_MAX && + textbox.widget.current_cursor_index >= textbox.widget.cursors.size() + ) { + textbox.widget.current_cursor_index = UINT64_MAX; + } + + if (textbox.widget.current_cursor_index != UINT64_MAX) { + ekg::textbox_t::cursor_t &cursor { + textbox.widget.cursors[textbox.widget.current_cursor_index] + }; + + if (ekg::textbox_t::cursor_t {.a = pick_index, .b = pick_index} <= cursor.delta) { + cursor.direction = ekg::dock::left; + textbox.widget.picked_left = cursor.delta; + textbox.widget.picked_right = pick_index; + } else { + cursor.direction = ekg::dock::right; + textbox.widget.picked_left = pick_index; + textbox.widget.picked_right = cursor.delta; + } + + cursor.a = textbox.widget.picked_left; + cursor.b = textbox.widget.picked_right; + + ekg::gui.ui.redraw = true; + property.widget.should_buffering = true; + } + + ekg::textbox_t::cursor_t cursor {}; + if ( + should_create_a_cursor + && + !ekg::ui::find_cursor(textbox, pick_index, cursor) + && + ekg::fired("textbox-action-multicursor") + ) { + textbox.widget.current_cursor_index = textbox.widget.cursors.size(); + textbox.widget.cursors.push_back( + { + .highest_char_index = textbox.widget.picked_left.x, + .a = textbox.widget.picked_left, + .b = textbox.widget.picked_right, + .delta = first_pick_pos + } + ); + should_create_a_cursor = false; + } + + if (should_create_a_cursor) { + textbox.widget.current_cursor_index = 0; + textbox.widget.cursors.clear(); + textbox.widget.cursors.push_back( + { + .highest_char_index = textbox.widget.picked_left.x, + .a = textbox.widget.picked_left, + .b = textbox.widget.picked_right, + .delta = first_pick_pos + } + ); + } +} + +void ekg::ui::handle_erase( + ekg::textbox_t &textbox, + ekg::textbox_t::cursor_t &cursor +) { + if (cursor.a == cursor.b) { + return; + } + + ekg_log_low_level("begin") + + if (cursor.a.y == cursor.b.y) { + std::string line {textbox.text.at(cursor.a.y)}; + std::string concated {}; + + ekg::textbox_t::cursor_t origin {cursor}; + origin.a = origin.delta; + origin.b = origin.delta; + + cursor.is_ignored = true; + ekg::ui::refresh_cursors_pos( + textbox, + origin, + {cursor.b.x - cursor.a.x, 0}, + {cursor.b.x - cursor.a.x, 0}, + ekg::ui::textbox_operation::erase_inline + ); + cursor.is_ignored = false; + + ekg::utf8_concat( + line, + {0, cursor.a.x, cursor.b.x, line.size()}, + concated + ); + + textbox.text.set( + cursor.a.y, + concated + ); + + cursor.b = cursor.a; + cursor.delta = cursor.a; + cursor.highest_char_index = cursor.a.x; + + return; + } + + ekg::textbox_t::cursor_t origin {cursor}; + + cursor.is_ignored = true; + ekg::ui::refresh_cursors_pos( + textbox, + origin, + {cursor.b.x, cursor.b.y - cursor.a.y}, + {cursor.b.x, cursor.b.y - cursor.a.y}, + ekg::ui::textbox_operation::erase_multiline + ); + + cursor.is_ignored = false; + textbox.text.set( + cursor.a.y, + ekg::utf8_substr( + textbox.text.at(cursor.a.y), + 0, cursor.a.x + ) + + + ekg::utf8_substr( + textbox.text.at(cursor.b.y), + cursor.b.x, UINT32_MAX + ) + ); + + textbox.text.erase( + cursor.a.y + 1, + cursor.b.y + 1 + ); + + textbox.text.sanitize(); + + cursor.b = cursor.a; + cursor.delta = cursor.a; + cursor.highest_char_index = cursor.a.x; + + ekg_log_low_level("ended") +} + +void ekg::ui::handle_insert( + ekg::textbox_t &textbox, + ekg::textbox_t::cursor_t &cursor, + std::string_view typed +) { + if (cursor.a != cursor.b) { + ekg::ui::handle_erase(textbox, cursor); + } + + if (typed == EKG_EOF_SYSTEM) { + std::string line {textbox.text.at(cursor.a.y)}; + std::string right_cut {ekg::utf8_substr(line, cursor.a.x, UINT32_MAX)}; + size_t right_cut_size {ekg::utf8_length(right_cut)}; + + ekg::textbox_t::cursor_t origin {cursor}; + origin.b = origin.a; + + cursor.is_ignored = true; + ekg::ui::refresh_cursors_pos( + textbox, + origin, + {right_cut_size, 1}, + {right_cut_size, 1}, + ekg::ui::textbox_operation::insert_line + ); + cursor.is_ignored = false; + + textbox.text.set(cursor.a.y, ekg::utf8_substr(line, 0, cursor.a.x)); + textbox.text.insert(cursor.a.y, right_cut); + + cursor.a.y++; + cursor.a.x = 0; + cursor.b = cursor.a; + cursor.highest_char_index = cursor.a.x; + return; + } + + size_t byte_pos {}; + std::string line {textbox.text.at(cursor.a.y)}; + ekg::utf8_find_byte_pos_by_utf_pos(line, cursor.a.x, byte_pos); + + line.insert( + line.begin() + byte_pos, + typed.begin(), + typed.end() + ); + + ekg::textbox_t::cursor_t origin {cursor}; + ekg::vec2_t displacement_a {}; + ekg::vec2_t displacement_b {}; + + ekg::io::chunk_t split_endings {}; + size_t added {textbox.text.set(cursor.a.y, line, split_endings)}; + + if (added == 1) { + displacement_a = {ekg::utf8_length(typed), 0}; + displacement_b = displacement_a; + cursor.a.x += displacement_a.x; + } else { + displacement_a.y = added-1; + cursor.a.y += displacement_a.y; + + size_t ending {typed.rfind('\n')}; + if (ending != std::string::npos) { + displacement_a.x = ekg::utf8_length(typed.substr(ending, UINT32_MAX)); + } else { + displacement_a.x = ekg::utf8_length(typed); + } + + displacement_b = displacement_a; + cursor.a.x = displacement_a.x; + } + + origin.b = cursor.a; + + cursor.is_ignored = true; + ekg::ui::refresh_cursors_pos( + textbox, + origin, + displacement_a, + displacement_b, + ( + added == 1 ? + ekg::ui::textbox_operation::insert_inline + : + ekg::ui::textbox_operation::insert_multiline + ) + ); + cursor.is_ignored = false; + + cursor.b = cursor.a; + cursor.highest_char_index = cursor.a.x; +} + +void ekg::ui::reload( + ekg::property_t &property, + ekg::textbox_t &textbox +) { + ekg::axis pick_axis { + ekg::axis::horizontal + }; + + ekg::draw::font &draw_font { + ekg::draw::get_font_renderer(textbox.font_size) + }; + + textbox.widget.rect_text_size.h = draw_font.text_height + draw_font.offset_text_height; + textbox.widget.rect_text_size.w = textbox.widget.rect_text_size.h; + + ekg::aligned_t aligned_dimension {}; + ekg::align_rect_dimension( + pick_axis, + textbox.widget.rect_text_size, + ekg::dpi.min_sizes, + aligned_dimension + ); + + textbox.rect.scaled_height = ekg::max(1, textbox.rect.scaled_height); + textbox.rect.h = aligned_dimension.h * textbox.rect.scaled_height; + + size_t highest_size {}; + std::list &chunks {textbox.text.chunks_data()}; + + ekg::rect_t &rect_abs { + ekg::ui::get_abs_rect(property, textbox.rect) + }; + + textbox.widget.scrollbar.rect.x = rect_abs.x; + textbox.widget.scrollbar.rect.y = rect_abs.y; + textbox.widget.scrollbar.acceleration = {textbox.widget.rect_text_size.h, textbox.widget.rect_text_size.h}; + textbox.widget.scrollbar.color_scheme = ekg::p_core->handler_theme.get_current_theme().scrollbar_color_scheme; + + ekg::ui::reload( + textbox.widget.scrollbar_property, + textbox.widget.scrollbar, + rect_abs, + property.children, + false + ); + + ekg::ui::refresh_scroll_positions(textbox); +} + +void ekg::ui::event( + ekg::property_t &property, + ekg::textbox_t &textbox, + const ekg::io::stage &stage +) { + ekg::property_t &scrollbar_property { + textbox.widget.scrollbar_property + }; + + scrollbar_property.widget.rect_scissor = property.widget.rect_scissor; + + ekg::ui::event( + scrollbar_property, + textbox.widget.scrollbar, + stage, + ekg::ui::get_abs_rect(property, textbox.rect) + ); + + property.widget.should_buffering = scrollbar_property.widget.should_buffering; + property.states.is_absolute = scrollbar_property.states.is_absolute; + property.scroll = scrollbar_property.scroll; + + switch (stage) { + default: { + ekg::input_info_t input {ekg::p_core->handler_input.input}; + ekg::vec2_t interact {static_cast>(input.interact)}; + + /* focus part */ + + bool should_enable_high_frequency { + property.states.is_hovering + || + property.states.is_focused + || + scrollbar_property.widget.is_high_frequency + }; + + bool is_focus_action_fired { + property.states.is_hovering + && + input.was_pressed + && + ekg::fired("textbox-focus") + }; + + if ( + !property.states.is_focused + && + is_focus_action_fired + ) { + should_enable_high_frequency = true; + property.states.is_focused = true; + } + + if ( + property.states.is_focused + && + !property.states.is_hovering + && + input.was_pressed + && + !input.was_typed + ) { + property.states.is_focused = false; + should_enable_high_frequency = false; + } + + if (!property.states.is_focused) { + return; + } + + if (property.states.is_hovering) { + ekg::p_core->p_platform_base->system_cursor = ekg::system_cursor::ibeam; + } + + if ( + textbox.widget.scrollbar.widget.states_horizontal_bar.is_hovering + || + textbox.widget.scrollbar.widget.states_vertical_bar.is_hovering + ) { + ekg::p_core->p_platform_base->system_cursor = ekg::system_cursor::arrow; + } + + ekg::vec2_t pick_index {}; + bool should_pick_index {}; + bool picked_index {}; + + if ( + property.states.is_focused + && + property.states.is_hovering + && + input.was_pressed + && + !textbox.widget.scrollbar_property.states.is_hovering + ) { + should_pick_index = true; + } + + /* where input logic and optmized parts are placed */ + + if (textbox.widget.current_cursor_index != UINT64_MAX) { + should_pick_index = true; + } + + if (should_pick_index) { + picked_index = + ekg::ui::find_index_by_interact( + property, + textbox, + interact, + pick_index + ); + } + + if (textbox.widget.set_cursor_static) { + textbox.widget.unset_cursor_static = input.was_released; + } + + ekg::ui::handle_cursor_interact( + property, + textbox, + picked_index, + pick_index, + input + ); + + if ( + should_enable_high_frequency + ) { + ekg::io::dispatch( + ekg::io::operation::high_frequency, + property.at + ); + } + + if (!input.was_typed && !input.was_pressed) { + return; + } + + /** + * logic of cursors: + * for handling lot of cursors we will + * use only one loop for improve performance + **/ + + bool is_action_selected_all_fired {ekg::fired("textbox-action-select-all")}; + if (is_action_selected_all_fired) { + size_t lines {textbox.text.length_of_lines(true)}; + if (lines > 0) { + lines -= 1; + std::string last_line {textbox.text.at(lines)}; + textbox.widget.cursors.clear(); + textbox.widget.cursors.push_back( + { + .a = {0, 0}, + .b = {ekg::utf8_length(last_line), lines} + } + ); + } + + return; + } + + bool is_action_copy {ekg::fired("clipboard-copy")}; + bool is_action_paste {ekg::fired("clipboard-paste")}; + bool is_action_cut {ekg::fired("clipboard-cut")}; + + bool is_action_erase_right_fired {ekg::fired("textbox-action-erase-right")}; + bool is_action_erase_left_fired {ekg::fired("textbox-action-erase-left")}; + bool is_action_erase_fired {is_action_erase_left_fired || is_action_erase_right_fired}; + bool is_action_selected_keybind_fired {ekg::fired("textbox-action-select")}; + bool is_action_selected_fired {}; + bool is_action_break_line_fired {ekg::fired("textbox-action-break-line")}; + bool block_if_ab_diff_when_erased_is_fired {}; + + bool is_modifier_fired {ekg::fired("textbox-action-modifier")}; + bool is_left_fired {ekg::fired("textbox-action-left")}; + bool is_bind_left_fired {is_left_fired}; + bool is_modifier_left_fired {}; + bool is_right_fired {ekg::fired("textbox-action-right")}; + bool is_bind_right_fired {is_right_fired}; + bool is_modifier_right_fired {}; + bool is_up_fired {ekg::fired("textbox-action-up")}; + bool is_modifier_up_fired {is_up_fired && is_modifier_fired}; + bool is_down_fired {ekg::fired("textbox-action-down")}; + bool is_modifier_down_fired {is_down_fired && is_modifier_fired}; + bool is_arrows_fired {is_left_fired || is_right_fired || is_up_fired || is_down_fired}; + + if (is_action_paste) { + input.was_typed = true; + input.typed = ekg::p_core->p_platform_base->get_clipboard_text(); + } + + std::string clipboard_builder {}; + if (is_action_cut) { + input.was_typed = false; + } + + if ( + is_arrows_fired + || + is_action_erase_fired + || + input.was_typed + || + is_action_break_line_fired + || + is_action_copy + || + is_action_cut + || + is_action_paste + ) { + textbox.widget.set_cursor_static = true; + } + + bool is_ab_equals {}; + bool is_bounding {}; + + size_t text_total_lines {textbox.text.length_of_lines()}; + size_t byte_pos {}; + size_t nearest_byte_pos {}; + size_t cursor_byte_pos {}; + size_t highest {}; + + ekg::rect_t &rect_abs {ekg::ui::get_abs_rect(property, textbox.rect)}; + ekg::vec2_t cursor_pos {}; + std::string line {}; + + size_t cursors_size {textbox.widget.cursors.size()}; + size_t cursor_count {}; + + bool should_clear_equals_repeated_cursors {}; + + for (ekg::textbox_t::cursor_t &cursor : textbox.widget.cursors) { + if (cursor.is_deleted) continue; + + cursor_count++; + is_ab_equals = cursor.a == cursor.b; + + is_action_selected_fired = (is_arrows_fired && is_action_selected_keybind_fired) || (is_action_erase_fired && is_ab_equals); + is_left_fired = is_bind_left_fired || (is_action_erase_left_fired && is_ab_equals); + is_right_fired = is_bind_right_fired || (is_action_erase_right_fired && is_ab_equals); + + is_modifier_left_fired = is_left_fired && is_modifier_fired; + is_modifier_right_fired = is_right_fired && is_modifier_fired; + + /** + * Before move the cursor, we need make sure we are working with + * right directions. This is necessary for make possible select. + **/ + if ( + is_action_selected_fired + && + !(is_action_erase_left_fired || is_action_erase_left_fired) + ) { + if (cursor == cursor.delta && is_left_fired) { + cursor.b = cursor.a; + cursor.direction = ekg::dock::left; + } else if (cursor == cursor.delta && is_right_fired) { + cursor.a = cursor.b; + cursor.direction = ekg::dock::right; + } else if (cursor < cursor.delta) { + cursor.a = cursor.b; + cursor.direction = ekg::dock::right; + } else if (cursor > cursor.delta) { + cursor.b = cursor.a; + cursor.direction = ekg::dock::left; + } + + is_ab_equals = true; + } else { + cursor.delta = cursor.a; + } + + if (is_left_fired) { + if (is_ab_equals) { + if (cursor.a.x > 0) { + cursor.a.x -= 1; + } else if (cursor.a.x == 0 && cursor.a.y > 0) { + cursor.a.y -= 1; + cursor.a.x = ekg::utf8_length(textbox.text.at(cursor.a.y)); + is_modifier_left_fired = false; + } + } + + ( + is_modifier_left_fired + && + ekg::utf8_find_byte_pos_by_utf_pos( + (line = textbox.text.at(cursor.a.y)), + cursor.a.x, + cursor_byte_pos + ) + && + ekg::utf8_nearest_regex_group_matched_position( + line.cbegin(), + line.cbegin() + cursor_byte_pos, + nearest_byte_pos, + textbox.regex_operations[ekg::textbox_t::operation::modifier_left], + ekg::dock::left + ) + && + ekg::utf8_align_utf_pos_by_byte_pos( + line, + cursor_byte_pos, + cursor.a.x, + nearest_byte_pos + ) + ); + + cursor.highest_char_index = cursor.a.x; + cursor.b = cursor.a; + } + + if (is_right_fired) { + if (is_ab_equals) { + size_t line_text_length {ekg::utf8_length(textbox.text.at(cursor.a.y))}; + if (cursor.b.x < line_text_length) { + cursor.b.x += 1; + } else if (cursor.a.y < text_total_lines-1) { + cursor.b.y += 1; + cursor.b.x = 0; + is_modifier_right_fired = false; + } + } + + if ( + is_modifier_right_fired + && + ekg::utf8_find_byte_pos_by_utf_pos( + (line = textbox.text.at(cursor.b.y)), + cursor.b.x, + cursor_byte_pos + ) + && + ekg::utf8_nearest_regex_group_matched_position( + line.cbegin() + cursor_byte_pos, + line.cend(), + nearest_byte_pos, + textbox.regex_operations[ekg::textbox_t::operation::modifier_right], + ekg::dock::right + ) + && + ekg::utf8_align_utf_pos_by_byte_pos( + line, + cursor_byte_pos, + cursor.b.x, + cursor_byte_pos + nearest_byte_pos + ) + ); + + cursor.highest_char_index = cursor.b.x; + cursor.a = cursor.b; + } + + if (is_up_fired) { + if (is_ab_equals) cursor.a.x = cursor.highest_char_index; + + is_bounding = true; + if (cursor.a.y > 0) { + cursor.a.y -= 1; + is_bounding = false; + } + + if (!is_ab_equals) cursor.highest_char_index = cursor.a.x; + + size_t line_text_length {ekg::utf8_length(textbox.text.at(cursor.a.y))}; + if (cursor.a.x > line_text_length) { + cursor.a.x = line_text_length; + } + + if (is_bounding) { + cursor.highest_char_index = cursor.a.x; + cursor.a.x = 0; + } + + cursor.b = cursor.a; + } + + if (is_down_fired) { + if (is_ab_equals) cursor.b.x = cursor.highest_char_index; + + is_bounding = true; + if (cursor.b.y < text_total_lines-1) { + cursor.b.y += 1; + is_bounding = false; + } + + if (!is_ab_equals) cursor.highest_char_index = cursor.b.x; + + size_t line_text_length {ekg::utf8_length(textbox.text.at(cursor.b.y))}; + if (cursor.b.x > line_text_length) { + cursor.b.x = line_text_length; + } + + if (is_bounding) { + cursor.highest_char_index = cursor.b.x; + cursor.b.x = line_text_length; + } + + cursor.a = cursor.b; + } + + /** + * First we move cursor and check the right direction (first structure like this). Then. + * We need check again the right direction after moved. + * This will make possible modifier works with select from any direction. + **/ + if (is_action_selected_fired) { + if (cursor == cursor.delta && is_left_fired) { + cursor.b = cursor.a; + cursor.direction = ekg::dock::left; + } else if (cursor == cursor.delta && is_right_fired) { + cursor.a = cursor.b; + cursor.direction = ekg::dock::right; + } else if (cursor < cursor.delta) { + cursor.a = cursor.b; + cursor.direction = ekg::dock::right; + } else if (cursor > cursor.delta) { + cursor.b = cursor.a; + cursor.direction = ekg::dock::left; + } + + highest = cursor.highest_char_index; + if (cursor.direction == ekg::dock::left) { + cursor.a = cursor.a; + cursor.b = cursor.delta; + highest = cursor.a.x; + } else if (cursor.direction == ekg::dock::right) { + cursor.a = cursor.delta; + cursor.b = cursor.b; + highest = cursor.a.x; + } + + if (is_left_fired) { + cursor.highest_char_index = highest; + } else if (is_right_fired) { + cursor.highest_char_index = highest; + } + } + + if (is_action_cut || is_action_copy) { + clipboard_builder += textbox.text.read( + cursor.a, + cursor.b + ) + + + ( + ( + (cursors_size > 1) + && + !is_ab_equals + && + cursor_count != cursors_size + ) + ? EKG_EOF_SYSTEM : "" + ); + } + + if (is_action_erase_fired || is_action_cut) { + ekg::ui::handle_erase(textbox, cursor); + } + + if (input.was_typed || is_action_break_line_fired) { + ekg::ui::handle_insert( + textbox, + cursor, + is_action_break_line_fired + ? EKG_EOF_SYSTEM : input.typed + ); + + if (is_action_break_line_fired || is_action_paste) { + text_total_lines = textbox.text.length_of_lines(true); + } + } + + if (!is_action_selected_fired) { + cursor.delta = cursor.a; + } + + cursor_pos.y = textbox.widget.scrollbar_property.scroll.position.y + (cursor.a.y * textbox.widget.rect_text_size.h); + cursor_pos.y = static_cast(static_cast(floorf(cursor_pos.y))); + + if (cursor_pos.y + textbox.widget.rect_text_size.h > rect_abs.h) { + textbox.widget.scrollbar_property.scroll.position.w -= + ekg::max(cursor_pos.y + textbox.widget.rect_text_size.h - rect_abs.h, textbox.widget.rect_text_size.h); + } else if (cursor_pos.y <= 0.0f) { + textbox.widget.scrollbar_property.scroll.position.w += + ekg::max(-cursor_pos.y, textbox.widget.rect_text_size.h); + } + + if (!cursor.is_deleted) { + cursor.is_ignored = true; + for (ekg::textbox_t::cursor_t &nearest_cursor : textbox.widget.cursors) { + if ( + nearest_cursor.is_ignored + || + nearest_cursor.is_deleted + || + cursor != nearest_cursor + ) continue; + should_clear_equals_repeated_cursors = true; + nearest_cursor.is_deleted = true; + } + cursor.is_ignored = false; + } + } + + if (should_clear_equals_repeated_cursors) { + for (size_t it {}; it < textbox.widget.cursors.size(); it++) { + ekg::textbox_t::cursor_t &cursor {textbox.widget.cursors.at(it)}; + if (!cursor.is_deleted) continue; + textbox.widget.cursors.erase(textbox.widget.cursors.begin() + it); + } + } + + if (is_action_cut || is_action_copy) { + ekg::p_core->p_platform_base->set_clipboard_text(clipboard_builder.c_str()); + } + + if (is_action_erase_fired || input.was_typed || is_action_break_line_fired) { + ekg::ui::refresh_scroll_positions(textbox); + } + + ekg::gui.ui.redraw = true; + + ekg_log_low_level(textbox.text.length_of_chunks()) + + break; + } + case ekg::io::stage::pre: + ekg::ui::pre_event(property, textbox.rect, false); + break; + case ekg::io::stage::post: + ekg::ui::post_event(property); + break; + } +} + +void ekg::ui::high_frequency( + ekg::property_t &property, + ekg::textbox_t &textbox +) { + if (textbox.widget.set_cursor_static) { + ekg::reset(textbox.widget.cursor_timing); + textbox.widget.set_cursor_static = !textbox.widget.unset_cursor_static; + } + + textbox.widget.unset_cursor_static = false; + ekg::reset_if_reach(textbox.widget.cursor_timing, 1000); + + ekg::rect_t &rect_abs { + ekg::ui::get_abs_rect(property, textbox.rect) + }; + + ekg::ui::high_frequency( + textbox.widget.scrollbar_property, + textbox.widget.scrollbar, + rect_abs + ); + + ekg::gui.ui.redraw = true; + property.widget.should_buffering = true; + property.widget.is_high_frequency = ( + property.states.is_focused + || + textbox.widget.scrollbar_property.widget.is_high_frequency + ); +} + +void ekg::ui::pass( + ekg::property_t &property, + ekg::textbox_t &textbox +) { + ekg_draw_allocator_bind_local( + &property.widget.geometry_buffer, + &property.widget.gpu_data_buffer + ); + + if (property.widget.should_buffering || !textbox.text.audited()) { + property.widget.should_buffering = true; + return; + } + + ekg_draw_allocator_pass(); +} + +void ekg::ui::buffering( + ekg::property_t &property, + ekg::textbox_t &textbox +) { + ekg::rect_t &rect_abs { + ekg::ui::get_abs_rect(property, textbox.rect) + }; + + ekg_draw_allocator_assert_scissor( + property.widget.rect_scissor, + rect_abs, + ekg::query(property.parent_at).widget.rect_scissor, + ekg::always_parented + ); + + textbox.text.unset_audited(); + + /* start of background */ + + ekg::draw::rect( + rect_abs, + textbox.color_scheme.background, + ekg::draw::mode::fill, + textbox.layers[ekg::layer::bg] + ); + + /* start of select */ + + bool elapsed_mid_second { + textbox.widget.cursor_timing.current_ticks > 500 + }; + + ekg::rect_t rect_select {}; + for (ekg::textbox_t::select_draw_layer_t &layer : textbox.widget.layers_select) { + if (!layer.is_always_static && layer.is_ab_equals && elapsed_mid_second) { + continue; + } + + rect_select = layer.rect + rect_abs + textbox.widget.scrollbar_property.scroll.position; + rect_select.y = layer.rect.y + rect_abs.y; + + ekg::draw::rect( + rect_select, + layer.is_ab_equals ? textbox.color_scheme.text_cursor_foreground : textbox.color_scheme.text_select_foreground, + ekg::draw::mode::fill, + ekg::at_t::not_found + ); + } + + /* start of text */ + + ekg::draw::font &draw_font { + ekg::draw::get_font_renderer(textbox.font_size) + }; + + ekg::gpu::data_t &data { + ekg::p_core->draw_allocator.bind_current_data() + }; + + ekg::vec2_t pos { + static_cast(static_cast(rect_abs.x + textbox.color_scheme.gutter_margin)) + textbox.widget.scrollbar_property.scroll.position.x, + floorf(static_cast(static_cast(rect_abs.y - draw_font.offset_text_height))) + }; + + ekg::io::font_face_t &text_font_face {draw_font.faces[ekg::io::font_face_type::text]}; + ekg::io::font_face_t &emojis_font_face {draw_font.faces[ekg::io::font_face_type::emojis]}; + ekg::io::font_face_t &kanjis_font_face {draw_font.faces[ekg::io::font_face_type::kanjis]}; + + data.buffer[0] = pos.x; + data.buffer[1] = pos.y; + data.buffer[2] = static_cast(-draw_font.non_swizzlable_range); + + data.buffer[4] = static_cast(textbox.color_scheme.text_foreground.x / 255); + data.buffer[5] = static_cast(textbox.color_scheme.text_foreground.y / 255); + data.buffer[6] = static_cast(textbox.color_scheme.text_foreground.z / 255); + data.buffer[7] = static_cast(textbox.color_scheme.text_foreground.w / 255); + + ekg::hash_t hash {}; + ekg::rect_t vertex {}; + ekg::rect_t uv {}; + ekg::rect_t rendered {}; + + size_t text_len {}; + size_t utf8_text_len {}; + uint8_t uc8 {}; + char32_t c32 {}; + + FT_Face ft_face {}; + FT_Vector ft_vector_previous_char {}; + char32_t ft_uint_previous {}; + + bool is_inline_selected {}; + bool is_complete_line_selected {}; + bool is_ab_equals_selected {}; + bool is_cursor_at_end_of_line {}; + bool cursors_going_on {!textbox.widget.cursors.empty()}; + bool reached_renderable_state {}; + bool get_out {}; + bool is_last_char_from_line {}; + + float end_cursor_position {}; + float extra_rect_height {rect_abs.h + textbox.widget.rect_text_size.h}; + float glyph_wsize {}; + float previous_fitting_glyph {}; + float previous_kerning {}; + + size_t current_line_for_cursor_complete {UINT64_MAX}; + size_t text_total_chars {textbox.text.length_of_chars()}; + size_t text_total_lines {textbox.text.length_of_lines()}; + size_t il {}; + size_t sum_chunk_sizes {}; + size_t chunk_size {}; + ekg::vec2_t index {}; + + ekg::pixel_t line_wsize {}; + ekg::textbox_t::cursor_t cursor {}; + std::list &chunks {textbox.text.chunks_data()}; + size_t chunks_size {chunks.size()}; + bool is_renderable {}; + + textbox.widget.scrollbar.rect.h = text_total_lines * textbox.widget.rect_text_size.h; + textbox.widget.view_line_index = + static_cast( + -ekg::arithmetic_normalize( + textbox.widget.scrollbar_property.scroll.position.y, + textbox.widget.scrollbar.rect.h + ) + * + text_total_lines + ); + + /** + * This technique prevent from floating-point precision loss inside GPU. + * + * Think that you are scrolling a 1milion text, if you subtract the scroll-position (1 milion) + * with the text-content, for do scrolling effect, this will send 1000000.0f to the GPU. + * Some GPUs can not handle high floating numbers, because matrix projection calculate + * is too heavy to a large float32. + * + * This method send to the GPU: from 0.0 to textbox-height. The scrolling effect happens but + * without sending 1 milion of f32 to the GPU. The effect is emulated by scrolling only the first + * line: from 0.0 to negative text-height; so the effect occurs without the needs of stupid + * scrolling everything. + * + * This technique works because the line height is fixed, ultimately, soon, should be re-worked + * to support differents text-heights at same time, also, for widgets scrolling (I do not think + * someone can write a GUI context with +5000000 heights from widgets without pages). + * + * - Rina - 11:39; 08/06/2025 + **/ + float visible_text_height {static_cast(textbox.widget.view_line_index * textbox.widget.rect_text_size.h)}; + pos.y = textbox.widget.scrollbar_property.scroll.position.y + visible_text_height; + pos.y = static_cast(static_cast(floorf(pos.y))); + pos.x = textbox.color_scheme.gutter_margin; + + std::string empty {"\n"}; + bool is_empty {}; + bool was_empty_before {}; + + textbox.widget.layers_select.clear(); + for (std::list::iterator ic {chunks.begin()}; ic != chunks.end(); ic++) { + ekg::io::chunk_t &chunk {*ic}; + chunk_size = chunk.size(); + if (sum_chunk_sizes + chunk_size < textbox.widget.view_line_index) { + pos.x = 0.0f; + index.y += chunk_size; + sum_chunk_sizes += chunk_size; + continue; + } + + il = 0; + if (!reached_renderable_state) { + il = textbox.widget.view_line_index - sum_chunk_sizes; + index.y += il; + textbox.widget.view_chunk_line_index = il; + textbox.widget.view_chunk_it = ic; + reached_renderable_state = true; + } + + sum_chunk_sizes += chunk_size; + for (;il < chunk_size; il++) { + std::string &chunk_line {chunk.at(il)}; + std::string &line {(is_empty = chunk_line.empty()) ? empty : chunk_line}; + + index.x = 0; + text_len = line.size(); + for (size_t it {}; it < text_len; it++) { + uc8 = static_cast(line.at(it)); + + ekg::utf8_sequence( + uc8, + c32, + line, + it + ); + + if (draw_font.ft_bool_kerning && ft_uint_previous) { + switch (c32 < 256 || !emojis_font_face.was_loaded) { + case true: { + ft_face = text_font_face.ft_face; + break; + } + + default: { + ft_face = emojis_font_face.ft_face; + break; + } + } + + FT_Get_Kerning(ft_face, ft_uint_previous, c32, 0, &ft_vector_previous_char); + pos.x += (previous_kerning = static_cast(ft_vector_previous_char.x >> 6)); + } + + ekg::io::glyph_t &glyph {draw_font.mapped_glyph[c32]}; + glyph.kerning = previous_kerning; + + is_last_char_from_line = it+1 == text_len; + if ( + cursors_going_on + && + ( + ekg::ui::find_cursor(textbox, index, cursor) + || + ( + is_cursor_at_end_of_line = + is_last_char_from_line + && + ++index.x && ekg::ui::find_cursor(textbox, index, cursor) + ) + ) + ) { + glyph_wsize = glyph.wsize; + end_cursor_position = glyph_wsize * is_cursor_at_end_of_line; + is_ab_equals_selected = property.states.is_focused && cursor == index; + + is_inline_selected = cursor >= index && cursor < index && !is_ab_equals_selected; + is_complete_line_selected = false; + + if (is_inline_selected) { + if ( + ( + ( + cursor >= ekg::vec2_t(0, index.y) + ) + && + ( + cursor <= ekg::vec2_t(text_len, index.y) + ) + ) + ) { + is_inline_selected = false; + } + + is_complete_line_selected = !is_inline_selected; + } + + ekg::textbox_t::cursor_t nearest_cursor {}; + ekg::vec2_t next_line_index(0, index.y + 1); + bool is_next_line_selected_in_some_way {}; + + if ( + is_last_char_from_line + && + (is_next_line_selected_in_some_way = ekg::ui::find_cursor(textbox, next_line_index, nearest_cursor)) + && + !is_complete_line_selected + ) { + glyph_wsize = (glyph_wsize * !is_cursor_at_end_of_line) + draw_font.space_wsize; + } + + ekg::vec2_t next_char_index(next_line_index.x + 1, next_line_index.y); + if ( + !textbox.color_scheme.caret_cursor + && + is_next_line_selected_in_some_way + && + !ekg::ui::find_cursor(textbox, next_char_index, nearest_cursor) + ) { + cursor.rect.x = textbox.color_scheme.gutter_margin; + cursor.rect.y = pos.y + textbox.widget.rect_text_size.h; + cursor.rect.w = textbox.color_scheme.cursor_thickness; + cursor.rect.h = textbox.widget.rect_text_size.h; + textbox.widget.layers_select.push_back({.is_ab_equals = true, .is_always_static = true, .rect = cursor.rect}); + } + + if (is_ab_equals_selected) { + cursor.rect.x = pos.x + end_cursor_position; + cursor.rect.y = pos.y; + cursor.rect.w = textbox.color_scheme.caret_cursor ? glyph_wsize : textbox.color_scheme.cursor_thickness; + cursor.rect.h = textbox.widget.rect_text_size.h; + textbox.widget.layers_select.push_back({.is_ab_equals = true, .rect = cursor.rect}); + } + + if (is_inline_selected) { + cursor.rect.x = pos.x + end_cursor_position; + cursor.rect.y = pos.y; + cursor.rect.w = glyph_wsize; + cursor.rect.h = textbox.widget.rect_text_size.h; + textbox.widget.layers_select.push_back({.rect = cursor.rect}); + } + + if ( + it == 0 + ) { + line_wsize = 0; + } + + if (is_complete_line_selected && !is_empty) { + line_wsize += glyph_wsize; + } + + is_cursor_at_end_of_line = false; + } + + if (is_empty) { + hash += ekg_generate_hash(pos.x, c32, glyph.x++); + continue; + } + + if (!glyph.was_sampled) { + draw_font.new_glyphs_to_atlas.emplace_back(c32); + glyph.was_sampled = true; + } + + vertex.x = pos.x + glyph.left; + vertex.y = pos.y + draw_font.atlas_rect.h - glyph.top; + + vertex.w = glyph.w; + vertex.h = glyph.h; + + uv.x = glyph.x; + uv.w = vertex.w / draw_font.atlas_rect.w; + uv.h = vertex.h / draw_font.atlas_rect.h; + + ekg::p_core->draw_allocator.push_back_geometry( + vertex.x, + vertex.y, + uv.x, + uv.y + ); + + ekg::p_core->draw_allocator.push_back_geometry( + vertex.x, + vertex.y + vertex.h, + uv.x, + uv.y + uv.h + ); + + ekg::p_core->draw_allocator.push_back_geometry( + vertex.x + vertex.w, + vertex.y + vertex.h, + uv.x + uv.w, + uv.y + uv.h + ); + + ekg::p_core->draw_allocator.push_back_geometry( + vertex.x + vertex.w, + vertex.y + vertex.h, + uv.x + uv.w, + uv.y + uv.h + ); + + ekg::p_core->draw_allocator.push_back_geometry( + vertex.x + vertex.w, + vertex.y, + uv.x + uv.w, + uv.y + ); + + ekg::p_core->draw_allocator.push_back_geometry( + vertex.x, + vertex.y, + uv.x, + uv.y + ); + + pos.x += glyph.wsize; + ft_uint_previous = c32; + previous_fitting_glyph = glyph.wsize - (glyph.left + glyph.w); + + /** + * Peek `ekg/io/memory.hpp` for better hash definition and purpose. + **/ + hash += ekg_generate_hash(pos.x, c32, glyph.x); + index.x++; + } + + if ( + is_complete_line_selected + ) { + cursor.rect.x = textbox.color_scheme.gutter_margin; + cursor.rect.y = pos.y; + cursor.rect.h = textbox.widget.rect_text_size.h; + cursor.rect.w = line_wsize + draw_font.space_wsize; + textbox.widget.layers_select.push_back({.rect = cursor.rect}); + is_complete_line_selected = false; + line_wsize = 0; + } + + pos.x = textbox.color_scheme.gutter_margin; + pos.y += textbox.widget.rect_text_size.h; + rendered.y += textbox.widget.rect_text_size.h; + index.y++; + hash += pos.y * 32; + index.x += is_empty; + previous_fitting_glyph = 0.0f; + + if (rendered.y > extra_rect_height) { + get_out = true; + break; + } + } + + if (get_out) { + break; + } + } + + draw_font.flush(); + data.hash = hash; + + ekg::draw::allocator::is_simple_shape = false; + ekg::p_core->draw_allocator.bind_texture(draw_font.atlas_texture_sampler_at); + ekg::p_core->draw_allocator.dispatch(); + + /* start of scrollbar */ + + textbox.widget.scrollbar.rect.x = rect_abs.x; + textbox.widget.scrollbar.rect.y = rect_abs.y; + + ekg::ui::buffering( + textbox.widget.scrollbar_property, + textbox.widget.scrollbar, + rect_abs, + ekg::query(property.parent_at).widget.rect_scissor + ); + + /* start of outline */ + + ekg::draw::rect( + rect_abs, + textbox.color_scheme.outline, + ekg::draw::mode::outline, + ekg::at_t::not_found + ); + + /* start of final tweaks */ + + /** + * Cursors should be rendered backward from text-render. + * Textbox first try-render render the cursors, then at end of renderer-segment + * check if latest size is diff, then re-call buffering once for render + * the cursos (backward). + * + * The EKG renderer engine does not allow doing strip on gpu resources (not yet). + * May soon strip-resources should be implemented and this code part become deprecated. + **/ + size_t layers_select_size {textbox.widget.layers_select.size()}; + if (textbox.widget.last_layers_select_size != layers_select_size) { + property.widget.should_buffering = true; + textbox.widget.last_layers_select_size = layers_select_size; + ekg::gui.ui.redraw = true; + } + + /* end of pass */ + + ekg_draw_allocator_pass(); +} + +void ekg::ui::unmap( + ekg::textbox_t &textbox +) { + +} diff --git a/version/todo.txt b/version/todo.txt index 76282c29..c2d58f13 100644 --- a/version/todo.txt +++ b/version/todo.txt @@ -1,29 +1,6 @@ -/* DPI, UI, and Scale */ - -The current stupid system of EKG for scalling is actually useless; the scaled height factors at each widget, -is not really scaled by the layout, so, I want to use scaled height factor but scale with the DPI/UI scale. - -So 800x600 will be the same as 1920x1080 indifferent of font-size. -How? idk, i will use the mother/parent frame as base/factor and the height font too as base. -So the layout service will get the perfect sized DPI and scale. -Some day I do this. - -/* Slider must be rewrite the part of docking text value! */ - -Slider contains a glitch were you can drag with no maximum limit. - -/* Textbox is not updating newer text! */ - -stupid ticking glitch. - -/* stupid scorlling glitch */ - -rewrite slider -fix listbox issue when scrolling and dragging? -reduce ekg::slider verbose number setting. - -// Add checkbox - -✔ - -// add `set_width` and `set_height` to all widgets setters dimension methods >< meowg \ No newline at end of file +text chunk-based for textbox: +1- chunk is opportunish, any inserted text will occup any chunk if is not limited +2- if chunk is full a new one is created +3- copypasting should be calculated to fit or generate news chunks if necessary +4- if textbox is unfocused, once time should re-index and fix incorrect text align --- this may affect perf but idc for now +5- the user-programmer will use the `ekg::text` class for handling texts --- but should be like an equals one