diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ee127..37e616f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# v0.0.21 - 4 Jan 2024 +# v0.0.22 - 9 February 2025 + +* Fixed a bug when using `max_length` where `selection_end` (and `selection_start`) where incorrectly set (thanks to @TheCire for reporting in Discord). +* Fixed a cursor placement and selection background issues in the `Text` component. + +# v0.0.21 - 4 January 2025 * Updated clipboard to use getclipboard and setclipboard so the system clipboard is used * Updated menu to handle scrolling with keyboard if there are too many items, including menu sizing diff --git a/app/book_sample.rb b/app/book_sample.rb index a8d5991..96c8528 100644 --- a/app/book_sample.rb +++ b/app/book_sample.rb @@ -26,6 +26,7 @@ def tick(args) x: 20, y: 660, w: 1240, + padding: 10, prompt: 'Title', value: "Alice's Adventures in Wonderland 🐰", font: FONT, diff --git a/lib/base.rb b/lib/base.rb index ea08830..a12be85 100644 --- a/lib/base.rb +++ b/lib/base.rb @@ -61,8 +61,6 @@ def initialize(**params) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticCo # Render target for text scrolling @path = "__input_#{@@id += 1}" - @scroll_x = 0 - @scroll_y = 0 @content_w = @w @content_h = @h @@ -113,11 +111,12 @@ def draw_cursor(rt) else 255 end + rt.primitives << { x: (@cursor_x - 1).greater(0) - @scroll_x, - y: @cursor_y - @padding - @scroll_y, + y: @cursor_y - @scroll_y, w: @cursor_width, - h: @font_height + @padding * 2 + h: @font_height }.solid!(**@cursor_color, a: alpha) end @@ -174,21 +173,21 @@ def selection_end=(index) def insert(str) @selection_end, @selection_start = @selection_start, @selection_end if @selection_start > @selection_end insert_at(str, @selection_start, @selection_end) - - @selection_start += str.length - @selection_end = @selection_start end alias replace insert def insert_at(str, start_at, end_at = start_at) end_at, start_at = start_at, end_at if start_at > end_at if @max_length && @value.length - (end_at - start_at) + str.length > @max_length - str = str[0, @max_length - @value.length + (end_at - start_at) - str.length] + str = str[0, @max_length - @value.length + (end_at - start_at)] return if str.nil? # too long end @value.insert(start_at, end_at, str) @value_changed = true + + @selection_start += str.length + @selection_end = @selection_start end alias replace_at insert_at diff --git a/lib/multiline.rb b/lib/multiline.rb index d82d338..2e4763a 100644 --- a/lib/multiline.rb +++ b/lib/multiline.rb @@ -358,10 +358,10 @@ def prepare_render_target # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticC @cursor_x = 0 @scroll_x = 0 if @fill_from_bottom - @cursor_y = 0 + @cursor_y = -@padding rt.primitives << @font_style.label(x: 0, y: 0, text: @prompt, **@prompt_color) else - @cursor_y = @h - @font_height + @cursor_y = @h - @font_height - @padding rt.primitives << @font_style.label(x: 0, y: @h - @font_height, text: @prompt, **@prompt_color) end else @@ -374,7 +374,7 @@ def prepare_render_target # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticC @cursor_index = 0 end - @cursor_y = @scroll_h - (@cursor_line.number + 1) * @font_height + @cursor_y = @scroll_h - (@cursor_line.number + 1) * @font_height - @padding @cursor_y += @fill_from_bottom ? @content_h : @h - @content_h if @content_h < @h if @scroll_h <= @h # total height is less than height of the control @scroll_y = @fill_from_bottom ? @scroll_h : 0 diff --git a/lib/text.rb b/lib/text.rb index f399027..6eeb792 100644 --- a/lib/text.rb +++ b/lib/text.rb @@ -169,13 +169,13 @@ def prepare_render_target if @value.empty? @cursor_x = 0 - @cursor_y = 0 + @cursor_y = @padding @scroll_x = 0 rt.primitives << @font_style.label(x: 0, y: @padding, text: @prompt, **@prompt_color) else # CURSOR AND SCROLL LOCATION @cursor_x = @font_style.string_width(@value[0, @selection_end].to_s) - @cursor_y = 0 + @cursor_y = @padding if @content_w < @w @scroll_x = 0 @@ -199,7 +199,7 @@ def prepare_render_target right = (@font_style.string_width(@value[0, @selection_start].to_s) - @scroll_x).cap_min_max(0, @w) end - rt.primitives << { x: left, y: @padding, w: right - left, h: @font_height + @padding * 2 }.solid!(sc) + rt.primitives << { x: left, y: 0, w: right - left, h: @font_height + @padding * 2 }.solid!(sc) end # TEXT diff --git a/metadata/game_metadata.txt b/metadata/game_metadata.txt index 17e7e7b..d233a00 100644 --- a/metadata/game_metadata.txt +++ b/metadata/game_metadata.txt @@ -3,7 +3,7 @@ devid=fascinationworks devtitle=Fascination Works gameid=dr-input gametitle=Dragon Ruby Input -version=0.0.21 +version=0.0.22 icon=metadata/icon.png # === Flags available at all licensing tiers === diff --git a/tests/tests.rb b/tests/tests.rb index 6e229ef..d9b0bac 100644 --- a/tests/tests.rb +++ b/tests/tests.rb @@ -1,28 +1,28 @@ -def xtest_calcstringbox_works_in_tests(_args, assert) +def test_calcstringbox_works_in_tests(_args, assert) w, h = $gtk.calcstringbox('1234567890', 0, '') assert.true! w > 0 assert.true! h > 0 end -def xtest_calcstringbox_new_line_has_no_width(_args, assert) +def test_calcstringbox_new_line_has_no_width(_args, assert) w, h = $gtk.calcstringbox("\n", 0, '') assert.equal! w, 0.0 assert.equal! h, 22.0 # Yep, it has a height end -def xtest_calcstringbox_double_new_line_has_no_width(_args, assert) +def test_calcstringbox_double_new_line_has_no_width(_args, assert) w, h = $gtk.calcstringbox("\n\n", 0, '') assert.equal! w, 0.0 assert.equal! h, 22.0 # Yep, it has a height end -def xtest_calcstringbox_with_day_roman_new_line_has_width(_args, assert) +def test_calcstringbox_with_day_roman_new_line_has_width(_args, assert) w, h = $gtk.calcstringbox("\n\n", 0, 'fonts/day-roman/DAYROM__.ttf') assert.false! w == 0.0 # :/ assert.false! h == 0.0 # :/ end -def xtest_calcstringbox_tab_has_no_witdh(_args, assert) +def test_calcstringbox_tab_has_no_witdh(_args, assert) w, h = $gtk.calcstringbox("\t", 0, '') assert.equal! w, 0.0 # Yep, it has no width :/ assert.equal! h, 22.0 # Yep, it has a height @@ -30,14 +30,14 @@ def xtest_calcstringbox_tab_has_no_witdh(_args, assert) # ---------------------- Util color tests --------------------- -def xtest_parse_color_integer_rgb(_args, assert) +def test_parse_color_integer_rgb(_args, assert) assert.equal! Input::Util.parse_color({ test_color: 0 }, 'test'), { r: 0, g: 0, b: 0, a: 255 } assert.equal! Input::Util.parse_color({ test_color: 0xFF0000 }, 'test'), { r: 255, g: 0, b: 0, a: 255 } assert.equal! Input::Util.parse_color({ test_color: 0x00FF00 }, 'test'), { r: 0, g: 255, b: 0, a: 255 } assert.equal! Input::Util.parse_color({ test_color: 0x0000FF }, 'test'), { r: 0, g: 0, b: 255, a: 255 } end -def xtest_parse_color_integer_rgba(_args, assert) +def test_parse_color_integer_rgba(_args, assert) # NOTE: For Integer (hex) rgba to work, there has to be a red component > 0 assert.equal! Input::Util.parse_color({ test_color: 0x01000000 }, 'test'), { r: 1, g: 0, b: 0, a: 0 } assert.equal! Input::Util.parse_color({ test_color: 0xFF000000 }, 'test'), { r: 255, g: 0, b: 0, a: 0 } @@ -45,47 +45,47 @@ def xtest_parse_color_integer_rgba(_args, assert) assert.equal! Input::Util.parse_color({ test_color: 0x0100FF00 }, 'test'), { r: 1, g: 0, b: 255, a: 0 } end -def xtest_parse_color_array_rgb(_args, assert) +def test_parse_color_array_rgb(_args, assert) assert.equal! Input::Util.parse_color({ test_color: [255, 0, 0] }, 'test'), { r: 255, g: 0, b: 0, a: 255 } assert.equal! Input::Util.parse_color({ test_color: [0, 255, 0] }, 'test'), { r: 0, g: 255, b: 0, a: 255 } assert.equal! Input::Util.parse_color({ test_color: [0, 0, 255] }, 'test'), { r: 0, g: 0, b: 255, a: 255 } end -def xtest_parse_color_array_rgb_da(_args, assert) +def test_parse_color_array_rgb_da(_args, assert) assert.equal! Input::Util.parse_color({ test_color: [255, 0, 0] }, 'test', da: 1), { r: 255, g: 0, b: 0, a: 1 } assert.equal! Input::Util.parse_color({ test_color: [0, 255, 0] }, 'test', da: 1), { r: 0, g: 255, b: 0, a: 1 } assert.equal! Input::Util.parse_color({ test_color: [0, 0, 255] }, 'test', da: 1), { r: 0, g: 0, b: 255, a: 1 } end -def xtest_parse_color_array_rgba(_args, assert) +def test_parse_color_array_rgba(_args, assert) assert.equal! Input::Util.parse_color({ test_color: [255, 0, 0, 0] }, 'test'), { r: 255, g: 0, b: 0, a: 0 } assert.equal! Input::Util.parse_color({ test_color: [0, 255, 0, 0] }, 'test'), { r: 0, g: 255, b: 0, a: 0 } assert.equal! Input::Util.parse_color({ test_color: [0, 0, 255, 0] }, 'test'), { r: 0, g: 0, b: 255, a: 0 } assert.equal! Input::Util.parse_color({ test_color: [0, 0, 0, 0] }, 'test'), { r: 0, g: 0, b: 0, a: 0 } end -def xtest_parse_color_hash_rgba(_args, assert) +def test_parse_color_hash_rgba(_args, assert) assert.equal! Input::Util.parse_color({ test_color: { r: 255, g: 0, b: 0, a: 0 } }, 'test'), { r: 255, g: 0, b: 0, a: 0 } assert.equal! Input::Util.parse_color({ test_color: { r: 0, g: 255, b: 0, a: 0 } }, 'test'), { r: 0, g: 255, b: 0, a: 0 } assert.equal! Input::Util.parse_color({ test_color: { r: 0, g: 0, b: 255, a: 0 } }, 'test'), { r: 0, g: 0, b: 255, a: 0 } assert.equal! Input::Util.parse_color({ test_color: { r: 0, g: 0, b: 0, a: 0 } }, 'test'), { r: 0, g: 0, b: 0, a: 0 } end -def xtest_parse_color_hash_component(_args, assert) +def test_parse_color_hash_component(_args, assert) assert.equal! Input::Util.parse_color({ test_color: { r: 255 } }, 'test'), { r: 255, g: 0, b: 0, a: 255 } assert.equal! Input::Util.parse_color({ test_color: { g: 255 } }, 'test'), { r: 0, g: 255, b: 0, a: 255 } assert.equal! Input::Util.parse_color({ test_color: { b: 255 } }, 'test'), { r: 0, g: 0, b: 255, a: 255 } assert.equal! Input::Util.parse_color({ test_color: { a: 0 } }, 'test'), { r: 0, g: 0, b: 0, a: 0 } end -def xtest_parse_color_nil(_args, assert) +def test_parse_color_nil(_args, assert) assert.equal! Input::Util.parse_color({}, 'test'), { r: 0, g: 0, b: 0, a: 255 } assert.equal! Input::Util.parse_color_nilable({}, 'test'), nil end # ---------------------- Word break tests --------------------- -def xtest_find_word_break_left(_args, assert) +def test_find_word_break_left(_args, assert) assert_finds_word_break_left(assert, '|word test', '|word test') assert_finds_word_break_left(assert, 'wo|rd test', '|word test') assert_finds_word_break_left(assert, 'word| test', '|word test') @@ -94,7 +94,7 @@ def xtest_find_word_break_left(_args, assert) assert_finds_word_break_left(assert, 'word test|', 'word |test') end -def xtest_find_word_break_right(_args, assert) +def test_find_word_break_right(_args, assert) assert_finds_word_break_right(assert, '|word test', 'word| test') assert_finds_word_break_right(assert, 'wo|rd test', 'word| test') assert_finds_word_break_right(assert, 'wor|d test', 'word| test') @@ -106,7 +106,7 @@ def xtest_find_word_break_right(_args, assert) # ---------------------- Current word tests ------------------- -def xtest_finds_current_word(_args, assert) +def test_finds_current_word(_args, assert) assert_current_word(assert, '|word test', nil) assert_current_word(assert, 'w|ord test', 'word') assert_current_word(assert, 'wor|d test', 'word') @@ -116,77 +116,123 @@ def xtest_finds_current_word(_args, assert) # ---------------------- Line wrap tests ---------------------- -def xtest_find_word_breaks_empty_value(_args, assert) +def test_find_word_breaks_empty_value(_args, assert) assert.equal! word_wrap_result(''), [''] end -def xtest_find_word_breaks_single_space(_args, assert) +def test_find_word_breaks_single_space(_args, assert) assert.equal! word_wrap_result(' '), [' '] end -def xtest_find_word_breaks_single_char(_args, assert) +def test_find_word_breaks_single_char(_args, assert) assert.equal! word_wrap_result('a'), ['a'] end -def xtest_multiline_word_breaks_two_words(_args, assert) +def test_multiline_word_breaks_two_words(_args, assert) assert.equal! word_wrap_result('Hello, world'), ['Hello, ', 'world'] end -def xtest_find_word_breaks_leading_and_trailing_white_space(_args, assert) +def test_find_word_breaks_leading_and_trailing_white_space(_args, assert) assert.equal! word_wrap_result(" \t hello \t "), [" \t hello \t "] end -def xtest_find_word_breaks_leading_and_trailing_white_space_multiple_words(_args, assert) +def test_find_word_breaks_leading_and_trailing_white_space_multiple_words(_args, assert) assert.equal! word_wrap_result(" \t hello, \t world \t"), [" \t hello, \t ", "world \t"] end -def xtest_multiline_word_breaks_trailing_new_line(_args, assert) +def test_multiline_word_breaks_trailing_new_line(_args, assert) assert.equal! word_wrap_result("hello, \n"), ['hello, ', "\n"] end -def xtest_multiline_word_breaks_new_line(_args, assert) +def test_multiline_word_breaks_new_line(_args, assert) assert.equal! word_wrap_result("hello, \n world"), ['hello, ', "\n world"] end -def xtest_multiline_word_breaks_double_new_line(_args, assert) +def test_multiline_word_breaks_double_new_line(_args, assert) assert.equal! word_wrap_result("hello, \n\n world"), ['hello, ', "\n", "\n world"] end -def xtest_multiline_word_breaks_multiple_new_lines(_args, assert) +def test_multiline_word_breaks_multiple_new_lines(_args, assert) assert.equal! word_wrap_result("hello, \n\n\n world"), ['hello, ', "\n", "\n", "\n world"] end -def xtest_perform_word_wrap_multiple_new_lines(_args, assert) +def test_perform_word_wrap_multiple_new_lines(_args, assert) assert.equal! word_wrap_result("1\n\n\n2"), ['1', "\n", "\n", "\n2"] end -def xtest_perform_word_wrap_trailing_new_line(_args, assert) +def test_perform_word_wrap_trailing_new_line(_args, assert) assert.equal! word_wrap_result("1\n"), ['1', "\n"] end -def xtest_find_word_breaks_trailing_new_line_after_wrap(_args, assert) +def test_find_word_breaks_trailing_new_line_after_wrap(_args, assert) assert.equal! word_wrap_result("1234567890 1234567890 1234567890\n"), ['1234567890 ', '1234567890 ', '1234567890', "\n"] end -def xtest_multiline_word_breaks_a_very_long_word(_args, assert) +def test_multiline_word_breaks_a_very_long_word(_args, assert) assert.equal! word_wrap_result('Supercalifragilisticexpialidocious'), ['Supercalif', 'ragilistic', 'expialidoc', 'ious'] end -def xtest_multiline_word_breaks_breaks_very_long_word_after_something_that_isnt(_args, assert) +def test_multiline_word_breaks_breaks_very_long_word_after_something_that_isnt(_args, assert) assert.equal! word_wrap_result('Super califragilisticexpialidocious'), ['Super ', 'califragil', 'isticexpia', 'lidocious'] end +# ---------------------- Max length tests ---------------------- + +def test_no_max_length(_args, assert) + input = build_text_input('1234567890', 10, 10) + input.insert('abc') + + assert.equal! input.value.to_s, '1234567890abc' + assert.equal! input.selection_end, 13 + assert.equal! input.selection_start, 13 +end + +def test_max_length(_args, assert) + input = build_text_input('1234567890', 10, 10, max_length: 10) + input.insert('abc') + + assert.equal! input.value.to_s, '1234567890' + assert.equal! input.selection_end, 10 + assert.equal! input.selection_start, 10 +end + +def test_max_length_inserts_as_much_as_possible(_args, assert) + input = build_text_input('1234567890', 10, 10, max_length: 11) + input.insert('abc') + + assert.equal! input.value.to_s, '1234567890a' + assert.equal! input.selection_end, 11 + assert.equal! input.selection_start, 11 +end + +def test_max_length_inserts_as_much_as_possible_in_the_middle(_args, assert) + input = build_text_input('1234567890', 5, 5, max_length: 11) + input.insert('abc') + + assert.equal! input.value.to_s, '12345a67890' + assert.equal! input.selection_end, 6 + assert.equal! input.selection_start, 6 +end + +def test_max_length_inserts_as_much_as_possible_in_the_middle_overwriting(_args, assert) + input = build_text_input('1234567890', 5, 6, max_length: 11) + input.insert('abc') + + assert.equal! input.value.to_s, '12345ab7890' + assert.equal! input.selection_end, 7 + assert.equal! input.selection_start, 7 +end # ---------------------- Font size calculation tests ---------------------- -def xtest_default_height_is_calculated_from_padding_and_font_height(_args, assert) +def test_default_height_is_calculated_from_padding_and_font_height(_args, assert) _, font_height = $gtk.calcstringbox('A', 0) text_input = Input::Text.new(padding: 10, size_enum: 0) assert.equal! text_input.h, font_height + 20 end -def xtest_multiline_scrolls_in_font_height_steps_by_default(args, assert) +def test_multiline_scrolls_in_font_height_steps_by_default(args, assert) $args = args _, font_height = $gtk.calcstringbox('A', 0) input = Input::Multiline.new(x: 100, y: 100, w: 100, size_enum: 0) @@ -203,7 +249,7 @@ def xtest_multiline_scrolls_in_font_height_steps_by_default(args, assert) assert.equal! input.scroll_y, font_height end -def xtest_text_click_inside_sets_selection(args, assert) +def test_text_click_inside_sets_selection(args, assert) $args = args three_letters_wide, _ = $gtk.calcstringbox('ABC', 0) input = Input::Text.new(x: 100, y: 100, w: 100, size_enum: 0, value: 'ABCDEF', focussed: true) @@ -219,7 +265,7 @@ def xtest_text_click_inside_sets_selection(args, assert) assert.equal! input.selection_end, 3 end -def xtest_text_drag_inside_sets_selection(args, assert) +def test_text_drag_inside_sets_selection(args, assert) $args = args three_letters_wide, _ = $gtk.calcstringbox('ABC', 0) six_letters_wide, _ = $gtk.calcstringbox('ABCDEF', 0) @@ -236,7 +282,7 @@ def xtest_text_drag_inside_sets_selection(args, assert) assert.equal! input.selection_end, 6 end -def xtest_multiline_click_inside_sets_selection(args, assert) +def test_multiline_click_inside_sets_selection(args, assert) $args = args three_letters_wide, font_height = $gtk.calcstringbox('ABC', 0) input = Input::Multiline.new(x: 100, y: 100, w: 100, h: font_height * 2, size_enum: 0, value: "ABCDEF\nGHIJKL", focussed: true) @@ -254,7 +300,7 @@ def xtest_multiline_click_inside_sets_selection(args, assert) assert.equal! input.selection_end, 10 end -def xtest_text_drag_inside_sets_selection(args, assert) +def test_text_drag_inside_sets_selection(args, assert) $args = args three_letters_wide, font_height = $gtk.calcstringbox('ABC', 0) input = Input::Multiline.new(x: 100, y: 100, w: 100, h: font_height * 2, size_enum: 0, value: "ABCDEF\nGHIJKL", focussed: true) @@ -275,14 +321,14 @@ def xtest_text_drag_inside_sets_selection(args, assert) # Two representative test cases using size_px instead of size_enum -def xtest_default_height_is_calculated_from_padding_and_font_height_size_px(_args, assert) +def test_default_height_is_calculated_from_padding_and_font_height_size_px(_args, assert) _, font_height = $gtk.calcstringbox('A', size_px: 30) text_input = Input::Text.new(padding: 10, size_px: 30) assert.equal! text_input.h, font_height + 20 end -def xtest_text_drag_inside_sets_selection_size_px(args, assert) +def test_text_drag_inside_sets_selection_size_px(args, assert) $args = args three_letters_wide, _ = $gtk.calcstringbox('ABC', size_px: 44) six_letters_wide, _ = $gtk.calcstringbox('ABCDEF', size_px: 44) @@ -301,7 +347,7 @@ def xtest_text_drag_inside_sets_selection_size_px(args, assert) # ---------------------- menu tests -------------------------- -def xtest_menu_constrains_selected_index(args, assert) +def test_menu_constrains_selected_index(args, assert) menu = Input::Menu.new(items: %w[1 2 3]) menu.selected_index = 0 @@ -391,8 +437,8 @@ def test_menu_calculates_menu_items_to_show_long(args, assert) # ---------------------- helper methods ---------------------- -def build_text_input(value, selection_start = 0, selection_end = selection_start) - input = Input::Text.new(value: value, selection_start: selection_start, selection_end: selection_end) +def build_text_input(value, selection_start = 0, selection_end = selection_start, **attr) + Input::Text.new(value: value, selection_start: selection_start, selection_end: selection_end, **attr) end def make_word_break_error(input, actual, expected)