From 689a46625c495fa950b66a2dae9a53af618d61e8 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 23 Jan 2026 18:43:23 -0600 Subject: [PATCH 1/7] Add word wrap support for more unicode whitespace characters. --- build/foo_openlyrics.vcxproj | 1 + build/foo_openlyrics.vcxproj.filters | 3 +++ src/locale_init.cpp | 13 +++++++++++++ src/lyric_auto_edit.cpp | 6 +++--- src/ui_lyric_editor.cpp | 2 +- src/ui_lyrics_externalwindow.cpp | 2 +- src/ui_lyrics_panel.cpp | 6 +++--- src/win32_util.cpp | 24 ++++++++++++++++++++++++ src/win32_util.h | 3 +++ 9 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 src/locale_init.cpp diff --git a/build/foo_openlyrics.vcxproj b/build/foo_openlyrics.vcxproj index 8bb955b3..f30af9e9 100644 --- a/build/foo_openlyrics.vcxproj +++ b/build/foo_openlyrics.vcxproj @@ -224,6 +224,7 @@ + diff --git a/build/foo_openlyrics.vcxproj.filters b/build/foo_openlyrics.vcxproj.filters index e3eea598..73c62536 100644 --- a/build/foo_openlyrics.vcxproj.filters +++ b/build/foo_openlyrics.vcxproj.filters @@ -195,6 +195,9 @@ Source Files + + Source Files + diff --git a/src/locale_init.cpp b/src/locale_init.cpp new file mode 100644 index 00000000..a152634d --- /dev/null +++ b/src/locale_init.cpp @@ -0,0 +1,13 @@ +#include "stdafx.h" +#include + +class LocaleInit : public initquit +{ +public: + void on_init() + { + std::setlocale(LC_ALL, ""); + } +}; + +static initquit_factory_t g_locale_init_factory; diff --git a/src/lyric_auto_edit.cpp b/src/lyric_auto_edit.cpp index 992640c2..cbfc1701 100644 --- a/src/lyric_auto_edit.cpp +++ b/src/lyric_auto_edit.cpp @@ -49,7 +49,7 @@ static std::optional RemoveRepeatedSpaces(const LyricData& lyrics) size_t search_start = 0; while(search_start < line.text.length()) { - size_t next_space = line.text.find_first_of(_T(' '), search_start); + size_t next_space = find_first_space(line.text, true, search_start); // NOTE: If the line was empty we would not enter this loop. // We subtract 1 from the length to avoid overflowing when next_space == npos == (size_t)-1 @@ -60,7 +60,7 @@ static std::optional RemoveRepeatedSpaces(const LyricData& lyrics) } size_t erase_start = next_space + 1; - size_t erase_end = line.text.find_first_not_of(_T(' '), erase_start); + size_t erase_end = find_first_space(line.text, false, erase_start); if((erase_end != std::tstring::npos) && (erase_end > erase_start)) { @@ -90,7 +90,7 @@ static std::optional RemoveRepeatedBlankLines(const LyricData& lyrics LyricData new_lyrics = lyrics; for(auto iter = new_lyrics.lines.begin(); iter != new_lyrics.lines.end(); /*Omitted*/) { - size_t first_non_space = iter->text.find_first_not_of(' '); + size_t first_non_space = find_first_space(iter->text, false); bool is_blank = (first_non_space == std::tstring::npos); if(is_blank && previous_blank) { diff --git a/src/ui_lyric_editor.cpp b/src/ui_lyric_editor.cpp index fef84e93..a1885c27 100644 --- a/src/ui_lyric_editor.cpp +++ b/src/ui_lyric_editor.cpp @@ -326,7 +326,7 @@ void LyricEditor::SelectLineWithTimestampGreaterOrEqual(double threshold_timesta line_buffer_len); // EM_GETLINE reads the first word as the number of characters in the buffer LRESULT chars_copied = SendDlgItemMessage(IDC_LYRIC_TEXT, EM_GETLINE, i, (LPARAM)line_buffer); std::string linestr = from_tstring(std::tstring_view { line_buffer, (size_t)chars_copied }); - if(linestr.empty() || ((linestr.length() == 1) && (linestr[0] == ' '))) continue; + if(linestr.empty() || ((linestr.length() == 1) && (_istspace(linestr[0]) > 0))) continue; if(parsers::lrc::is_tag_line(linestr)) continue; double line_timestamp = parsers::lrc::get_line_first_timestamp(linestr); diff --git a/src/ui_lyrics_externalwindow.cpp b/src/ui_lyrics_externalwindow.cpp index 2d1a6116..ddadc051 100644 --- a/src/ui_lyrics_externalwindow.cpp +++ b/src/ui_lyrics_externalwindow.cpp @@ -428,7 +428,7 @@ static int _WrapSimpleLyricsLineToRect(D2DTextRenderContext& render, // We do this once now (before allocating anything dependent on string length) // and then since we don't ever move the "end" of the string, we assume that line // doesn't end in a space for the rest of the function. - size_t last_not_space = line.find_last_not_of(_T(' ')); + size_t last_not_space = find_last_space(line, false); if(last_not_space == std::tstring_view::npos) { return line_height; // Our line is exclusively whitespace diff --git a/src/ui_lyrics_panel.cpp b/src/ui_lyrics_panel.cpp index d418b62f..bba28fef 100644 --- a/src/ui_lyrics_panel.cpp +++ b/src/ui_lyrics_panel.cpp @@ -447,10 +447,10 @@ static int _WrapSimpleLyricsLineToRect(HDC dc, CRect clip_rect, std::tstring_vie int total_height = 0; while(text_outstanding.length() > 0) { - size_t leading_spaces = text_outstanding.find_first_not_of(_T(' ')); + size_t leading_spaces = find_first_space(text_outstanding, false); text_outstanding.remove_prefix(std::min(leading_spaces, text_outstanding.size())); - size_t last_not_space = text_outstanding.find_last_not_of(_T(' ')); + size_t last_not_space = find_last_space(text_outstanding, false); if(last_not_space != std::tstring_view::npos) { size_t trailing_spaces = text_outstanding.length() - 1 - last_not_space; @@ -476,7 +476,7 @@ static int _WrapSimpleLyricsLineToRect(HDC dc, CRect clip_rect, std::tstring_vie else { assert(chars_to_draw > 0); - const int previous_space_index = int(text_outstanding.rfind(' ', chars_to_draw - 1)); + const int previous_space_index = int(find_last_space(text_outstanding, true, chars_to_draw - 1)); if(previous_space_index == std::tstring::npos) { // There is a single word that doesn't fit on the line diff --git a/src/win32_util.cpp b/src/win32_util.cpp index ecba2ff3..e32ebbca 100644 --- a/src/win32_util.cpp +++ b/src/win32_util.cpp @@ -158,6 +158,30 @@ std::tstring normalise_utf8(std::tstring_view input) return result; } +size_t find_first_space(const std::tstring_view str, bool positive, size_t pos) +{ + const auto it = std::find_if(std::next(str.begin(), pos), + str.end(), + [positive](TCHAR c) { return _istspace(c) > 0 == positive; }); + + if(it == str.end()) return std::tstring_view::npos; + + return it - str.begin(); +} + +size_t find_last_space(const std::tstring_view str, bool positive, size_t pos) +{ + size_t offset = 0; + if(pos != std::tstring_view::npos) offset = str.length() - pos - 1; + + const auto it = std::find_if(std::next(str.rbegin(), offset), + str.rend(), + [positive](TCHAR c) { return std::_istspace(c) > 0 == positive; }); + if(it == str.rend()) return std::tstring_view::npos; + + return str.rend() - it - 1; +} + bool hr_success(HRESULT result, const char* filename, int line_number) { const bool success = (result == S_OK); diff --git a/src/win32_util.h b/src/win32_util.h index 0c804079..23636a5c 100644 --- a/src/win32_util.h +++ b/src/win32_util.h @@ -29,5 +29,8 @@ std::string from_tstring(const std::tstring& string); std::tstring normalise_utf8(std::tstring_view input); +size_t find_first_space(const std::tstring_view str, bool positive = true, size_t pos = 0); +size_t find_last_space(const std::tstring_view str, bool positive = true, size_t pos = std::tstring_view::npos); + #define HR_SUCCESS(hr) hr_success(hr, __FILE__, __LINE__) bool hr_success(HRESULT result, const char* filename, int line_number); From 974f8c5605c588d09fad86f1494e259279c06efa Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 27 Jan 2026 19:35:14 -0600 Subject: [PATCH 2/7] Extract whitespace detection to is_char_whitespace function --- src/ui_lyric_editor.cpp | 2 +- src/win32_util.cpp | 11 +++++++++-- src/win32_util.h | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ui_lyric_editor.cpp b/src/ui_lyric_editor.cpp index a1885c27..243f6389 100644 --- a/src/ui_lyric_editor.cpp +++ b/src/ui_lyric_editor.cpp @@ -326,7 +326,7 @@ void LyricEditor::SelectLineWithTimestampGreaterOrEqual(double threshold_timesta line_buffer_len); // EM_GETLINE reads the first word as the number of characters in the buffer LRESULT chars_copied = SendDlgItemMessage(IDC_LYRIC_TEXT, EM_GETLINE, i, (LPARAM)line_buffer); std::string linestr = from_tstring(std::tstring_view { line_buffer, (size_t)chars_copied }); - if(linestr.empty() || ((linestr.length() == 1) && (_istspace(linestr[0]) > 0))) continue; + if(linestr.empty() || ((linestr.length() == 1) && is_char_whitespace(linestr[0]))) continue; if(parsers::lrc::is_tag_line(linestr)) continue; double line_timestamp = parsers::lrc::get_line_first_timestamp(linestr); diff --git a/src/win32_util.cpp b/src/win32_util.cpp index e32ebbca..62886c5f 100644 --- a/src/win32_util.cpp +++ b/src/win32_util.cpp @@ -158,11 +158,18 @@ std::tstring normalise_utf8(std::tstring_view input) return result; } +bool is_char_whitespace(TCHAR c) +{ + // U+00A0 and U+202F are non-breaking spaces. + // U+180E was classified as a space when microsoft first defined isspace, but was later removed from the standard. + return (_istspace(c) > 0) && (c != L'\u00A0') && (c != L'\u202F') && (c != L'\u180E'); +} + size_t find_first_space(const std::tstring_view str, bool positive, size_t pos) { const auto it = std::find_if(std::next(str.begin(), pos), str.end(), - [positive](TCHAR c) { return _istspace(c) > 0 == positive; }); + [positive](TCHAR c) { return is_char_whitespace(c) == positive; }); if(it == str.end()) return std::tstring_view::npos; @@ -176,7 +183,7 @@ size_t find_last_space(const std::tstring_view str, bool positive, size_t pos) const auto it = std::find_if(std::next(str.rbegin(), offset), str.rend(), - [positive](TCHAR c) { return std::_istspace(c) > 0 == positive; }); + [positive](TCHAR c) { return is_char_whitespace(c) == positive; }); if(it == str.rend()) return std::tstring_view::npos; return str.rend() - it - 1; diff --git a/src/win32_util.h b/src/win32_util.h index 23636a5c..0c3d850c 100644 --- a/src/win32_util.h +++ b/src/win32_util.h @@ -29,6 +29,7 @@ std::string from_tstring(const std::tstring& string); std::tstring normalise_utf8(std::tstring_view input); +bool is_char_whitespace(TCHAR c); size_t find_first_space(const std::tstring_view str, bool positive = true, size_t pos = 0); size_t find_last_space(const std::tstring_view str, bool positive = true, size_t pos = std::tstring_view::npos); From c823be87cded0ee30ab892e11f34b8dcf9a76bbd Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 27 Jan 2026 19:46:07 -0600 Subject: [PATCH 3/7] Split find_space positive/negative parameter into find/find_non functions --- src/lyric_auto_edit.cpp | 6 +++--- src/ui_lyrics_externalwindow.cpp | 2 +- src/ui_lyrics_panel.cpp | 6 +++--- src/win32_util.cpp | 32 ++++++++++++++++++++++++-------- src/win32_util.h | 6 ++++-- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/lyric_auto_edit.cpp b/src/lyric_auto_edit.cpp index cbfc1701..ccf1fb8b 100644 --- a/src/lyric_auto_edit.cpp +++ b/src/lyric_auto_edit.cpp @@ -49,7 +49,7 @@ static std::optional RemoveRepeatedSpaces(const LyricData& lyrics) size_t search_start = 0; while(search_start < line.text.length()) { - size_t next_space = find_first_space(line.text, true, search_start); + size_t next_space = find_first_whitespace(line.text, search_start); // NOTE: If the line was empty we would not enter this loop. // We subtract 1 from the length to avoid overflowing when next_space == npos == (size_t)-1 @@ -60,7 +60,7 @@ static std::optional RemoveRepeatedSpaces(const LyricData& lyrics) } size_t erase_start = next_space + 1; - size_t erase_end = find_first_space(line.text, false, erase_start); + size_t erase_end = find_first_nonwhitespace(line.text, erase_start); if((erase_end != std::tstring::npos) && (erase_end > erase_start)) { @@ -90,7 +90,7 @@ static std::optional RemoveRepeatedBlankLines(const LyricData& lyrics LyricData new_lyrics = lyrics; for(auto iter = new_lyrics.lines.begin(); iter != new_lyrics.lines.end(); /*Omitted*/) { - size_t first_non_space = find_first_space(iter->text, false); + size_t first_non_space = find_first_nonwhitespace(iter->text); bool is_blank = (first_non_space == std::tstring::npos); if(is_blank && previous_blank) { diff --git a/src/ui_lyrics_externalwindow.cpp b/src/ui_lyrics_externalwindow.cpp index ddadc051..de798e45 100644 --- a/src/ui_lyrics_externalwindow.cpp +++ b/src/ui_lyrics_externalwindow.cpp @@ -428,7 +428,7 @@ static int _WrapSimpleLyricsLineToRect(D2DTextRenderContext& render, // We do this once now (before allocating anything dependent on string length) // and then since we don't ever move the "end" of the string, we assume that line // doesn't end in a space for the rest of the function. - size_t last_not_space = find_last_space(line, false); + size_t last_not_space = find_last_nonwhitespace(line); if(last_not_space == std::tstring_view::npos) { return line_height; // Our line is exclusively whitespace diff --git a/src/ui_lyrics_panel.cpp b/src/ui_lyrics_panel.cpp index bba28fef..5c83c6fd 100644 --- a/src/ui_lyrics_panel.cpp +++ b/src/ui_lyrics_panel.cpp @@ -447,10 +447,10 @@ static int _WrapSimpleLyricsLineToRect(HDC dc, CRect clip_rect, std::tstring_vie int total_height = 0; while(text_outstanding.length() > 0) { - size_t leading_spaces = find_first_space(text_outstanding, false); + size_t leading_spaces = find_first_nonwhitespace(text_outstanding); text_outstanding.remove_prefix(std::min(leading_spaces, text_outstanding.size())); - size_t last_not_space = find_last_space(text_outstanding, false); + size_t last_not_space = find_last_nonwhitespace(text_outstanding); if(last_not_space != std::tstring_view::npos) { size_t trailing_spaces = text_outstanding.length() - 1 - last_not_space; @@ -476,7 +476,7 @@ static int _WrapSimpleLyricsLineToRect(HDC dc, CRect clip_rect, std::tstring_vie else { assert(chars_to_draw > 0); - const int previous_space_index = int(find_last_space(text_outstanding, true, chars_to_draw - 1)); + const int previous_space_index = int(find_last_whitespace(text_outstanding, chars_to_draw - 1)); if(previous_space_index == std::tstring::npos) { // There is a single word that doesn't fit on the line diff --git a/src/win32_util.cpp b/src/win32_util.cpp index 62886c5f..2288edbb 100644 --- a/src/win32_util.cpp +++ b/src/win32_util.cpp @@ -165,25 +165,41 @@ bool is_char_whitespace(TCHAR c) return (_istspace(c) > 0) && (c != L'\u00A0') && (c != L'\u202F') && (c != L'\u180E'); } -size_t find_first_space(const std::tstring_view str, bool positive, size_t pos) +size_t find_first_whitespace(const std::tstring_view str, size_t pos) { - const auto it = std::find_if(std::next(str.begin(), pos), - str.end(), - [positive](TCHAR c) { return is_char_whitespace(c) == positive; }); + const auto it = std::find_if(std::next(str.begin(), pos), str.end(), is_char_whitespace); if(it == str.end()) return std::tstring_view::npos; return it - str.begin(); } -size_t find_last_space(const std::tstring_view str, bool positive, size_t pos) +size_t find_first_nonwhitespace(const std::tstring_view str, size_t pos) +{ + const auto it = std::find_if_not(std::next(str.begin(), pos), str.end(), is_char_whitespace); + + if(it == str.end()) return std::tstring_view::npos; + + return it - str.begin(); +} + +size_t find_last_whitespace(const std::tstring_view str, size_t pos) +{ + size_t offset = 0; + if(pos != std::tstring_view::npos) offset = str.length() - pos - 1; + + const auto it = std::find_if(std::next(str.rbegin(), offset), str.rend(), is_char_whitespace); + if(it == str.rend()) return std::tstring_view::npos; + + return str.rend() - it - 1; +} + +size_t find_last_nonwhitespace(const std::tstring_view str, size_t pos) { size_t offset = 0; if(pos != std::tstring_view::npos) offset = str.length() - pos - 1; - const auto it = std::find_if(std::next(str.rbegin(), offset), - str.rend(), - [positive](TCHAR c) { return is_char_whitespace(c) == positive; }); + const auto it = std::find_if_not(std::next(str.rbegin(), offset), str.rend(), is_char_whitespace); if(it == str.rend()) return std::tstring_view::npos; return str.rend() - it - 1; diff --git a/src/win32_util.h b/src/win32_util.h index 0c3d850c..d5eb64df 100644 --- a/src/win32_util.h +++ b/src/win32_util.h @@ -30,8 +30,10 @@ std::string from_tstring(const std::tstring& string); std::tstring normalise_utf8(std::tstring_view input); bool is_char_whitespace(TCHAR c); -size_t find_first_space(const std::tstring_view str, bool positive = true, size_t pos = 0); -size_t find_last_space(const std::tstring_view str, bool positive = true, size_t pos = std::tstring_view::npos); +size_t find_first_whitespace(const std::tstring_view str, size_t pos = 0); +size_t find_first_nonwhitespace(const std::tstring_view str, size_t pos = 0); +size_t find_last_whitespace(const std::tstring_view str, size_t pos = std::tstring_view::npos); +size_t find_last_nonwhitespace(const std::tstring_view str, size_t pos = std::tstring_view::npos); #define HR_SUCCESS(hr) hr_success(hr, __FILE__, __LINE__) bool hr_success(HRESULT result, const char* filename, int line_number); From 59da7f866cf500fffa6c7b98a9968f01baaf2837 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 27 Jan 2026 19:48:09 -0600 Subject: [PATCH 4/7] Remove unnecessary locale_init.cpp --- build/foo_openlyrics.vcxproj | 1 - build/foo_openlyrics.vcxproj.filters | 3 --- src/locale_init.cpp | 13 ------------- 3 files changed, 17 deletions(-) delete mode 100644 src/locale_init.cpp diff --git a/build/foo_openlyrics.vcxproj b/build/foo_openlyrics.vcxproj index f30af9e9..8bb955b3 100644 --- a/build/foo_openlyrics.vcxproj +++ b/build/foo_openlyrics.vcxproj @@ -224,7 +224,6 @@ - diff --git a/build/foo_openlyrics.vcxproj.filters b/build/foo_openlyrics.vcxproj.filters index 73c62536..e3eea598 100644 --- a/build/foo_openlyrics.vcxproj.filters +++ b/build/foo_openlyrics.vcxproj.filters @@ -195,9 +195,6 @@ Source Files - - Source Files - diff --git a/src/locale_init.cpp b/src/locale_init.cpp deleted file mode 100644 index a152634d..00000000 --- a/src/locale_init.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "stdafx.h" -#include - -class LocaleInit : public initquit -{ -public: - void on_init() - { - std::setlocale(LC_ALL, ""); - } -}; - -static initquit_factory_t g_locale_init_factory; From d66377e68eb9c5427c3af74532bd0f8ca0fb493c Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 28 Jan 2026 22:43:21 -0600 Subject: [PATCH 5/7] Add bounds checking for find_whitespace functions --- src/win32_util.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/win32_util.cpp b/src/win32_util.cpp index 2288edbb..33f95e85 100644 --- a/src/win32_util.cpp +++ b/src/win32_util.cpp @@ -167,6 +167,9 @@ bool is_char_whitespace(TCHAR c) size_t find_first_whitespace(const std::tstring_view str, size_t pos) { + // match behavior of std::string_view::find_first_of + if(pos >= str.length() || str.empty()) return std::tstring_view::npos; + const auto it = std::find_if(std::next(str.begin(), pos), str.end(), is_char_whitespace); if(it == str.end()) return std::tstring_view::npos; @@ -176,6 +179,9 @@ size_t find_first_whitespace(const std::tstring_view str, size_t pos) size_t find_first_nonwhitespace(const std::tstring_view str, size_t pos) { + // match behavior of std::string_view::find_first_not_of + if(pos >= str.length() || str.empty()) return std::tstring_view::npos; + const auto it = std::find_if_not(std::next(str.begin(), pos), str.end(), is_char_whitespace); if(it == str.end()) return std::tstring_view::npos; @@ -185,8 +191,10 @@ size_t find_first_nonwhitespace(const std::tstring_view str, size_t pos) size_t find_last_whitespace(const std::tstring_view str, size_t pos) { + if(str.empty()) return std::tstring_view::npos; + size_t offset = 0; - if(pos != std::tstring_view::npos) offset = str.length() - pos - 1; + if(pos != std::tstring_view::npos && pos < str.length()) offset = str.length() - pos - 1; const auto it = std::find_if(std::next(str.rbegin(), offset), str.rend(), is_char_whitespace); if(it == str.rend()) return std::tstring_view::npos; @@ -196,8 +204,10 @@ size_t find_last_whitespace(const std::tstring_view str, size_t pos) size_t find_last_nonwhitespace(const std::tstring_view str, size_t pos) { + if(str.empty()) return std::tstring_view::npos; + size_t offset = 0; - if(pos != std::tstring_view::npos) offset = str.length() - pos - 1; + if(pos != std::tstring_view::npos && pos < str.length()) offset = str.length() - pos - 1; const auto it = std::find_if_not(std::next(str.rbegin(), offset), str.rend(), is_char_whitespace); if(it == str.rend()) return std::tstring_view::npos; From 33d7342f3660eb4527077bf90b3f9fa749a56458 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 28 Jan 2026 22:58:27 -0600 Subject: [PATCH 6/7] Add tests for new functions --- src/win32_util.cpp | 117 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/src/win32_util.cpp b/src/win32_util.cpp index 33f95e85..241d6b0f 100644 --- a/src/win32_util.cpp +++ b/src/win32_util.cpp @@ -267,4 +267,121 @@ MVTF_TEST(win32_string_narrow_to_wide_handles_ascii) const std::wstring output = std::wstring(output_buffer.data(), output_chars); ASSERT(output == L"test string!\nwith a newline :O"); } + +MVTF_TEST(win32_is_char_whitespace_true_for_breaking_whitespace) +{ + ASSERT(is_char_whitespace(L'\t')); + ASSERT(is_char_whitespace(L'\n')); + ASSERT(is_char_whitespace(L'\v')); + ASSERT(is_char_whitespace(L'\f')); + ASSERT(is_char_whitespace(L'\r')); + ASSERT(is_char_whitespace(L' ')); + + ASSERT(is_char_whitespace(L'\u0085')); // Next line + ASSERT(is_char_whitespace(L'\u1680')); // Ogham space mark + + ASSERT(is_char_whitespace(L'\u2000')); // En quad + ASSERT(is_char_whitespace(L'\u2001')); // Em quad + ASSERT(is_char_whitespace(L'\u2002')); // En space + ASSERT(is_char_whitespace(L'\u2003')); // Em space + ASSERT(is_char_whitespace(L'\u2004')); // Three-per-em space + ASSERT(is_char_whitespace(L'\u2005')); // Four-per-em space + ASSERT(is_char_whitespace(L'\u2006')); // Six-per-em space + ASSERT(is_char_whitespace(L'\u2007')); // Figure space + ASSERT(is_char_whitespace(L'\u2008')); // Punctuation space + ASSERT(is_char_whitespace(L'\u2009')); // Thin space + ASSERT(is_char_whitespace(L'\u200A')); // Hair space + + ASSERT(is_char_whitespace(L'\u2028')); // Line separator + ASSERT(is_char_whitespace(L'\u2029')); // Paragraph separator + ASSERT(is_char_whitespace(L'\u205F')); // Medium mathematical space + ASSERT(is_char_whitespace(L'\u3000')); // Ideographic space + + ASSERT(!is_char_whitespace(L'\u00A0')); // Non-breaking space + ASSERT(!is_char_whitespace(L'\u202F')); // Narrow non-breaking space + ASSERT(!is_char_whitespace(L'\u180E')); // Mongolian vowel separator + ASSERT(!is_char_whitespace(L'A')); + ASSERT(!is_char_whitespace(L'1')); + ASSERT(!is_char_whitespace(L'-')); +} + +MVTF_TEST(win32_find_first_whitespace_gives_correct_indices) +{ + std::tstring_view input = _T("Test string.\u3000Second sentence."); + ASSERT(find_first_whitespace(input) == 4); + ASSERT(find_first_whitespace(input, 4) == 4); + ASSERT(find_first_whitespace(input, 5) == 12); + ASSERT(find_first_whitespace(input, 23) == std::tstring_view::npos); + ASSERT(find_first_whitespace(input, 100) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_first_whitespace_empty_string) +{ + ASSERT(find_first_whitespace(_T("")) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_first_whitespace_no_whitespace) +{ + ASSERT(find_last_whitespace(_T("abcdef")) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_first_nonwhitespace_gives_correct_indices) +{ + std::tstring_view input = _T(" \u3000Test string. "); + ASSERT(find_first_nonwhitespace(input) == 4); + ASSERT(find_first_nonwhitespace(input, 4) == 4); + ASSERT(find_first_nonwhitespace(input, 10) == 12); + ASSERT(find_first_nonwhitespace(input, 20) == std::tstring_view::npos); + ASSERT(find_first_nonwhitespace(input, 100) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_first_nonwhitespace_empty_string) +{ + ASSERT(find_first_nonwhitespace(_T("")) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_first_nonwhitespace_no_nonwhitespace) +{ + ASSERT(find_last_nonwhitespace(_T(" ")) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_last_whitespace_gives_correct_indices) +{ + std::tstring_view input = _T("Test string.\u3000Second sentence."); + ASSERT(find_last_whitespace(input) == 19); + ASSERT(find_last_whitespace(input, 19) == 19); + ASSERT(find_last_whitespace(input, 15) == 12); + ASSERT(find_last_whitespace(input, 2) == std::tstring_view::npos); + ASSERT(find_last_whitespace(input, 100) == 19); +} + +MVTF_TEST(win32_find_last_whitespace_empty_string) +{ + ASSERT(find_last_whitespace(_T("")) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_last_whitespace_no_whitespace) +{ + ASSERT(find_last_whitespace(_T("abcdef")) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_last_nonwhitespace_gives_correct_indices) +{ + std::tstring_view input = _T(" \u3000Test string. "); + ASSERT(find_last_nonwhitespace(input) == 18); + ASSERT(find_last_nonwhitespace(input, 18) == 18); + ASSERT(find_last_nonwhitespace(input, 10) == 7); + ASSERT(find_last_nonwhitespace(input, 3) == std::tstring_view::npos); + ASSERT(find_last_nonwhitespace(input, 100) == 18); +} + +MVTF_TEST(win32_find_last_nonwhitespace_empty_string) +{ + ASSERT(find_last_nonwhitespace(_T("")) == std::tstring_view::npos); +} + +MVTF_TEST(win32_find_last_nonwhitespace_no_nonwhitespace) +{ + ASSERT(find_last_nonwhitespace(_T(" ")) == std::tstring_view::npos); +} #endif From 1d41ac2e3cbcacdabd545a3f430bc2831f71cb6b Mon Sep 17 00:00:00 2001 From: Cwazywierdo <42323315+Cwazywierdo@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:46:35 -0600 Subject: [PATCH 7/7] Update src/ui_lyrics_externalwindow.cpp Co-authored-by: Jacques Heunis --- src/ui_lyrics_externalwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui_lyrics_externalwindow.cpp b/src/ui_lyrics_externalwindow.cpp index de798e45..4a5ba638 100644 --- a/src/ui_lyrics_externalwindow.cpp +++ b/src/ui_lyrics_externalwindow.cpp @@ -427,7 +427,7 @@ static int _WrapSimpleLyricsLineToRect(D2DTextRenderContext& render, // Remove trailing whitespace // We do this once now (before allocating anything dependent on string length) // and then since we don't ever move the "end" of the string, we assume that line - // doesn't end in a space for the rest of the function. + // doesn't end in whitespace for the rest of the function. size_t last_not_space = find_last_nonwhitespace(line); if(last_not_space == std::tstring_view::npos) {