From 0eaef67b9d63cb0700c63ad9d4722968698c0abb Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 15 Jun 2026 22:23:17 +0200 Subject: [PATCH] feat(D.2b): UiChatView drives the shared UiScrollable model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the ad-hoc _scroll float with a public UiScrollable instance. OnDraw feeds ContentHeight/ViewHeight/LineHeight into the model each frame and reads baseY = bottom - contentH + (MaxScroll - ScrollY) — the (MaxScroll-ScrollY) inversion reconciles UiScrollable's top-origin convention (0=oldest, MaxScroll=newest) with the visual layout (newest at bottom). The wheel handler routes through ScrollByLines with a sign flip so wheel-up still reveals older lines. _pinBottom tracks whether the view is at the end and calls ScrollToEnd() each draw to auto-scroll new messages. ClampScroll static method kept — referenced by existing tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/UI/UiChatView.cs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/AcDream.App/UI/UiChatView.cs b/src/AcDream.App/UI/UiChatView.cs index 1392c26f..9dbe9cd3 100644 --- a/src/AcDream.App/UI/UiChatView.cs +++ b/src/AcDream.App/UI/UiChatView.cs @@ -52,8 +52,12 @@ public sealed class UiChatView : UiElement /// Inner text inset from the view edges, px. public float Padding { get; set; } = 4f; - // Pixels the transcript is scrolled UP from the newest line (0 = pinned to bottom). - private float _scroll; + /// The scroll model — also read by the linked UiChatScrollbar. + public UiScrollable Scroll { get; } = new(); + + /// True while the view is pinned to the newest line (auto-scrolls as content grows). + private bool _pinBottom = true; + private const float WheelLines = 1f; // lines advanced per wheel notch (retail = 1 line per notch) // ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ── @@ -112,11 +116,19 @@ public sealed class UiChatView : UiElement float top = Padding, bottom = Height - Padding; float innerH = bottom - top; float contentH = lines.Count * lh; - _scroll = ClampScroll(_scroll, contentH, innerH); - // Bottom-pin: with _scroll==0 the LAST line ends at `bottom`; scrolling up - // shifts the whole block down so older lines are revealed at the top. - float baseY = bottom - contentH + _scroll; + // Drive the shared scroll model with the current geometry. + Scroll.LineHeight = (int)MathF.Round(lh); + Scroll.ContentHeight = (int)MathF.Ceiling(contentH); + Scroll.ViewHeight = (int)MathF.Floor(innerH); + if (_pinBottom) Scroll.ScrollToEnd(); + + // UiScrollable: ScrollY=0 is TOP/oldest, ScrollY=MaxScroll is BOTTOM/newest. + // Visual layout: newest at bottom → baseY = bottom - contentH (ScrollY at max). + // Invert: baseY = bottom - contentH + (MaxScroll - ScrollY). + // With _pinBottom: ScrollY=MaxScroll → baseY=bottom-contentH → last line ends at bottom. ✓ + // Scrolled to top: ScrollY=0 → baseY=bottom-contentH+MaxScroll=bottom-innerH=top. ✓ + float baseY = bottom - contentH + (Scroll.MaxScroll - Scroll.ScrollY); _lastBaseY = baseY; // Normalised selection span (start <= end), if any. @@ -166,9 +178,11 @@ public sealed class UiChatView : UiElement { case UiEventType.Scroll: { - float lh = DatFont?.LineHeight ?? (Font ?? _lastFont)?.LineHeight ?? 16f; - // Silk wheel +Y = scroll up = reveal older = shift content down = larger _scroll. - _scroll += e.Data0 * WheelLines * lh; // re-clamped next OnDraw against live content + // Silk wheel +Y = scroll up = reveal older = toward the TOP = decrease ScrollY. + // ScrollByLines sign: +down/newer, -up/older. + // e.Data0 > 0 → wheel up → want older → ScrollByLines with negative lines. + Scroll.ScrollByLines((int)(-e.Data0 * WheelLines)); + _pinBottom = Scroll.AtEnd; return true; }