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;
}