Menu β–Ύ β–΄

#623 TScroller scroll bars behave erratically

7
pending
GUI (79)
1
2026-10-20
2025-07-30
No

TScroller does not update the scroll bars reliably when the scroll range is changed.

For example, TCoolEdit will not display the vertical scroller when the number of text lines exceeds the number of the visible lines. Only after resizing the window the scrollbar will appear. When opening a file that doesn't fit into the window, the scroller is displayed.

To reproduce open a new edit window and type Enter until the cursor reaches the end of the window.

Related

Bugs: #615
Feature Requests: #266
Wiki: OWLNext_7_Series_Revision_History

Discussion

1 2 > >> (Page 1 of 2)
  • Erwin Lotter

    Erwin Lotter - 2025-08-04

    Although AdjustScroller() is called when a line is added, the scrollbar will not appear even if Scroller->SetRange applies an yRange well beyond the number of sreen lines.
    The problem is not related to TCoolScroller, it also occurs with TScroller.

     
    πŸ‘
    1

    Last edit: Erwin Lotter 2025-08-04
  • Erwin Lotter

    Erwin Lotter - 2025-10-14

    This issue has been fixed in branches/elotter-cooledit by [r8524].
    In fact, the fix is more of a workaround than a solution and could indicate that there is an issue with the underlying scroller handling.

     
    πŸ‘
    1

    Related

    Commit: [r8524]

  • Vidar Hasfjord

    Vidar Hasfjord - 2025-10-14
    • labels: --> GUI
    • summary: TCoolEdit not displaying the vertical scrollbar --> TScroller scroll bars behave erratically
    • Description has changed:

    Diff:

    --- old
    +++ new
    @@ -1,2 +1,5 @@
    -TCoolEdit will not display the vertical scroller when the number of text lines exceeds the number of the visible lines. Only after resizing the window the scrollbar will appear. When opening a file that doesn't fit into the window, the scroller is displayed.
    +TScroller does not update the scroll bars reliably when the scroll range is changed.
    +
    +For example, TCoolEdit will not display the vertical scroller when the number of text lines exceeds the number of the visible lines. Only after resizing the window the scrollbar will appear. When opening a file that doesn't fit into the window, the scroller is displayed.
    +
     To reproduce open a new edit window and type Enter until the cursor reaches the end of the window.
    
    • Group: unspecified --> 8
     
  • Vidar Hasfjord

    Vidar Hasfjord - 2025-10-14

    Thanks for creating the ticket and looking into this. I ran into the same problem on the trunk. At some point I had some elaborate code trying to circumvent the issue, but I decided to remove it, since it was quite brittle, and I concluded the issue originates in TScroller and should be fixed at source.

    I've updated the ticket title to reflect the source of the problem.

     
  • Erwin Lotter

    Erwin Lotter - 2025-10-20

    On introducing the horizontal scrollbar adjustment new issues appeared.
    I could solve most of them by skipping the Scroller->ShowWindow calls and hiding unnecessary bars by setting the scroll range to zero. The remaining issues are:

    1. If a bar gets activated while the other bar is already visible, its range will be ignored and the maximum range will be applied. Only after resizing the window the bars will be ok.

    2. After the horizontal bar is activated, the scroll range will be too small by approx. two characters. Only after resizing the window the scroller will be ok.

     
    πŸ‘
    1
  • Ognyan Chernokozhev

    Claude Code analysis:


    I analyzed bug #623 ("TScroller scroll bars behave erratically", open, milestone 8) against trunk and branches/elotter-cooledit (both at r8968). The cause is how TScroller and TWindow update the scroll bars, not TCoolEdit, which matches Vidar's view on the ticket. This comes from reading the code and the Win32 scroll-bar rules; I haven't run a repro.

    What the ticket says

    • Symptom: in a new TCoolEdit, press Enter until the text passes the bottom of the window. No vertical scroll bar appears until you resize. A file that's already too long when opened is fine.
    • Erwin's workaround (r8524, branch only): show/hide the bar by hand in AdjustScroller. Showing it only works after toggling it off and on again.
    • Later (Oct 2025): he switched to hiding bars by setting their range to 0. Two problems remain, and both fix themselves after a resize:
    • If one bar appears while the other is already visible, its range is ignored.
    • The horizontal range is about 2 characters too short.
    • r8825, which is linked to the ticket, only clears the read-only flag on "Save As". It looks mis-tagged.

    Root causes

    1. A scroll bar's page size and range are set in separate calls.

    • SetRange β†’ SetSBarRange sends only SIF_RANGE (scroller.cpp:135).
    • The page (SIF_PAGE) is set only in SetPageSize, which runs on WM_SIZE or SetUnits (scroller.cpp:62).
    • Windows decides whether to show a bar on every SetScrollInfo call, using whatever page and range it has at that moment. Because the two are set separately, each call can toggle the bar using half-updated state.
    • Trunk TCoolEdit::AdjustScroller calls SetUnits and then SetRange, so it toggles twice per update.

    2. Nested WM_SIZE messages are thrown away.

    • When a bar appears or disappears, Windows sends WM_SIZE immediately, while still inside SetScrollInfo. That is exactly when the page size needs recalculating.
    • Two static guards discard it instead:
    • TWindow::EvSize has static bool inScroller (window.cpp:1641).
    • The branch's TCoolTextWnd::AdjustScroller has static bool in_work, whose comment says it exists to block the EvSize triggered by its own SetRange.
    • Result: XPage/YPage, the editor's cached screen lines and characters, and the clamping in ScrollTo all keep the client size from before the bar changed.
    • This explains Erwin's remaining problems:
    • 2 characters short: a vertical bar is about 17 px wide, roughly 2 characters at 7–8 px. Erwin's +2; // kompensate bug in TScroller hack makes up for exactly this.
    • Second bar uses the wrong range: showing it changes the first bar's page, and that update is lost.
    • "Fixed after resize": a real resize is the first WM_SIZE that isn't nested.
    • Both guards are static, so they are shared by every window and editor instance, not just one. A child window's legitimate WM_SIZE can be dropped too.

    3. Smaller issues

    • SetSBarRange skips the update when only the range is unchanged. It never checks the page.
    • EvSize updates page and range but never calls ScrollTo to re-clamp the scroll position against the new page.
    • The branch's EvSize calls Inherited::EvSize before resetting ScreenLines/ScreenChars, so the base class works with stale values.
    • HasHScrollBar/HasVScrollBar are read once from the window style when the scroller is constructed, so later style changes are missed.
    • In r8524, ShowScrollBar runs before SetRange. The following SetScrollInfo (with the old page) can hide the bar again, which is probably why the "off/on twice" trick was needed.

    Suggested fix (in TScroller, trunk)

    1. Add one UpdateScrollBars() that works out the page from the current client size and sets range, page and position in a single SetScrollInfo(SIF_RANGE|SIF_PAGE|SIF_POS) call per bar. Have SetRange, SetUnits, SetPageSize, SetSBarRange and EvSize all go through it.
    2. Remove the static guards. After updating, check whether the client size changed; if it did, run the update again until it stops changing (at most 2–3 rounds, since the two bars affect each other). Use a per-instance flag if re-entry still needs guarding.
    3. Re-clamp the position with ScrollTo(XPos, YPos) after the page changes.
    4. Then remove the TCoolEdit workarounds: the manual ShowScrollBar, in_work, the +2, and the zero-range hiding. Windows already hides a bar when its page covers the whole range.
     
    πŸ˜•
    1
  • Ognyan Chernokozhev

    • status: open --> pending
    • assigned_to: Ognyan Chernokozhev
     
  • Ognyan Chernokozhev

    Fixed in [r8969].

     
    πŸ‘Ž
    1

    Related

    Commit: [r8969]

  • Erwin Lotter

    Erwin Lotter - 2026-10-15

    Can [r8969] be merged to 7?

     
    πŸ‘Ž
    1

    Related

    Commit: [r8969]

  • Vidar Hasfjord

    Vidar Hasfjord - 2026-10-15

    @elotter wrote:

    Can [r8969] be merged to 7?

    Have you tested it? I did some simple testing with OWLMaker after rebuilding it with this fix, and the scrollbars still behave kind of wonky.

     

    Related

    Commit: [r8969]

  • Erwin Lotter

    Erwin Lotter - 2026-10-15

    Have you tested it?

    No I didn't because I need it in branch-7 for doing so.

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2026-10-15

    No I didn't because I need it in branch-7 for doing so.

    Why? Just check out the trunk and you can build the library and all the examples in a few clicks in OWLMaker (or just open the new VS solution and build from there). Then you can explore latest changes easily.

     
  • Erwin Lotter

    Erwin Lotter - 2026-10-15

    I'd like to see what happens when I remove the workarounds in my version of TCoolEdit that will not compile with the trunk I fear.

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2026-10-15

    I'd like to see what happens when I remove the workarounds in my version of TCoolEdit that will not compile with the trunk I fear.

    Ah. That makes sense. Still, having a look at the developments on the trunk will do you good.

    (Of course, I'm only trying to get you involved on the trunk, now that I've retired from trunk duty myself. We need more human resources to keep the AI agents in check. :-))

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2026-10-15

    In collaboration with ChatGPT:


    The vital issues to observe:

    • Never use the current client area as the basis for deciding which scrollbars are needed. Its dimensions depend on which scrollbars are currently visible, making it a moving target.

    • Start with the potential client area with both scrollbars absent. Use this as the reference geometry to evaluate the possible outcomes.

    • Evaluate all four combinations: neither scrollbar, horizontal only, vertical only, and both. For each combination, calculate the resulting client dimensions and determine which scrollbars the content actually requires.

    • Select a self-consistent combination requiring the fewest scrollbars. A scrollbar reduces the available space in the other dimension, so both dimensions must be evaluated together.

    • Only then apply the chosen layout and set the scrollbar ranges, page sizes and positions using the final client dimensions.

    The key is to separate the visibility decision from the actual scrollbar updates. Don't let changes in scrollbar visibility trigger a chain of calculations based on successively changing client dimensions. Determine the correct layout first, then apply it.

    Note: GetClientRect reports the current client dimensions, dependent on currently visible scrollbars, not the potential dimensions with both scrollbars absent.

    Illustrative C++ logic

    Assuming baseWidth and baseHeight represent the client dimensions with both scrollbars absent, and contentWidth, contentHeight, vBarWidth and hBarHeight are measured in the same units:

    #include <cassert>
    
    struct TLayout
    {
      bool HasHorizontalBar;
      bool HasVerticalBar;
      int ClientWidth;
      int ClientHeight;
    };
    
    auto ChooseLayout(
      int baseWidth, int baseHeight,
      int contentWidth, int contentHeight,
      int vBarWidth, int hBarHeight)
      -> TLayout
    {
      // Try layouts in order of increasing scrollbar count.
      //
      for (auto count = 0; count <= 2; ++count)
      {
        for (auto mask = 0; mask < 4; ++mask)
        {
          const auto hb = (mask & 1) != 0;
          const auto vb = (mask & 2) != 0;
          if (int(hb) + int(vb) != count) continue;
    
          const auto w = baseWidth - (vb ? vBarWidth : 0);
          const auto h = baseHeight - (hb ? hBarHeight : 0);
          if (w < 0 || h < 0) continue;
    
          if (hb == (contentWidth > w) && vb == (contentHeight > h))
            return {hb, vb, w, h};
        }
      }
    
      // No consistent layout: investigate invalid assumptions or dimensions.
      //
      assert(false && "No self-consistent scrollbar layout");
      return {false, false, baseWidth, baseHeight};
    }
    

    This illustrates the layout calculation, not a drop-in OWLNext fix. The reference dimensions and scrollbar metrics must be obtained correctly for the actual window, DPI and scrollbar configuration. The selected visibility state must then be applied, and the actual client dimensions verified before setting page sizes and ranges.

     

    Last edit: Vidar Hasfjord 2026-10-15
  • Vidar Hasfjord

    Vidar Hasfjord - 2026-10-15

    Wow, Copilot says:

    Does TScroller actually need a visibility-decision algorithm at all? Windows already has one. If you provide a single coherent SCROLLINFO structure, Windows can determine whether the scrollbar should be enabled or effectively hidden.

    See documentation for SetScrollInfo.

    There is even the SIF_DISABLENOSCROLL flag:

    Disables the scroll bar instead of removing it, if the scroll bar's new parameters make the scroll bar unnecessary.

    So indeed, it appears the algorithm can entirely be left to Windows, if the parameters of the scrollbars are simply set correctly and completely.

     
  • Erwin Lotter

    Erwin Lotter - 2026-10-15

    So indeed, it appears the algorithm can entirely be left to Windows, ..

    That wouldn't be particularly surprising, considering that scrolling is such a fundamental aspect of windows.

     
    πŸ‘
    1
  • Vidar Hasfjord

    Vidar Hasfjord - 2026-10-15

    That wouldn't be particularly surprising, considering that scrolling is such a fundamental aspect of windows.

    True. The question is whether Windows makes it easy or not to set the parameters to get the right behaviour. I suspect it may not. If the correct page size depends on the available client area after subtracting space for scrollbars, then you have a circular dependency that needs to be broken. The algorithm ChatGPT provided does just that. If however, you could set the page size based on the potential area available sans scrollbars, then it would be pretty easy, since you wouldn't have to correct for the space taken by the scrollbars themselves.

    It would probably be easier to start from first principles than try to repair horrible code with reentrancy guards etc. and no clear underlying logic.

     
  • Ognyan Chernokozhev

    Claude Code:


    I've now tested r8969 in OWLMaker, comparing it with the code before the fix (r8968).

    Setup

    • I built two OWLMakers (Debug x64, v145) from a clean export of trunk r9075. One has r8969. The other has scroller.cpp/scroller.h reverted to r8968, the only commit that touched them, so the fix is the only difference.
    • A script drove the CoolEdit editor (TOWLMakerEdit):
      • File New, 30 Γ— Enter, then a 120-character line;
      • delete it all again;
      • add 30 lines plus a long line, so both bars are needed;
      • six resizes of the MDI child, maximize/restore, Page Down.
    • After each step, the script shrinks the window by 1 pixel and back. If that changes any scroll bar value (visibility, range, page or position), the state before the nudge was stale. That is the "only correct after resizing" symptom Erwin described.

    Results

    r8968 (before) r8969 (fix)
    Stale steps (state changed by the 1 px nudge) 6 / 22 0 / 22
    Position past the end of the range 0 0

    What goes wrong without the fix:

    1. Wrong page size after a bar disappears. After deleting lines, the horizontal bar was gone, but the vertical page still assumed it was there (20 vs. 19).
    2. Horizontal page off by about 3 characters after resizing (65 instead of 68, 88 instead of 85). This matches Erwin's "range about two characters too small".
    3. View and scroll bar out of step. Enlarge the window until no bar is needed, then shrink it. The scroll bar shows the top, but the view stays scrolled down (bar position 0, actual position 11). A 1 px resize makes the bar jump to 11.

    With r8969, every step settles correctly straight away.

    What this does not cover

    • Flicker. Windows doesn't send show/hide events for window scroll bars, so I couldn't measure bars briefly appearing and disappearing during an update.
    • Mouse wheel and dragging the thumb.
    • The original symptom. The vertical bar not appearing while you press Enter doesn't occur in trunk's TCoolEdit even without the fix. So that part may be specific to Erwin's branch.

    I also found an unrelated TCoolEdit issue: after you delete a long line, the horizontal bar stays visible until the line count changes. The range only drops to 87 against a page of 85. That's TCoolEdit's range calculation, not TScroller.

    Vidar, could you describe what looked wonky in your test? Was it flicker, the horizontal bar that doesn't go away, or something else, and with which steps? Then I can check it specifically.

    On the design: r8969 already decides visibility from the client area without any of our bars, in the same way as the ChooseLayout approach. It works out the bars needed step by step rather than evaluating all four combinations, which gives the same result. The re-entry guard and the repeat loop are only there as a safety net for nested WM_SIZE. SIF_DISABLENOSCROLL would remove the circular dependency, but bars would then always show (disabled when not needed), which changes how windows look. Without that flag, Windows already shows and hides the bars itself, but the page size still depends on whether the other bar is visible, so TScroller has to work out the layout anyway. I'm happy to look at a cleaner rewrite if we can name a case r8969 gets wrong.

    Erwin, I can merge r8969 to branch 7 so you can test it with your workarounds removed. Let me know if you'd like that.

    (Testing done with the help of Claude Code.)

     
    πŸ‘
    1
  • Ognyan Chernokozhev

    By the way, I have also not been able to reproduce the original reported issue: "To reproduce open a new edit window and type Enter until the cursor reaches the end of the window."

    I tried it in OWLMaker 2026.2.22.8642 - the latest published version - and opening a new editor window and pressing the Enter key till it reaches the end of the window and beyond, results in the vertical scroll bar being displayed.

     
    πŸ‘
    1
  • Erwin Lotter

    Erwin Lotter - 2026-10-17

    By the way, I have also not been able to reproduce the original reported issue: "To reproduce open a new edit window and type Enter until the cursor reaches the end of the window."

    I can still reproduce this issue in 7.0.21 when I replace my version of TCoolTextWnd::AdjustScroller() by the one from the trunk.
    But it is gone with [r8969], which I merged manually into my branches-7 working copy.

    A few quick tests have shown me that [r8969] generally works quite well.
    So far I noticed two minor issues:
    1.) Entering new lines in an empty window causes the invisible vertical bar to show up briefly. Once it becomes permanently active, however, it will not flicker anymore.
    I think that this can be avoided by testing the need of the vertical bar only if its close to getting visible.
    2.) When the horizontal bar is activated (by entering characters) the window content is not scrolled up even if the new horizontal bar hides the cursor line.
    I'm however not sure whether this should be handled by TScroller or by TCoolTextWnd.

     
    πŸ‘
    1

    Related

    Commit: [r8969]

    • Ognyan Chernokozhev

      Claude Code:


      Erwin, thanks for testing r8969 on branch 7. I've reproduced the flash and found the cause.

      The flash

      Windows clamps a scroll bar's stored page to the size of its range (nPage <= nMax - nMin + 1). While the vertical bar was hidden with its real range, the page Windows stored was only just big enough to keep it hidden. Branch 7's TCoolTextWnd::AdjustScroller calls SetRange on every edit, so each Enter grows the range by one line. That made the stored page small enough for Windows to show the bar, until r8969 re-applied the real page and hid it again. Setting range and page in either order flashes; only setting both in a single call avoids it. TScroller can't do that through its virtual SetScrollRange/SetScrollPage setters (which TFlatScroller overrides) without an interface change.

      The fix, as you did in your branch, hides a bar that isn't needed with an empty range (0..0). Whether a bar is needed is decided from the scroller's XRange/YRange, so the range stored in the bar no longer matters. Trunk's TCoolEdit never showed the flash because it only touches the scroller once the text is taller than the window.

      Results (test programs on a hidden desktop that log every scroll bar call)

      Test before r8969 r8969 new fix
      Branch 7 CoolEdit, 88 keystrokes (Enter, Backspace, Enter): steps with a flash 2 44 0
      OWLMaker editor (trunk): stale steps 6 / 22 0 / 22 0 / 22
      TCoolGrid (trunk): steps with wrong visibility, flash, stale state or capped range 30 19 0

      In the CoolEdit test, the bar is shown and hidden at exactly the same line counts as with r8969, just without the flashes in between.

      TCoolGrid

      TCoolGrid::TGridScroller had its own SetSBarRange override: an old copy of the base function that set only the range, capped at SHRT_MAX. It conflicts with the empty-range approach, and grids were already showing unneeded bars, flashing and going stale before r8969. Because the grid scrolls in pixels, the cap also stopped the thumb from reaching the end of grids taller or wider than 32,767 px (about 2,000 rows). I've removed the override. Branch 7 has the same one (TCoolGridScroller::SetSBarRange), so it must be removed in the same merge.

      Your second point, the content not scrolling to keep the cursor visible when the horizontal bar appears: TScroller doesn't know where the caret is, so I think this belongs in the editor. It should scroll to the caret when a scroll bar changes the client size, for example in EvSize. I'll look at that separately.

      The fix is committed as [r9088] in trunk. Erwin, if you merge it together with r8969 (and remove the grid override), please check whether the flash is gone in your editor too.

      (Investigation and testing done with the help of Claude Code.)

       
      πŸ‘
      1

      Related

      Commit: [r9088]

  • Ognyan Chernokozhev

    Claude Code:


    The fix is now in branch 7 as [r9089], merged manually from [r8969] and [r9088] in trunk.

    It is binary compatible: no headers have changed, and no members or virtual functions have been added or removed. To keep it that way, the implementation differs from trunk:

    • The logic is in the body of TScroller::SetSBarRange, not in a new private member function.
    • The re-entry guard uses a window property instead of a thread_local.
    • TCoolGridScroller::SetSBarRange is kept, but now just calls the base implementation (in trunk, it was removed).

    Tests (test programs on a hidden desktop that log every scroll bar call)

    • Binary compatibility: the same executables, built against the previous branch 7 headers and DLL import libraries, were run first with the old DLLs and then with the new ones:
    Same executables with… CoolEdit: steps with a flash (88 keystrokes) TCoolGrid: steps with problems
    previous DLLs 2 30
    new DLLs 0 0

    Both test programs derive from DLL classes (TCoolEditFile, TCoolGrid), so this checks class layout and vtables as well. The exported symbols of the owl and coolprj DLLs are unchanged.

    • Behaviour: the CoolEdit bar is shown and hidden at exactly the same line counts as in trunk.
    • Compilers: the changed files compile with MSVC (x64, x86) and C++Builder (bcc64, bcc32c).

    Erwin, since you merged r8969 into your branch 7 working copy by hand, please revert that and update to [r9089] instead. The r8969 header change isn't needed in branch 7. You can then remove your scroll bar workarounds, as in your earlier test. Please let me know whether the flash is gone in your editor too.

    Your other point, keeping the cursor visible when the horizontal bar appears, is still open. I think it belongs in the editor (EvSize), not in TScroller.

    (Investigation and testing done with the help of Claude Code.)

     
    πŸ‘
    1

    Related

    Commit: [r8969]
    Commit: [r9088]
    Commit: [r9089]

  • Ognyan Chernokozhev

    • Group: 8 --> 7
     
  • Vidar Hasfjord

    Vidar Hasfjord - 2026-10-18

    On behalf of @jogybl, Claude wrote:

    Vidar, could you describe what looked wonky in your test? Was it flicker, the horizontal bar that doesn't go away, or something else, and with which steps?

    I've retested the wonkiness now.

    "Sticky" bars test

    1. Using latest OWLMaker Build 9096, I open a new file.
    2. I am careful not to move or size the empty MDI child that appears.
    3. I observe that the MDI child is wider than tall and has no scrollbars.
    4. I press "x" to fill the first line.
    5. The horisontal scrollbar becomes visible an em or so before the edge.
    6. Deleting the last character does not undo the scrollbar.
    7. I press Home to return and scroll to the start of the line.
    8. The scroll bar is still visible.
    9. I press End and continue to delete characters until the line is empty.
    10. The scrollbar is still visible.
    11. I now press Enter repeatedly to add lines.
    12. ~2 lines from the bottom edge, the horizontal bar disappears and the verical bar appears.
    13. I delete the last line.
    14. The scrollbar visibility remains unchanged.
    15. I continue deleting lines until the window is again empty.
    16. Nothing changes with the bars; the vertical bar is still visible.
    17. I repeat the first action, filling the line with "x".
    18. As I get close to the edge, the vertical bar disappears.
    19. At the next "x" nothing happens (I'm still within the client area).
    20. Then at the next "x" the horizontal bar appears again.

    This wonky behaviour may be more of an issue to do with content extent calculation and update notifications, rather than faulty logic in the scroll bars.

    Bar visibility test

    1. I again open a new file.
    2. I fill the first line with "x" to the edge without overflowing.
    3. No scrollbars are shown.
    4. I press Enter, select all (Ctrl+A) and duplicate (Ctrl+D).
    5. I continue to duplicate the line until I get close to the bottom.
    6. Around two lines from the bottom edge, both scrollbars appear.
    7. I grab the lower-right corner of the window and move it around.
    8. As the window sizes, scrollbar visibility changes as expected; whenever the content overflows an edge, the corresponding scrollbar appears.

    This seems to be beautifully handled now, except that the vertical bar triggers a little early, i.e. with the content having a line to spare from the bottom edge.

    However:

    1. Now delete half the lines.
    2. Grab a corner of the window and resize it again.
    3. Scrollbars appear and disappear as if the content extent hasn't changed.

    Again, this may have more to do with content extent calculation and update.

     

    Last edit: Vidar Hasfjord 2026-10-18
1 2 > >> (Page 1 of 2)

Log in to post a comment.