using System; namespace AcDream.App.UI; /// /// Pixel-based vertical scroll model. Port of retail UIElement_Scrollable: /// the scroll offset is an integer pixel value (m_iScrollableY) clamped to /// [0, ContentHeight - ViewHeight]; the thumb ratio is view/content; the position /// ratio is scroll/(content-view). Pure (no GL) so it is fully unit-tested and /// shared by the transcript (UiChatView) and the scrollbar (UiScrollbar). /// Decomp anchors: SetScrollableXY @0x4740c0, UpdateScrollbarSize_ @0x4741a0, /// UpdateScrollbarPosition_ @0x473f20, UIElement_Text::InqScrollDelta @0x4689b0. /// public sealed class UiScrollable { /// Total wrapped content height in px (m_iScrollableHeight). public int ContentHeight { get; set; } /// Visible viewport height in px. public int ViewHeight { get; set; } /// Pixels per text line (scroll quantum). InqScrollDelta line case. public int LineHeight { get; set; } = 16; private int _scrollY; /// Current scroll offset in px from the top of the content. public int ScrollY => _scrollY; /// Max scroll = max(0, content - view). public int MaxScroll => Math.Max(0, ContentHeight - ViewHeight); /// True when content exceeds the view (a scrollbar is warranted). public bool HasOverflow => ContentHeight > ViewHeight; /// True when the offset is at (or past) the bottom — used for bottom-pin. public bool AtEnd => _scrollY >= MaxScroll; /// Set the offset, clamped to [0, MaxScroll] (SetScrollableXY clamp). public void SetScrollY(int y) => _scrollY = Math.Clamp(y, 0, MaxScroll); /// Pin to the bottom (newest content visible). public void ScrollToEnd() => _scrollY = MaxScroll; /// Thumb size ratio = view/content, clamped to 1 (UpdateScrollbarSize_). public float ThumbRatio => ContentHeight <= 0 ? 1f : Math.Min(1f, (float)ViewHeight / ContentHeight); /// Position ratio = scroll/(content-view) in [0,1] (UpdateScrollbarPosition_). public float PositionRatio => MaxScroll <= 0 ? 0f : (float)_scrollY / MaxScroll; /// Inverse of PositionRatio — used when the user drags the thumb. public void SetPositionRatio(float ratio) => SetScrollY((int)MathF.Round(Math.Clamp(ratio, 0f, 1f) * MaxScroll)); /// Scroll by whole lines (sign: +down/newer, -up/older). public void ScrollByLines(int lines) => SetScrollY(_scrollY + lines * LineHeight); /// Scroll by a page = one view height (InqScrollDelta page case). public void ScrollByPage(int pages) => SetScrollY(_scrollY + pages * ViewHeight); }