LunaScroller is a game-grade scroll container: drag past a threshold, flick into a momentum glide with tunable friction, rubber-banded ends with spring-back, mouse wheel, an optional styled scrollbar, and scroll-into-view on gamepad/keyboard focus change. It shares its gesture core with Carousel, so a horizontal offer strip composes inside a vertical scroller through axis-lock arbitration: whichever axis dominates at the drag threshold owns the gesture, and a lost gesture stays lost for that pointer.
Like the Carousel, it is deliberately not built on Unity's ScrollView — the component owns the whole gesture on a plain container, so the ScrollView elastic-offset clamp bug (long touch lists jumping near the tail) structurally cannot occur.
Children become the scrolled content. The scroller needs a size from USS or layout; content overflows it and is clipped:
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:luna="CupkekGames.Luna">
<luna:LunaScroller name="Shop" style="flex-grow: 1;">
<ui:Label text="DAILY OFFERS" />
<luna:Carousel name="Offers" page-width-percent="64">...</luna:Carousel>
<ui:VisualElement name="RewardList">...</ui:VisualElement>
</luna:LunaScroller>
</ui:UXML>| Attribute | Default | Meaning |
|---|---|---|
axis | Vertical | Scroll direction. Horizontal lays content out in a row. |
friction | 4 | Per-second exponential decay of the glide velocity. Higher stops sooner. |
wheel-speed | 60 | Pixels scrolled per mouse-wheel delta unit. |
show-scrollbar | true | Styled thumb; auto-hidden when the content fits. |
scroll-into-view | true | Scroll a newly focused descendant into view (deferred one frame so nav transitions land first). |
spring-duration | 0.2 | Settle time for spring-back, ScrollTo, and track clicks. |
LunaScroller scroller = root.Q<LunaScroller>("Shop");
scroller.ScrollOffset; // px scrolled, 0 at the start edge (set = instant)
scroller.MaxScroll; // content size minus viewport
scroller.ScrollTo(0f); // default motion: spring-duration + ease-out
scroller.ScrollTo(0f, 0.45f, EasingType.QuadInOut); // explicit duration + easing; duration 0 = instant
scroller.ScrollTo(element); // scroll-into-view; same overloads, no-op when visible
scroller.ScrollChanged += px => UpdateParallax(px);The scrollbar thumb is draggable and the track click-jumps with a settle. Presses inside the content only become drags after 8px of travel, so buttons keep their plain clicks — past the threshold the scroller captures the pointer and the child press cancels.
.my-shop .luna-scroller__thumb { --luna-scroller-thumb: rgba(255, 255, 255, 0.25); }
.my-shop .luna-scroller__thumb:hover { --luna-scroller-thumb-hover: rgba(255, 255, 255, 0.5); }| Class | Element |
|---|---|
.luna-scroller / .luna-scroller--horizontal | Root (clips overflow). |
.luna-scroller__content | The moving content (translate transform; carries the load-bearing flex-shrink: 0). |
.luna-scroller__bar / .luna-scroller__thumb | Scrollbar track and thumb. |
LunaListView is a fixed-height virtualized list built on the scroller: rows outside the viewport (plus a 2-row overscan band) are recycled through a pool, so a 10,000-entry inventory costs a screenful of elements. It inherits every scroller behavior above — glide, rubber-band, scrollbar, wheel, axis-lock — and adds the list contract:
LunaListView list = new LunaListView();
list.FixedItemHeight = 64;
list.MakeItem = () => new RewardRow();
list.BindItem = (element, index) => ((RewardRow)element).Bind(_rewards[index]);
list.UnbindItem = (element, index) => ((RewardRow)element).Clear(); // optional
list.ItemsSource = _rewards; // any IList
list.RefreshItems(); // re-bind visible rows after data mutates
list.Rebuild(); // full reset — also drops the row pool (row shape may have changed)Rows are absolute-positioned and carry .luna-listview__row.
Single selection ships with the Unity-compatible event signature, so handlers written for ListView.selectionChanged plug straight in:
list.SelectionType = SelectionType.Single; // default None; Multiple warns and falls back
list.SelectionChanged += items => Show(items.FirstOrDefault());
list.SelectedIndex = 3; // -1 clears; out-of-range clears
object current = list.SelectedItem;
list.SetSelectionWithoutNotify(3);
list.ScrollToIndex(40); // minimal scroll into view; no-op when visible
list.ScrollToIndex(400, ScrollAlignment.Center, 0.42f); // "jump to my rank": centred with neighboursScrollAlignment is Auto (minimal, the default), Start, Center, or End — explicit alignments always scroll, so a "jump to me" button works even when the row is already on screen. Animated jumps longer than two viewports teleport most of the distance and ease only the tail: it still reads as a scroll, but the virtualizer binds ~a screen of rows instead of every row in between.
Clicking a row selects it; clicking the selected row notifies again (toggle-deselect patterns rely on that), while setting SelectedIndex to its current value stays silent. A drag never selects — past the 8px threshold the scroller owns the pointer. Assigning a new ItemsSource clears the selection silently. The selected row carries .luna-listview__row--selected, themed via --luna-listview-selected.
It is deliberately not Unity's editor ListView: no multi-select, reorder, tree view, or dynamic heights — those arrive demand-driven (drag-reorder is next in the queue). Vertical only. GridViewList renders through it.
The Components showcase ships ScrollerShowcase.unity — a shop-style page with a horizontal Carousel offer strip inside the vertical scroller (the axis-lock composition), a reward track long enough to flick through, the offset readout, and a ScrollTo button.
Settings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction