From 0a8f302b0ae7aa61dbc5621a925b1a9f3da1f3da Mon Sep 17 00:00:00 2001 From: Rainer Kottenhoff Date: Sat, 7 Mar 2026 13:01:22 +0100 Subject: [PATCH] fix: drag-handle bug on resizable windows. --- minipath/src/Dialogs.c | 49 +- plans/replace_oniguruma_by_pcre2.md | 824 ---------------------------- src/Dialogs.c | 162 +++--- src/Dialogs.h | 1 + 4 files changed, 120 insertions(+), 916 deletions(-) delete mode 100644 plans/replace_oniguruma_by_pcre2.md diff --git a/minipath/src/Dialogs.c b/minipath/src/Dialogs.c index 30fe39892..cca920f53 100644 --- a/minipath/src/Dialogs.c +++ b/minipath/src/Dialogs.c @@ -395,17 +395,18 @@ INT_PTR CALLBACK GotoDlgProc(HWND hwnd,UINT umsg,WPARAM wParam,LPARAM lParam) cxClient = rc.right - rc.left; cyClient = rc.bottom - rc.top; - AdjustWindowRectEx(&rc,GetWindowLong(hwnd,GWL_STYLE)|WS_THICKFRAME,FALSE,0); + // Apply WS_THICKFRAME BEFORE computing borders so styles match + SetWindowLongPtr(hwnd,GWL_STYLE,GetWindowLongPtr(hwnd,GWL_STYLE)|WS_THICKFRAME); + SetWindowPos(hwnd,NULL,0,0,0,0,SWP_NOZORDER|SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED); + + AdjustWindowRectEx(&rc,GetWindowLong(hwnd,GWL_STYLE),FALSE,GetWindowLong(hwnd,GWL_EXSTYLE)); mmiPtMinX = rc.right-rc.left; mmiPtMaxY = rc.bottom-rc.top; - if (Settings.cxGotoDlg < (rc.right-rc.left)) { - Settings.cxGotoDlg = rc.right-rc.left; + if (Settings.cxGotoDlg < mmiPtMinX) { + Settings.cxGotoDlg = mmiPtMinX; } - SetWindowPos(hwnd,NULL,rc.left,rc.top, Settings.cxGotoDlg,rc.bottom-rc.top,SWP_NOZORDER); - - SetWindowLongPtr(hwnd,GWL_STYLE,GetWindowLongPtr(hwnd,GWL_STYLE)|WS_THICKFRAME); - SetWindowPos(hwnd,NULL,0,0,0,0,SWP_NOZORDER|SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED); + SetWindowPos(hwnd,NULL,0,0,Settings.cxGotoDlg,mmiPtMaxY,SWP_NOZORDER|SWP_NOMOVE); if (g_hDlgIconSmall) { SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)g_hDlgIconSmall); @@ -1717,17 +1718,18 @@ INT_PTR CALLBACK CopyMoveDlgProc(HWND hwnd,UINT umsg,WPARAM wParam,LPARAM lParam cxClient = rc.right - rc.left; cyClient = rc.bottom - rc.top; - AdjustWindowRectEx(&rc,GetWindowLong(hwnd,GWL_STYLE)|WS_THICKFRAME,FALSE,0); + // Apply WS_THICKFRAME BEFORE computing borders so styles match + SetWindowLongPtr(hwnd,GWL_STYLE,GetWindowLongPtr(hwnd,GWL_STYLE)|WS_THICKFRAME); + SetWindowPos(hwnd,NULL,0,0,0,0,SWP_NOZORDER|SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED); + + AdjustWindowRectEx(&rc,GetWindowLong(hwnd,GWL_STYLE),FALSE,GetWindowLong(hwnd,GWL_EXSTYLE)); mmiPtMinX = rc.right-rc.left; mmiPtMaxY = rc.bottom-rc.top; - if (Settings.cxCopyMoveDlg < (rc.right-rc.left)) { - Settings.cxCopyMoveDlg = rc.right-rc.left; + if (Settings.cxCopyMoveDlg < mmiPtMinX) { + Settings.cxCopyMoveDlg = mmiPtMinX; } - SetWindowPos(hwnd,NULL,rc.left,rc.top, Settings.cxCopyMoveDlg,rc.bottom-rc.top,SWP_NOZORDER); - - SetWindowLongPtr(hwnd,GWL_STYLE,GetWindowLongPtr(hwnd,GWL_STYLE)|WS_THICKFRAME); - SetWindowPos(hwnd,NULL,0,0,0,0,SWP_NOZORDER|SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED); + SetWindowPos(hwnd,NULL,0,0,Settings.cxCopyMoveDlg,mmiPtMaxY,SWP_NOZORDER|SWP_NOMOVE); if (g_hDlgIconSmall) { SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)g_hDlgIconSmall); @@ -2003,20 +2005,21 @@ INT_PTR CALLBACK OpenWithDlgProc(HWND hwnd,UINT umsg,WPARAM wParam,LPARAM lParam cxClient = rc.right - rc.left; cyClient = rc.bottom - rc.top; - AdjustWindowRectEx(&rc,GetWindowLong(hwnd,GWL_STYLE)|WS_THICKFRAME,FALSE,0); + // Apply WS_THICKFRAME BEFORE computing borders so styles match + SetWindowLongPtr(hwnd,GWL_STYLE,GetWindowLongPtr(hwnd,GWL_STYLE)|WS_THICKFRAME); + SetWindowPos(hwnd,NULL,0,0,0,0,SWP_NOZORDER|SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED); + + AdjustWindowRectEx(&rc,GetWindowLong(hwnd,GWL_STYLE),FALSE,GetWindowLong(hwnd,GWL_EXSTYLE)); mmiPtMinX = rc.right-rc.left; mmiPtMaxY = rc.bottom-rc.top; - if (Settings.cxOpenWithDlg < (rc.right-rc.left)) { - Settings.cxOpenWithDlg = rc.right-rc.left; + if (Settings.cxOpenWithDlg < mmiPtMinX) { + Settings.cxOpenWithDlg = mmiPtMinX; } - if (Settings.cyOpenWithDlg < (rc.bottom-rc.top)) { - Settings.cyOpenWithDlg = rc.bottom-rc.top; + if (Settings.cyOpenWithDlg < mmiPtMaxY) { + Settings.cyOpenWithDlg = mmiPtMaxY; } - SetWindowPos(hwnd,NULL,rc.left,rc.top, Settings.cxOpenWithDlg, Settings.cyOpenWithDlg,SWP_NOZORDER); - - SetWindowLongPtr(hwnd,GWL_STYLE,GetWindowLongPtr(hwnd,GWL_STYLE)|WS_THICKFRAME); - SetWindowPos(hwnd,NULL,0,0,0,0,SWP_NOZORDER|SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED); + SetWindowPos(hwnd,NULL,0,0,Settings.cxOpenWithDlg,Settings.cyOpenWithDlg,SWP_NOZORDER|SWP_NOMOVE); if (g_hDlgIconSmall) { SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)g_hDlgIconSmall); diff --git a/plans/replace_oniguruma_by_pcre2.md b/plans/replace_oniguruma_by_pcre2.md deleted file mode 100644 index ccfbc86ac..000000000 --- a/plans/replace_oniguruma_by_pcre2.md +++ /dev/null @@ -1,824 +0,0 @@ -# Plan: Replace Oniguruma by PCRE2 in Notepad3/Scintilla - -## Background & Motivation - -Oniguruma (https://github.com/kkos/oniguruma) was **archived on April 24, 2025** by its -maintainer K. Kosako. The repository is now read-only. No actively maintained community -fork exists. Security vulnerabilities will not be patched. - -**PCRE2** (https://github.com/PCRE2Project/pcre2) is the recommended replacement: -actively maintained, C library, UTF-8 native, feature-complete superset of Oniguruma -for Notepad3's use case. - ---- - -## Current Architecture - -### Scintilla Regex Engine Interface - -**File**: `scintilla/src/Document.h` (lines 107-119) - -```cpp -class RegexSearchBase { -public: - virtual ~RegexSearchBase() = default; - virtual Sci::Position FindText(Document *doc, Sci::Position minPos, Sci::Position maxPos, - const char *s, bool caseSensitive, bool word, bool wordStart, - Scintilla::FindOption flags, Sci::Position *length) = 0; - virtual const char *SubstituteByPosition(Document *doc, const char *text, - Sci::Position *length) = 0; -}; -extern RegexSearchBase *CreateRegexSearch(CharClassify *charClassTable); -``` - -### Factory Selection (compile-time) - -**File**: `scintilla/src/Document.cxx` (lines 3604-3610) - -```cpp -#ifndef SCI_OWNREGEX -RegexSearchBase *Scintilla::Internal::CreateRegexSearch(CharClassify *charClassTable) { - return new BuiltinRegex(charClassTable); // fallback: RESearch -} -#endif -``` - -When `SCI_OWNREGEX` is defined, the factory is provided by the custom engine file instead. - -### Current Oniguruma Implementation - -**File**: `scintilla/oniguruma/scintilla/OnigurumaRegExEngine.cxx` (904 lines) - -This file contains **two classes**: - -1. **`OnigurumaRegExEngine`** (line 162-244) — implements `RegexSearchBase` for Scintilla's - find/replace system. This is the main engine. -2. **`SimpleRegExEngine`** (line 753-805) — lightweight standalone engine used via the - exported `OnigRegExFind()` function for simple pattern matching outside Scintilla - (URL detection, lexer file matching). - -**Factory** (line 248-251): -```cpp -RegexSearchBase *Scintilla::Internal::CreateRegexSearch(CharClassify *charClassTable) { - return new OnigurumaRegExEngine(charClassTable); -} -``` - -**Exported C function** (line 888-901): -```cpp -extern "C" __declspec(dllexport) -ptrdiff_t WINAPI OnigRegExFind(const char *pchPattern, const char *pchText, - const bool caseSensitive, const int eolMode, int *matchLen_out); -``` - -### Callers of OnigRegExFind (outside Scintilla) - -**Declaration**: `src/SciCall.h` (line 107-108) -```cpp -ptrdiff_t WINAPI OnigRegExFind(const char *pchPattern, const char *pchText, - const bool caseSensitive, const int eolMode, int *matchLen_out); -``` - -**Call sites**: -- `src/Edit.c:2126` — URL validation via hyperlink regex pattern -- `src/Styles.c:2533` — Lexer file-path pattern matching - -Both call sites use forward-only search on small strings (not document buffers). - -### Build Configuration - -**Two vcxproj files** reference Oniguruma: - -#### `scintilla/Scintilla.vcxproj` (static library) -- Preprocessor: `NO_CXX11_REGEX;SCI_OWNREGEX;SCI_EMPTYCATALOGUE;ONIG_STATIC` -- Include dirs: `../oniguruma/src` -- Source files (lines 523-541): 19 Oniguruma `.c` files + `OnigurumaRegExEngine.cxx` -- Header files (lines 587-593): 7 Oniguruma `.h` files - -#### `scintilla/ScintillaDLL.vcxproj` (DLL) -- Preprocessor: `NO_CXX11_REGEX;SCI_OWNREGEX;ONIG_EXTERN=extern;SCINTILLA_DLL` -- Include dirs: `../oniguruma/src` -- Source files (lines 227-247): 20 Oniguruma `.c` files + `OnigurumaRegExEngine.cxx` -- Header files (lines 341-347): 7 Oniguruma `.h` files - -### Oniguruma Source Files (vendored) - -Located in `scintilla/oniguruma/src/`: -``` -ascii.c mktable.c onig_init.c regcomp.c regenc.c regerror.c -regexec.c regext.c reggnu.c regparse.c regposix.c regsyntax.c -regtrav.c regversion.c st.c unicode.c unicode_egcb_data.c -unicode_fold1_key.c unicode_fold2_key.c unicode_fold3_key.c -unicode_fold_data.c unicode_property_data.c unicode_property_data_posix.c -unicode_unfold_key.c unicode_wb_data.c utf8.c -``` - -Headers: `config.h oniggnu.h oniguruma.h regenc.h regint.h regparse.h st.h` - ---- - -## Notepad3 Regex Feature Usage (What Must Be Preserved) - -### Syntax features exposed to users via find/replace - -| Feature | Oniguruma syntax | PCRE2 equivalent | Action needed | -|---------|-----------------|------------------|---------------| -| Basic matching `.` `*` `+` `?` `{n,m}` | Same | Same | None | -| Character classes `[...]` | Same | Same | None | -| POSIX classes `[:alpha:]` | Same | Same | None | -| Alternation `\|` | Same | Same | None | -| Capturing groups `(...)` | Same | Same | None | -| Non-capturing groups `(?:...)` | Same | Same | None | -| Backreferences `\1`..`\99` | Same | Same | None | -| Named groups `(?...)` | Same | Same | None | -| Named groups `(?P...)` | Same | Same | None | -| Lookbehind `(?<=...)` `(?`** word boundaries | Oniguruma-specific | **Not supported** | **Translate** | -| **`\h` `\H`** horiz. space | Currently translated to char class | **Native in PCRE2** | **Simplify** | -| **`\uHHHH`** Unicode escape | Oniguruma-specific | `\x{HHHH}` in PCRE2 | **Translate** | - -### Replacement string features - -The replacement logic is **not** handled by Oniguruma itself — it's custom code in -`SubstituteByPosition()` (lines 458-522). It processes: -- `$1`..`$99` and `\1`..`\9` — numbered group references -- `${name}` and `$+{name}` — named group references (uses `onig_name_to_backref_number()`) -- `$$` and `\\` — literal escapes -- `\n \r \t \a \b \f \v` — escape sequences -- `\xHH` and `\uHHHH` — hex/Unicode escapes (converted to UTF-8 via `WideCharToMultiByte`) - -The `convertReplExpr()` method (lines 640-743) converts `\1`..`\9` to `$1`..`$9` format -and processes escape sequences into literal bytes before `SubstituteByPosition()` handles -group substitution. - -### Compile options mapping - -Current NP3 configuration in `SetSimpleOptions()` (lines 97-158): - -| Oniguruma option | PCRE2 equivalent | NP3 default | -|-----------------|------------------|-------------| -| `ONIG_OPTION_IGNORECASE` | `PCRE2_CASELESS` | OFF (case-sensitive) | -| `ONIG_OPTION_MULTILINE` (dot-matches-all) | `PCRE2_DOTALL` | OFF (enabled by `FindOption::DotMatchAll`) | -| `ONIG_OPTION_SINGLELINE` | (none needed) | OFF | -| `ONIG_OPTION_NOT_BEGIN_STRING` | `PCRE2_NOTBOL` | Dynamic (based on range position) | -| `ONIG_OPTION_NOT_END_STRING` | `PCRE2_NOTEOL` | Dynamic (based on range position) | - -Custom syntax modifications in constructor (lines 78-88): -- **Removed**: `ONIG_SYN_OP2_ESC_H_XDIGIT` (replaced `\h`/`\H` with custom char classes) -- **Added**: `ONIG_SYN_OP_ESC_LTGT_WORD_BEGIN_END` (enables `\<` `\>`) -- **Added**: `ONIG_SYN_OP2_ESC_U_HEX4` (enables `\uHHHH`) - ---- - -## Implementation Plan - -### Step 1: Obtain PCRE2 Sources - -Download PCRE2 source release from https://github.com/PCRE2Project/pcre2/releases -(latest stable, currently 10.44+). - -Create directory `scintilla/pcre2/` (parallel to existing `scintilla/oniguruma/`). - -Minimal files needed for static compilation: -``` -pcre2.h (public API — generated from pcre2.h.in) -pcre2_internal.h -pcre2_intmodedep.h -pcre2_ucp.h -pcre2_chartables.c (generated or use default) -pcre2_auto_possess.c -pcre2_chkdint.c -pcre2_compile.c -pcre2_config.c -pcre2_context.c -pcre2_dfa_match.c -pcre2_error.c -pcre2_extuni.c -pcre2_find_bracket.c -pcre2_maketables.c -pcre2_match.c -pcre2_match_data.c -pcre2_newline.c -pcre2_ord2utf.c -pcre2_pattern_info.c -pcre2_script_run.c -pcre2_string_utils.c -pcre2_study.c -pcre2_substitute.c -pcre2_substring.c -pcre2_tables.c -pcre2_ucd.c -pcre2_valid_utf.c -pcre2_xclass.c -``` - -Generate `config.h` and `pcre2.h` for MSVC (or use the CMake config step, or create -manually — PCRE2 provides `pcre2.h.generic` as a starting point). - -Key defines for `config.h`: -```c -#define PCRE2_CODE_UNIT_WIDTH 8 // UTF-8 mode -#define HAVE_MEMMOVE 1 -#define PCRE2_STATIC 1 // static linking -#define SUPPORT_UNICODE 1 // Unicode properties, scripts, \p{}, \X, etc. -#define SUPPORT_UCP 1 // Unicode character properties -#define SUPPORT_UTF 1 // UTF-8/16/32 support -#define BSR_ANYCRLF 0 // \R matches any Unicode newline by default -#define NEWLINE_DEFAULT 0 // 0 = any newline -#define LINK_SIZE 2 // internal link size -#define PARENS_NEST_LIMIT 250 // parentheses nesting limit -#define MATCH_LIMIT 10000000 // default match limit -#define MATCH_LIMIT_DEPTH 10000000 // default depth limit (prevents stack overflow) -#define MAX_NAME_SIZE 128 // max named group name length -#define MAX_NAME_COUNT 10000 // max number of named groups -``` - -### Step 2: Create PCRE2RegExEngine.cxx - -New file: `scintilla/pcre2/scintilla/PCRE2RegExEngine.cxx` - -Structure mirrors `OnigurumaRegExEngine.cxx` exactly: - -```cpp -// encoding: UTF-8 -/** - * @file PCRE2RegExEngine.cxx - * @brief integrate PCRE2 regex engine for Scintilla library - * Replaces Oniguruma (archived April 2025) - * - * uses PCRE2 - Perl Compatible Regular Expressions (v10.x) - * https://github.com/PCRE2Project/pcre2 - */ - -#ifdef SCI_OWNREGEX - -#include -#include -#include -#include -#include - -#define VC_EXTRALEAN 1 -#define NOMINMAX 1 -#include - -#include "Geometry.h" -#include "Platform.h" -#include "Scintilla.h" -#include "ScintillaTypes.h" -#include "ILexer.h" -#include "SplitVector.h" -#include "Partitioning.h" -#include "CellBuffer.h" -#include "CaseFolder.h" -#include "RunStyles.h" -#include "Decoration.h" -#include "CharClassify.h" -#include "CharacterCategoryMap.h" -#include "Document.h" - -#define PCRE2_CODE_UNIT_WIDTH 8 -#define PCRE2_STATIC -#include "pcre2.h" - -// ... implementation ... - -#endif //SCI_OWNREGEX -``` - -#### Class: PCRE2RegExEngine - -```cpp -class PCRE2RegExEngine : public RegexSearchBase { -public: - explicit PCRE2RegExEngine(CharClassify* charClassTable); - ~PCRE2RegExEngine() override; - - Sci::Position FindText(Document* doc, Sci::Position minPos, Sci::Position maxPos, - const char* pattern, bool caseSensitive, bool word, bool wordStart, - Scintilla::FindOption searchFlags, Sci::Position *length) override; - - const char* SubstituteByPosition(Document* doc, const char* text, - Sci::Position* length) override; - -private: - void clear(); - std::string translateRegExpr(const std::string& regExprStr, bool wholeWord, - bool wordStart, EndOfLine eolMode); - std::string convertReplExpr(const std::string& replStr); - - std::string m_RegExprStrg; - uint32_t m_CompileOptions; - pcre2_code* m_CompiledPattern; - pcre2_match_data* m_MatchData; - pcre2_match_context* m_MatchContext; - EOLmode m_EOLmode; - Sci::Position m_RangeBeg; - Sci::Position m_RangeEnd; - char m_ErrorInfo[256]; - Sci::Position m_MatchPos; - Sci::Position m_MatchLen; - -public: - std::string m_SubstBuffer; -}; -``` - -#### Member mapping (Oniguruma -> PCRE2) - -| Oniguruma member | PCRE2 member | Notes | -|-----------------|--------------|-------| -| `OnigRegex m_RegExpr` | `pcre2_code* m_CompiledPattern` | Compiled pattern handle | -| `OnigRegion m_Region` | `pcre2_match_data* m_MatchData` | Match result storage | -| `OnigSyntaxType m_OnigSyntax` | (not needed) | PCRE2 has fixed syntax | -| `OnigOptionType m_CmplOptions` | `uint32_t m_CompileOptions` | Compile-time flags | -| — | `pcre2_match_context* m_MatchContext` | **New**: needed for offset limit | - -#### Constructor - -```cpp -PCRE2RegExEngine::PCRE2RegExEngine(CharClassify* /*charClassTable*/) - : m_CompileOptions(PCRE2_UTF | PCRE2_UCP) // UTF-8 + Unicode properties always on - , m_CompiledPattern(nullptr) - , m_MatchData(nullptr) - , m_MatchContext(nullptr) - , m_EOLmode(EOLmode::UDEF) - , m_RangeBeg(-1) - , m_RangeEnd(-1) - , m_ErrorInfo() - , m_MatchPos(-1) - , m_MatchLen(0) -{ - m_MatchContext = pcre2_match_context_create(nullptr); - // Set match limit to prevent catastrophic backtracking - pcre2_set_match_limit(m_MatchContext, 10000000); - pcre2_set_depth_limit(m_MatchContext, 10000000); -} -``` - -#### Destructor - -```cpp -PCRE2RegExEngine::~PCRE2RegExEngine() { - clear(); - if (m_MatchContext) pcre2_match_context_free(m_MatchContext); -} -``` - -#### clear() - -```cpp -void PCRE2RegExEngine::clear() { - m_RegExprStrg.clear(); - if (m_MatchData) { pcre2_match_data_free(m_MatchData); m_MatchData = nullptr; } - if (m_CompiledPattern) { pcre2_code_free(m_CompiledPattern); m_CompiledPattern = nullptr; } - m_RangeBeg = -1; - m_RangeEnd = -1; - m_MatchPos = -1; - m_MatchLen = 0; -} -``` - -#### FindText() — Key implementation - -This is the most complex method. Key differences from Oniguruma: - -**1. Compile options assembly:** -```cpp -uint32_t options = PCRE2_UTF | PCRE2_UCP; // always UTF-8 + Unicode properties -if (!caseSensitive) options |= PCRE2_CASELESS; -if (FlagSet(searchFlags, FindOption::DotMatchAll)) options |= PCRE2_DOTALL; -``` - -**2. Pattern compilation:** -```cpp -int errorcode; -PCRE2_SIZE erroroffset; -m_CompiledPattern = pcre2_compile( - (PCRE2_SPTR)sRegExprStrg.c_str(), - sRegExprStrg.length(), - options, - &errorcode, - &erroroffset, - nullptr // default compile context -); -if (!m_CompiledPattern) { - pcre2_get_error_message(errorcode, (PCRE2_UCHAR*)m_ErrorInfo, sizeof(m_ErrorInfo)); - return SciPos(-2); -} -m_MatchData = pcre2_match_data_create_from_pattern(m_CompiledPattern, nullptr); -// Optional: JIT compile for performance -pcre2_jit_compile(m_CompiledPattern, PCRE2_JIT_COMPLETE); -``` - -**3. Search execution (FORWARD):** - -```cpp -// Get document buffer pointer -auto const docBegPtr = (PCRE2_SPTR)doc->BufferPointer(); -auto const docLen = (PCRE2_SIZE)doc->Length(); - -// Set offset limit to constrain search end -pcre2_set_offset_limit(m_MatchContext, (PCRE2_SIZE)rangeEnd); - -// Match options -uint32_t matchOptions = 0; -if (rangeBeg != 0) matchOptions |= PCRE2_NOTBOL; -if (rangeEnd != docLen) matchOptions |= PCRE2_NOTEOL; - -int rc = pcre2_match( - m_CompiledPattern, - docBegPtr, - docLen, - (PCRE2_SIZE)rangeBeg, // start offset - matchOptions, - m_MatchData, - m_MatchContext -); - -if (rc > 0) { - PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(m_MatchData); - m_MatchPos = SciPos(ovector[0]); - m_MatchLen = SciPos(ovector[1]) - m_MatchPos; -} -``` - -**4. Search execution (BACKWARD) — THE HARD PART:** - -PCRE2 has no native backward search. Oniguruma does this by swapping range pointers. -Implementation strategy: - -```cpp -// Backward search: iterate forward, keep last match within range -Sci::Position lastMatchPos = -1; -Sci::Position lastMatchLen = 0; -PCRE2_SIZE searchStart = (PCRE2_SIZE)rangeBeg; - -while (true) { - int rc = pcre2_match( - m_CompiledPattern, docBegPtr, docLen, - searchStart, matchOptions, - m_MatchData, m_MatchContext - ); - if (rc <= 0) break; - - PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(m_MatchData); - Sci::Position pos = SciPos(ovector[0]); - Sci::Position len = SciPos(ovector[1]) - pos; - - if (pos > rangeEnd) break; // past search range - if (pos <= minPos) { // within backward search range (minPos > maxPos) - lastMatchPos = pos; - lastMatchLen = len; - } - - // Advance past this match (at least 1 byte, handle UTF-8) - searchStart = ovector[1]; - if (searchStart == ovector[0]) { - // zero-length match — advance by one UTF-8 character - searchStart = doc->MovePositionOutsideChar(searchStart + 1, 1, true); - } -} -m_MatchPos = lastMatchPos; -m_MatchLen = lastMatchLen; -``` - -**Performance note**: For backward search in large files, this iterates all forward matches. -Optimization: restrict the search range using `pcre2_set_offset_limit()` to only search -within `[maxPos, minPos]` (the relevant backward range), then find the last match in that window. - -**Alternative backward search (line-by-line):** -Search backward line by line from `minPos`, stopping at `maxPos`. This avoids scanning -the entire document but is more complex to implement. Scintilla's built-in `BuiltinRegex` -uses this approach. - -#### SubstituteByPosition() — Nearly identical - -The replacement logic is custom and doesn't use Oniguruma APIs except for one call: -`onig_name_to_backref_number()` (line 499) to resolve named group references. - -**PCRE2 replacement**: Use `pcre2_substring_number_from_name()`: - -```cpp -// Where Oniguruma had: -int grpNum = onig_name_to_backref_number(m_RegExpr, name_beg, name_end, &m_Region); - -// PCRE2 equivalent: -int grpNum = pcre2_substring_number_from_name(m_CompiledPattern, (PCRE2_SPTR)name_str); -``` - -**Important**: The name must be null-terminated for PCRE2 (Oniguruma took begin/end pointers). -Extract the name into a temporary buffer first. - -For numbered group access, replace `m_Region.beg[n]`/`m_Region.end[n]` with: -```cpp -PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(m_MatchData); -auto rBeg = SciPos(ovector[2 * grpNum]); // was: m_Region.beg[grpNum] -auto rEnd = SciPos(ovector[2 * grpNum + 1]); // was: m_Region.end[grpNum] -auto len = rEnd - rBeg; -// Check for unset group: ovector[2*n] == PCRE2_UNSET -``` - -Number of capture groups: `pcre2_get_ovector_count(m_MatchData)` replaces `m_Region.num_regs`. - -The rest of `SubstituteByPosition()` and `convertReplExpr()` can be copied verbatim — -they are pure string processing with no Oniguruma dependencies. - -#### translateRegExpr() — Pattern translation - -```cpp -std::string PCRE2RegExEngine::translateRegExpr(const std::string& regExprStr, - bool wholeWord, bool wordStart, EndOfLine eolMode) -{ - std::string transRegExpr; - - // Word boundary wrapping (same as Oniguruma version) - if (wholeWord || wordStart) { - transRegExpr.push_back('\\'); - transRegExpr.push_back('b'); - transRegExpr.append(regExprStr); - if (wholeWord) { - transRegExpr.push_back('\\'); - transRegExpr.push_back('b'); - } - replaceAll(transRegExpr, ".", R"(\w)"); - } else { - transRegExpr.append(regExprStr); - } - - // PCRE2 does NOT support \< and \> — translate to lookaround equivalents - replaceAll(transRegExpr, R"(\<)", R"((?)", R"((?<=\w)(?!\w))"); // word end - // NOTE: must also handle escaped versions \\< and \\> (literal < >) — do NOT translate those - - // \h and \H are NATIVE in PCRE2 — no translation needed! - // (Oniguruma version had: replaceAll \h -> [^\S\n\v\f\r\u2028\u2029]) - // REMOVE those translations. - - // \uHHHH -> \x{HHHH} for PCRE2 - // This requires a smarter regex-based replacement (not simple string replace) - // to transform \uHHHH patterns to \x{HHHH} - // Implementation: scan for \u followed by exactly 4 hex digits - translateUnicodeEscapes(transRegExpr); // helper function - - return transRegExpr; -} -``` - -**Helper for `\uHHHH` -> `\x{HHHH}` translation:** -```cpp -static void translateUnicodeEscapes(std::string& s) { - std::string result; - result.reserve(s.length()); - for (size_t i = 0; i < s.length(); ++i) { - if (s[i] == '\\' && (i + 5) <= s.length() && s[i+1] == 'u' && - isxdigit(s[i+2]) && isxdigit(s[i+3]) && isxdigit(s[i+4]) && isxdigit(s[i+5])) { - result += "\\x{"; - result += s[i+2]; result += s[i+3]; result += s[i+4]; result += s[i+5]; - result += '}'; - i += 5; - } else { - result += s[i]; - } - } - s = result; -} -``` - -**Edge case warning for `\<` / `\>` translation**: The naive `replaceAll` approach will -also incorrectly translate escaped `\\<` (literal `<`). A proper implementation must -skip escaped backslashes. Consider scanning character by character instead of using -`replaceAll`. - -#### SimpleRegExEngine / OnigRegExFind export - -Replace with a PCRE2-based equivalent. This is simpler — forward-only, no document -buffer concerns: - -```cpp -class SimplePCRE2Engine { -public: - explicit SimplePCRE2Engine(EOLmode eolMode); - ~SimplePCRE2Engine(); - ptrdiff_t Find(const char* pattern, const char* text, - bool caseSensitive, int* matchLen_out = nullptr); -private: - EOLmode m_EOLmode; - pcre2_code* m_Code; - pcre2_match_data* m_MatchData; -}; - -// Exported function — keep same signature for binary compatibility -extern "C" __declspec(dllexport) -ptrdiff_t WINAPI OnigRegExFind(const char *pchPattern, const char *pchText, - const bool caseSensitive, const int eolMode, int *matchLen_out) { - SimplePCRE2Engine engine(static_cast(eolMode)); - return engine.Find(pchPattern, pchText, caseSensitive, matchLen_out); -} -``` - -**IMPORTANT**: Keep the exported function name as `OnigRegExFind` (despite using PCRE2) -to avoid changing `src/SciCall.h`, `src/Edit.c`, and `src/Styles.c`. The function -signature is the same. Consider renaming to `NP3RegExFind` in a follow-up if desired. - -### Step 3: Update Build Configuration - -#### scintilla/Scintilla.vcxproj - -**Preprocessor** — All configurations, change: -``` -ONIG_STATIC --> PCRE2_CODE_UNIT_WIDTH=8;PCRE2_STATIC;HAVE_CONFIG_H -``` -Keep `SCI_OWNREGEX` and `NO_CXX11_REGEX` unchanged. - -**Include directories** — All configurations, change: -``` -../oniguruma/src --> ../pcre2/src -``` - -**Source files** — Replace Oniguruma `.c` files with PCRE2 `.c` files: -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -**Header files** — Replace Oniguruma headers: -```xml - - - - - - - -``` - -#### scintilla/ScintillaDLL.vcxproj - -Same changes as above, but paths use `../pcre2/` prefix instead of `../oniguruma/`. - -**Preprocessor** change: `ONIG_EXTERN=extern` --> `PCRE2_CODE_UNIT_WIDTH=8;PCRE2_STATIC;HAVE_CONFIG_H` - -### Step 4: Optional — JIT Support - -PCRE2 includes an optional JIT compiler (`pcre2_jit_compile`) that can significantly -speed up repeated matches. Enable it by: - -1. Including `pcre2_jit_compile.c` in the build (already listed above) -2. Including the sljit sources that PCRE2 bundles (`sljit/` subdirectory) -3. Calling `pcre2_jit_compile(m_CompiledPattern, PCRE2_JIT_COMPLETE)` after compilation -4. Using `pcre2_jit_match()` instead of `pcre2_match()` for JIT-compiled patterns - -If JIT adds too much complexity or build size, it can be omitted — the interpreter -is already faster than Oniguruma for most patterns. - -### Step 5: Testing Checklist - -1. **Forward search**: Basic patterns, regex patterns, case-sensitive/insensitive -2. **Backward search**: Find Previous with regex enabled -3. **Replace**: `$1`, `${name}`, escape sequences in replacement -4. **Replace All**: Full document replacement -5. **Word boundary**: `\` patterns still work (via translation) -6. **Unicode**: `\p{L}`, `\p{N}`, Unicode text in documents -7. **Lookbehind**: `(?<=prefix)match` patterns -8. **Possessive**: `a++b` type patterns -9. **\h \H**: Horizontal space matching (now native, should be same behavior) -10. **\uHHHH**: Unicode escapes in patterns (translated to `\x{HHHH}`) -11. **\K**: Match reset patterns -12. **\R**: Newline sequence matching -13. **OnigRegExFind**: URL detection in Edit.c still works -14. **OnigRegExFind**: Lexer file pattern matching in Styles.c still works -15. **Large files**: Performance regression test with multi-MB files -16. **All platforms**: Build x86, x64, x64_AVX2, ARM64 - -### Step 6: Cleanup - -After confirming everything works: -1. Remove `scintilla/oniguruma/` directory entirely -2. Update `CLAUDE.md` vendored dependencies table -3. Update `.github/copilot-instructions.md` if it references Oniguruma -4. Consider renaming `OnigRegExFind` export to `NP3RegExFind` (requires updating - `src/SciCall.h:107`, `src/Edit.c:2126`, `src/Styles.c:2533`) - ---- - -## API Quick Reference - -### PCRE2 Core Functions (8-bit) - -```c -// Compilation -pcre2_code *pcre2_compile(PCRE2_SPTR pattern, PCRE2_SIZE length, - uint32_t options, int *errorcode, PCRE2_SIZE *erroroffset, - pcre2_compile_context *ccontext); -void pcre2_code_free(pcre2_code *code); - -// JIT (optional) -int pcre2_jit_compile(pcre2_code *code, uint32_t options); - -// Matching -pcre2_match_data *pcre2_match_data_create_from_pattern(const pcre2_code *code, - pcre2_general_context *gcontext); -void pcre2_match_data_free(pcre2_match_data *match_data); - -int pcre2_match(const pcre2_code *code, PCRE2_SPTR subject, PCRE2_SIZE length, - PCRE2_SIZE startoffset, uint32_t options, - pcre2_match_data *match_data, pcre2_match_context *mcontext); - -// Results -PCRE2_SIZE *pcre2_get_ovector_pointer(pcre2_match_data *match_data); -uint32_t pcre2_get_ovector_count(pcre2_match_data *match_data); - -// Named groups -int pcre2_substring_number_from_name(const pcre2_code *code, PCRE2_SPTR name); - -// Context (for offset limit, match limits) -pcre2_match_context *pcre2_match_context_create(pcre2_general_context *gcontext); -void pcre2_match_context_free(pcre2_match_context *mcontext); -int pcre2_set_offset_limit(pcre2_match_context *mcontext, PCRE2_SIZE value); -int pcre2_set_match_limit(pcre2_match_context *mcontext, uint32_t value); -int pcre2_set_depth_limit(pcre2_match_context *mcontext, uint32_t value); - -// Error messages -int pcre2_get_error_message(int errorcode, PCRE2_UCHAR *buffer, PCRE2_SIZE bufflen); -``` - -### Key PCRE2 Compile Options - -```c -PCRE2_UTF // Treat pattern and subject as UTF-8 -PCRE2_UCP // Use Unicode properties for \d, \w, \s, \b -PCRE2_CASELESS // Case-insensitive matching -PCRE2_DOTALL // . matches any character including newline -PCRE2_MULTILINE // ^ and $ match at line boundaries (NOT same as Onig MULTILINE!) -PCRE2_BSR_UNICODE // \R matches any Unicode newline sequence -``` - -**IMPORTANT naming confusion**: Oniguruma `ONIG_OPTION_MULTILINE` = "dot matches newline" -= PCRE2 `PCRE2_DOTALL`. PCRE2 `PCRE2_MULTILINE` = "^ $ match line boundaries" which -is a different concept. - ---- - -## Risk Assessment - -| Risk | Impact | Mitigation | -|------|--------|------------| -| Backward search performance | Medium | Start with iterate-forward approach; optimize later if needed | -| `\<` `\>` translation edge cases | Low | Thorough testing; character-by-character scanner | -| `\uHHHH` translation edge cases | Low | Simple pattern; well-defined 4-hex-digit format | -| PCRE2 compile/config complexity | Low | Use `.h.generic` files; minimal config | -| Binary size change | Low | PCRE2 is similar size to Oniguruma | -| User-visible regex syntax changes | Low | All features preserved; `\h`/`\H` now native (better) | - -## Files to Create/Modify Summary - -| Action | File | -|--------|------| -| **CREATE** | `scintilla/pcre2/src/` — PCRE2 source + headers | -| **CREATE** | `scintilla/pcre2/scintilla/PCRE2RegExEngine.cxx` | -| **MODIFY** | `scintilla/Scintilla.vcxproj` — swap source/header/define/include | -| **MODIFY** | `scintilla/ScintillaDLL.vcxproj` — swap source/header/define/include | -| **DELETE** | `scintilla/oniguruma/` — entire directory (after validation) | -| **Optional MODIFY** | `src/SciCall.h:107` — rename `OnigRegExFind` to `NP3RegExFind` | -| **Optional MODIFY** | `src/Edit.c:2126` — update function call name | -| **Optional MODIFY** | `src/Styles.c:2533` — update function call name | diff --git a/src/Dialogs.c b/src/Dialogs.c index cf729af04..14f19d4b9 100644 --- a/src/Dialogs.c +++ b/src/Dialogs.c @@ -256,7 +256,7 @@ static INT_PTR CALLBACK _InfoBoxLngDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, if (hIconBmp) { SetBitmapControl(hwnd, IDC_INFOBOXICON, hIconBmp); } - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); } return TRUE; @@ -775,7 +775,7 @@ static INT_PTR CALLBACK CmdLineHelpProc(HWND hwnd, UINT umsg, WPARAM wParam, LPA return TRUE; case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; #ifdef D_NP3_WIN10_DARK_MODE @@ -1086,7 +1086,7 @@ INT_PTR CALLBACK AboutDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM lParam SendDlgItemMessageW(hwnd, IDC_VERSION, WM_SETFONT, (WPARAM)hVersionFont, true); } - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); } break; @@ -1328,7 +1328,7 @@ static INT_PTR CALLBACK RunDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM l case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -1579,7 +1579,7 @@ static INT_PTR CALLBACK OpenWithDlgProc(HWND hwnd,UINT umsg,WPARAM wParam,LPARAM case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -1601,8 +1601,7 @@ static INT_PTR CALLBACK OpenWithDlgProc(HWND hwnd,UINT umsg,WPARAM wParam,LPARAM ResizeDlg_Size(hwnd,lParam,&dx,&dy); HDWP hdwp; - hdwp = BeginDeferWindowPos(6); - hdwp = DeferCtlPos(hdwp,hwnd,IDC_RESIZEGRIP,dx,dy,SWP_NOSIZE); + hdwp = BeginDeferWindowPos(5); hdwp = DeferCtlPos(hdwp,hwnd,IDOK,dx,dy,SWP_NOSIZE); hdwp = DeferCtlPos(hdwp,hwnd,IDCANCEL,dx,dy,SWP_NOSIZE); hdwp = DeferCtlPos(hdwp,hwnd,IDC_OPENWITHDIR,dx,dy,SWP_NOMOVE); @@ -1839,7 +1838,7 @@ static INT_PTR CALLBACK FavoritesDlgProc(HWND hwnd,UINT umsg,WPARAM wParam,LPARA case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -1862,8 +1861,7 @@ static INT_PTR CALLBACK FavoritesDlgProc(HWND hwnd,UINT umsg,WPARAM wParam,LPARA ResizeDlg_Size(hwnd,lParam,&dx,&dy); HDWP hdwp; - hdwp = BeginDeferWindowPos(6); - hdwp = DeferCtlPos(hdwp,hwnd,IDC_RESIZEGRIP,dx,dy,SWP_NOSIZE); + hdwp = BeginDeferWindowPos(5); hdwp = DeferCtlPos(hdwp,hwnd,IDOK,dx,dy,SWP_NOSIZE); hdwp = DeferCtlPos(hdwp,hwnd,IDCANCEL,dx,dy,SWP_NOSIZE); hdwp = DeferCtlPos(hdwp,hwnd,IDC_FAVORITESDIR,dx,dy,SWP_NOMOVE); @@ -2067,15 +2065,14 @@ static INT_PTR CALLBACK AddToFavDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, LPA case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); break; case WM_SIZE: { int dx; ResizeDlg_Size(hwnd, lParam, &dx, NULL); - HDWP hdwp = BeginDeferWindowPos(5); - hdwp = DeferCtlPos(hdwp, hwnd, IDC_RESIZEGRIP, dx, 0, SWP_NOSIZE); + HDWP hdwp = BeginDeferWindowPos(4); hdwp = DeferCtlPos(hdwp, hwnd, IDOK, dx, 0, SWP_NOSIZE); hdwp = DeferCtlPos(hdwp, hwnd, IDCANCEL, dx, 0, SWP_NOSIZE); hdwp = DeferCtlPos(hdwp, hwnd, IDC_FAVORITESDESCR, dx, 0, SWP_NOMOVE); @@ -2360,7 +2357,6 @@ static INT_PTR CALLBACK FileMRUDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, LPAR int dx, dy; ResizeDlg_Size(hwnd, lParam, &dx, &dy); HDWP hdwp = BeginDeferWindowPos(8); - hdwp = DeferCtlPos(hdwp, hwnd, IDC_RESIZEGRIP, dx, dy, SWP_NOSIZE); hdwp = DeferCtlPos(hdwp, hwnd, IDOK, dx, dy, SWP_NOSIZE); hdwp = DeferCtlPos(hdwp, hwnd, IDCANCEL, dx, dy, SWP_NOSIZE); hdwp = DeferCtlPos(hdwp, hwnd, IDC_REMOVE, dx, dy, SWP_NOSIZE); @@ -2375,7 +2371,7 @@ static INT_PTR CALLBACK FileMRUDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, LPAR return TRUE; case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; case WM_GETMINMAXINFO: @@ -2681,7 +2677,7 @@ static INT_PTR CALLBACK ChangeNotifyDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, return TRUE; case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; #ifdef D_NP3_WIN10_DARK_MODE @@ -2876,7 +2872,7 @@ static INT_PTR CALLBACK ColumnWrapDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, L case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -3020,7 +3016,7 @@ static INT_PTR CALLBACK WordWrapSettingsDlgProc(HWND hwnd, UINT umsg, WPARAM wPa case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -3160,7 +3156,7 @@ static INT_PTR CALLBACK LongLineSettingsDlgProc(HWND hwnd, UINT umsg, WPARAM wPa case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -3318,7 +3314,7 @@ static INT_PTR CALLBACK TabSettingsDlgProc(HWND hwnd,UINT umsg,WPARAM wParam,LPA case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -3489,7 +3485,7 @@ static INT_PTR CALLBACK SelectDefEncodingDlgProc(HWND hwnd, UINT umsg, WPARAM wP case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -3685,7 +3681,7 @@ static INT_PTR CALLBACK SelectEncodingDlgProc(HWND hwnd,UINT umsg,WPARAM wParam, case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -3707,8 +3703,7 @@ static INT_PTR CALLBACK SelectEncodingDlgProc(HWND hwnd,UINT umsg,WPARAM wParam, int dx, dy; ResizeDlg_Size(hwnd,lParam,&dx,&dy); - HDWP hdwp = BeginDeferWindowPos(4); - hdwp = DeferCtlPos(hdwp,hwnd,IDC_RESIZEGRIP,dx,dy,SWP_NOSIZE); + HDWP hdwp = BeginDeferWindowPos(3); hdwp = DeferCtlPos(hdwp,hwnd,IDOK,dx,dy,SWP_NOSIZE); hdwp = DeferCtlPos(hdwp,hwnd,IDCANCEL,dx,dy,SWP_NOSIZE); hdwp = DeferCtlPos(hdwp,hwnd,IDC_ENCODINGLIST,dx,dy,SWP_NOMOVE); @@ -3892,7 +3887,7 @@ static INT_PTR CALLBACK SelectDefLineEndingDlgProc(HWND hwnd,UINT umsg,WPARAM wP case WM_DPICHANGED: - UpdateWindowLayoutForDPI(hwnd, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hwnd, (RECT*)lParam, LOWORD(wParam)); return TRUE; @@ -5607,11 +5602,34 @@ void SetDlgPos(HWND hDlg, int xDlg, int yDlg) // // Resize Dialog Helpers() // +// KNOWN ISSUE: DPI round-trip drift on resizable dialogs +// +// When a resizable dialog is moved between monitors with different DPI, +// stored dimensions (cxClient, cyClient, mmiPtMin/Max, attrs) are kept +// in the init-DPI coordinate space. ResizeDlg_Size and +// ResizeDlg_GetMinMaxInfo convert them on-the-fly via MulDiv(value, +// currentDpi, pm->dpi). Because MulDiv rounds to nearest integer, +// repeated DPI transitions accumulate rounding errors: +// +// 96 DPI → 120 DPI → 96 DPI: +// 300 → MulDiv(300,120,96)=375 → MulDiv(375,96,120)=300 (exact) +// 96 DPI → 144 DPI → 96 DPI: +// 301 → MulDiv(301,144,96)=451 → MulDiv(451,96,144)=300 (off by 1) +// +// After many round-trips the dialog may grow or shrink by a few pixels +// and control layout can shift. ResizeDlg_DPIChanged does NOT update +// pm->dpi, so every conversion is relative to the original init DPI — +// this avoids compounding drift within a single DPI stay but cannot +// eliminate the MulDiv rounding error across transitions. +// +// A full fix would require scaling all bookkeeping values in +// ResizeDlg_DPIChanged (pm->dpi, mmiPtMin/Max, attrs) to the new DPI +// while leaving cxClient/cyClient unscaled, so that ResizeDlg_Size +// computes correct deltas. However, this was tested and worsened +// layout transitions in practice — needs further investigation. + #define RESIZEDLG_PROP_KEY L"ResizeDlg" #define MAX_RESIZEDLG_ATTR_COUNT 2 -// temporary fix for moving dialog to monitor with different DPI -// TODO: all dimensions no longer valid after window DPI changed. -#define NP3_ENABLE_RESIZEDLG_TEMP_FIX 1 typedef struct _resizeDlg { int direction; @@ -5622,6 +5640,8 @@ typedef struct _resizeDlg { int mmiPtMinY; int mmiPtMaxX; // only Y direction int mmiPtMaxY; // only X direction + HWND hwndGrip; + int cGrip; int attrs[MAX_RESIZEDLG_ATTR_COUNT]; } RESIZEDLG, * PRESIZEDLG; @@ -5639,9 +5659,12 @@ void ResizeDlg_InitEx(HWND hwnd, int cxFrame, int cyFrame, int nIdGrip, RSZ_DLG_ pm->cxClient = rc.right - rc.left; pm->cyClient = rc.bottom - rc.top; + // Apply the target style BEFORE computing sizes so borders match const DWORD style = (pm->direction < 0) ? (GetWindowStyle(hwnd) & ~WS_THICKFRAME) : (GetWindowStyle(hwnd) | WS_THICKFRAME); + SetWindowStyle(hwnd, style); + SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); - Scintilla_AdjustWindowRectForDpi((LPWRECT)&rc, style, 0, pm->dpi); + Scintilla_AdjustWindowRectForDpi((LPWRECT)&rc, style, GetWindowExStyle(hwnd), pm->dpi); pm->mmiPtMinX = rc.right - rc.left; pm->mmiPtMinY = rc.bottom - rc.top; @@ -5661,10 +5684,7 @@ void ResizeDlg_InitEx(HWND hwnd, int cxFrame, int cyFrame, int nIdGrip, RSZ_DLG_ SetProp(hwnd, RESIZEDLG_PROP_KEY, (HANDLE)pm); - SetWindowPos(hwnd, NULL, rc.left, rc.top, cxFrame, cyFrame, SWP_NOZORDER); - - SetWindowStyle(hwnd, style); - SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); + SetWindowPos(hwnd, NULL, 0, 0, cxFrame, cyFrame, SWP_NOZORDER | SWP_NOMOVE); WCHAR wch[MIDSZ_BUFFER]; GetMenuString(GetSystemMenu(GetParent(hwnd), FALSE), SC_SIZE, wch, COUNTOF(wch), MF_BYCOMMAND); @@ -5672,11 +5692,14 @@ void ResizeDlg_InitEx(HWND hwnd, int cxFrame, int cyFrame, int nIdGrip, RSZ_DLG_ InsertMenu(GetSystemMenu(hwnd, FALSE), SC_CLOSE, MF_BYCOMMAND | MF_SEPARATOR, 0, NULL); if (pm->direction >= 0) { - HWND const hwndCtl = GetDlgItem(hwnd, nIdGrip); - if (hwndCtl) { - SetWindowStyle(hwndCtl, GetWindowStyle(hwndCtl) | SBS_SIZEGRIP | WS_CLIPSIBLINGS); - int const cGrip = Scintilla_GetSystemMetricsForDpi(SM_CXHTHUMB, pm->dpi); - SetWindowPos(hwndCtl, NULL, pm->cxClient - cGrip, pm->cyClient - cGrip, cGrip, cGrip, SWP_NOZORDER); + pm->hwndGrip = GetDlgItem(hwnd, nIdGrip); + if (pm->hwndGrip) { + SetWindowStyle(pm->hwndGrip, GetWindowStyle(pm->hwndGrip) | SBS_SIZEGRIP | WS_CLIPSIBLINGS); + pm->cGrip = Scintilla_GetSystemMetricsForDpi(SM_CXHTHUMB, pm->dpi); + GetClientRect(hwnd, &rc); + pm->cxClient = rc.right - rc.left; + pm->cyClient = rc.bottom - rc.top; + SetWindowPos(pm->hwndGrip, NULL, pm->cxClient - pm->cGrip, pm->cyClient - pm->cGrip, pm->cGrip, pm->cGrip, SWP_NOZORDER); } } } @@ -5705,7 +5728,6 @@ void ResizeDlg_Size(HWND hwnd, LPARAM lParam, int* cx, int* cy) if (pm) { const int cxClient = LOWORD(lParam); const int cyClient = HIWORD(lParam); -#if NP3_ENABLE_RESIZEDLG_TEMP_FIX const UINT dpi = Scintilla_GetWindowDPI(hwnd); const UINT old = pm->dpi; if (cx) { @@ -5717,16 +5739,10 @@ void ResizeDlg_Size(HWND hwnd, LPARAM lParam, int* cx, int* cy) // store in original DPI. pm->cxClient = MulDiv(cxClient, old, dpi); pm->cyClient = MulDiv(cyClient, old, dpi); -#else - if (cx) { - *cx = cxClient - pm->cxClient; + // position size grip absolutely at bottom-right corner + if (pm->hwndGrip) { + SetWindowPos(pm->hwndGrip, NULL, cxClient - pm->cGrip, cyClient - pm->cGrip, pm->cGrip, pm->cGrip, SWP_NOZORDER | SWP_NOACTIVATE); } - if (cy) { - *cy = cyClient - pm->cyClient; - } - pm->cxClient = cxClient; - pm->cyClient = cyClient; -#endif } } @@ -5734,7 +5750,6 @@ void ResizeDlg_GetMinMaxInfo(HWND hwnd, LPARAM lParam) { LPCRESIZEDLG pm = (LPCRESIZEDLG)GetProp(hwnd, RESIZEDLG_PROP_KEY); LPMINMAXINFO lpmmi = (LPMINMAXINFO)lParam; -#if NP3_ENABLE_RESIZEDLG_TEMP_FIX UINT const dpi = Scintilla_GetWindowDPI(hwnd); UINT const old = pm->dpi; @@ -5751,23 +5766,37 @@ void ResizeDlg_GetMinMaxInfo(HWND hwnd, LPARAM lParam) lpmmi->ptMaxTrackSize.x = MulDiv(pm->mmiPtMaxX, dpi, old); break; } -#else - lpmmi->ptMinTrackSize.x = pm->mmiPtMinX; - lpmmi->ptMinTrackSize.y = pm->mmiPtMinY; +} - // only one direction - switch (pm->direction) { - case RSZ_ONLY_X: - lpmmi->ptMaxTrackSize.y = pm->mmiPtMaxY; - break; +void ResizeDlg_DPIChanged(HWND hwnd, const RECT* pNewRect, UINT newDpi) +{ + PRESIZEDLG pm = (PRESIZEDLG)GetProp(hwnd, RESIZEDLG_PROP_KEY); + if (pm) { + UINT const oldDpi = pm->dpi; - case RSZ_ONLY_Y: - lpmmi->ptMaxTrackSize.x = pm->mmiPtMaxX; - break; + // Clamp suggested rect to scaled minimum (SetWindowPos doesn't enforce WM_GETMINMAXINFO) + RECT rc = *pNewRect; + int const newMinX = MulDiv(pm->mmiPtMinX, newDpi, oldDpi); + int const newMinY = MulDiv(pm->mmiPtMinY, newDpi, oldDpi); + if ((rc.right - rc.left) < newMinX) { + rc.right = rc.left + newMinX; + } + if ((rc.bottom - rc.top) < newMinY) { + rc.bottom = rc.top + newMinY; + } + + // Update grip size for new DPI + if (pm->hwndGrip) { + pm->cGrip = Scintilla_GetSystemMetricsForDpi(SM_CXHTHUMB, newDpi); + } + + UpdateWindowLayoutForDPI(hwnd, &rc, newDpi); + } else { + UpdateWindowLayoutForDPI(hwnd, pNewRect, newDpi); } -#endif } + void ResizeDlg_SetAttr(HWND hwnd, int index, int value) { if (index < MAX_RESIZEDLG_ATTR_COUNT) { @@ -5804,14 +5833,9 @@ int ResizeDlg_CalcDeltaY2(HWND hwnd, int dy, int cy, int nCtlId1, int nCtlId2) return MulDiv(dy, cy, 100); } const LPCRESIZEDLG pm = (LPCRESIZEDLG)GetProp(hwnd, RESIZEDLG_PROP_KEY); -#if NP3_ENABLE_RESIZEDLG_TEMP_FIX UINT const dpi = Scintilla_GetWindowDPI(hwnd); int const hMinX = MulDiv(pm->attrs[0], dpi, pm->dpi); int const hMinY = MulDiv(pm->attrs[1], dpi, pm->dpi); -#else - int const hMinX = pm->attrs[0]; - int const hMinY = pm->attrs[1]; -#endif int const h1 = GetDlgCtrlHeight(hwnd, nCtlId1); int const h2 = GetDlgCtrlHeight(hwnd, nCtlId2); // cy + h1 >= hMin1 cy >= hMin1 - h1 @@ -6516,7 +6540,7 @@ void UpdateWindowLayoutForDPI(HWND hwnd, const RECT *pNewRect, const UINT dpi) GetWindowRect(hwnd, &rc); //~MapWindowPoints(NULL, hWnd, (LPPOINT)&rc, 2); UINT const _dpi = (dpi < (USER_DEFAULT_SCREEN_DPI >> 2)) ? Scintilla_GetWindowDPI(hwnd) : dpi; - Scintilla_AdjustWindowRectForDpi((LPWRECT)&rc, uWndFlags, 0, _dpi); + Scintilla_AdjustWindowRectForDpi((LPWRECT)&rc, GetWindowStyle(hwnd), GetWindowExStyle(hwnd), _dpi); SetWindowPos(hwnd, NULL, rc.left, rc.top, (rc.right - rc.left), (rc.bottom - rc.top), uWndFlags); } RedrawWindow(hwnd, NULL, NULL, RDW_FRAME | RDW_INVALIDATE | RDW_ERASE | RDW_INTERNALPAINT | RDW_ALLCHILDREN | RDW_UPDATENOW); @@ -6687,7 +6711,7 @@ INT_PTR CALLBACK FontDialogHookProc( case WM_DPICHANGED: dpi = LOWORD(wParam); //dpi.y = HIWORD(wParam); - UpdateWindowLayoutForDPI(hdlg, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hdlg, (RECT*)lParam, LOWORD(wParam)); int const ctl[] = { cmb1, cmb2, cmb3, cmb4, cmb5 }; for (int i = 0; i < COUNTOF(ctl); ++i) { HFONT const hFont = (HFONT)SendMessage(GetDlgItem(hdlg, ctl[i]), WM_GETFONT, 0, 0); @@ -6791,7 +6815,7 @@ INT_PTR CALLBACK ColorDialogHookProc( case WM_DPICHANGED: dpi = LOWORD(wParam); //dpi.y = HIWORD(wParam); - UpdateWindowLayoutForDPI(hdlg, (RECT*)lParam, LOWORD(wParam)); + ResizeDlg_DPIChanged(hdlg, (RECT*)lParam, LOWORD(wParam)); int const ctl[] = { COLOR_ADD, COLOR_MIX, IDOK, IDCANCEL }; for (int i = 0; i < COUNTOF(ctl); ++i) { HFONT const hFont = (HFONT)SendMessage(GetDlgItem(hdlg, ctl[i]), WM_GETFONT, 0, 0); diff --git a/src/Dialogs.h b/src/Dialogs.h index c9eb7c1a9..60472e7ec 100644 --- a/src/Dialogs.h +++ b/src/Dialogs.h @@ -166,6 +166,7 @@ inline void ResizeDlg_InitY(HWND hwnd, int cyFrame, int nIdGrip) void ResizeDlg_Destroy(HWND hwnd, int* cxFrame, int* cyFrame); void ResizeDlg_Size(HWND hwnd, LPARAM lParam, int* cx, int* cy); void ResizeDlg_GetMinMaxInfo(HWND hwnd, LPARAM lParam); +void ResizeDlg_DPIChanged(HWND hwnd, const RECT* pNewRect, UINT newDpi); #define MAX_RESIZEDLG_ATTR_COUNT 2 void ResizeDlg_SetAttr(HWND hwnd, int index, int value);