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 (UiText) 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;
///
/// Update the content/view extents and clamp the current offset. When
/// is true, a view that was at the end before
/// the geometry change remains at the end afterward. User-driven scrollbar
/// changes move off the end, so later layout passes
/// preserve that selected position instead of snapping it back.
///
public void SetExtents(int contentHeight, int viewHeight, bool preserveEnd = false)
{
bool wasAtEnd = AtEnd;
ContentHeight = Math.Max(0, contentHeight);
ViewHeight = Math.Max(0, viewHeight);
if (preserveEnd && wasAtEnd)
ScrollToEnd();
else
SetScrollY(_scrollY);
}
/// 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);
}