configurable buffers and gutters

This commit is contained in:
Rowan 2025-08-06 13:30:28 -04:00
parent 9e6e3ccdee
commit 843e0220c2
7 changed files with 144 additions and 116 deletions

View file

@ -316,6 +316,7 @@ GameObject:
- component: {fileID: 1889780345} - component: {fileID: 1889780345}
- component: {fileID: 1889780344} - component: {fileID: 1889780344}
- component: {fileID: 1889780343} - component: {fileID: 1889780343}
- component: {fileID: 1889780346}
m_Layer: 0 m_Layer: 0
m_Name: Main Camera m_Name: Main Camera
m_TagString: MainCamera m_TagString: MainCamera
@ -397,6 +398,50 @@ Transform:
m_Children: [] m_Children: []
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1889780346
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1889780342}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_VolumeFrameworkUpdateModeOption: 2
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_AllowHDROutput: 1
m_UseScreenCoordOverride: 0
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_Version: 2
m_TaaSettings:
m_Quality: 3
m_FrameInfluence: 0.1
m_JitterScale: 1
m_MipBias: 0
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
--- !u!1660057539 &9223372036854775807 --- !u!1660057539 &9223372036854775807
SceneRoots: SceneRoots:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View file

@ -1,3 +1,4 @@
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
@ -5,55 +6,15 @@ using UnityEngine.UIElements;
namespace KitsuneCafe.UI namespace KitsuneCafe.UI
{ {
public record LayoutItem(float Position, float Size)
public class DynamicLayout : ILayout
{ {
public float Min => Position;
public float Max => Position + Size;
}
public class DynamicLayout : ILayout, IEnumerable<LayoutItem>
{
public struct LayoutEnumerator : IEnumerator<LayoutItem>
{
public readonly int Start;
public int Index;
private readonly DynamicLayout layout;
public LayoutEnumerator(int index, DynamicLayout layout)
{
Start = index;
Index = index;
this.layout = layout;
}
public LayoutEnumerator(DynamicLayout layout) : this(0, layout) { }
public readonly LayoutItem Current => new(
layout.positions[Index - 1],
layout.sizes[Index - 1]
);
readonly object IEnumerator.Current => Current;
public readonly void Dispose() { }
public bool MoveNext()
{
Index += 1;
return Index < layout.positions.Count;
}
public void Reset()
{
Index = Start;
}
}
public float DefaultItemSize { get; set; } public float DefaultItemSize { get; set; }
public float GutterSize { get; set; } public float GutterSize { get; set; }
public FlowDirection Direction { get; set; } public FlowDirection Direction { get; set; }
public float ContentSize { get; private set; } public float ContentSize { get; private set; }
public int Count { get; set; }
public int Buffer { get; set; }
private readonly List<float> positions = new(); private readonly List<float> positions = new();
private readonly List<float> sizes = new(); private readonly List<float> sizes = new();
@ -105,6 +66,7 @@ namespace KitsuneCafe.UI
sizes.RemoveRange(itemCount, sizes.Count - itemCount); sizes.RemoveRange(itemCount, sizes.Count - itemCount);
} }
Count = itemCount;
ContentSize = LastPositionOrDefault(); ContentSize = LastPositionOrDefault();
} }
@ -112,7 +74,7 @@ namespace KitsuneCafe.UI
{ {
var position = initialPosition; var position = initialPosition;
for (int i = startingIndex; i < positions.Count; i++) for (int i = startingIndex; i < Count; i++)
{ {
positions[i] = position; positions[i] = position;
position += GetItemSize(i) + GutterSize; position += GetItemSize(i) + GutterSize;
@ -134,13 +96,13 @@ namespace KitsuneCafe.UI
CalculatePositions(index, position); CalculatePositions(index, position);
} }
public int GetFirstVisibleIndex(float scrollOffset) public int GetFirstVisibleIndex(float offset)
{ {
if (scrollOffset <= 0) { return 0; } if (offset <= 0) { return 0; }
for (int i = 0; i < positions.Count; i++) for (int i = 0; i < Count; i++)
{ {
if (positions[i] >= scrollOffset) if (positions[i] >= offset)
{ {
return i; return i;
} }
@ -149,28 +111,29 @@ namespace KitsuneCafe.UI
return positions.Count - 1; return positions.Count - 1;
} }
public IEnumerable<LayoutItem> Enumerate(int startIndex = 0) public Range GetVisibleRange(float offset, float size)
{ {
var len = positions.Count; var halfBuffer = Mathf.CeilToInt(Buffer / 2);
for (int i = startIndex; i < len; i++)
var first = Math.Max(
GetFirstVisibleIndex(offset) - halfBuffer,
0
);
var count = 1;
var position = GetItemPosition(first);
var width = position + size;
for (int i = first; i < Count; i++)
{ {
yield return new LayoutItem(positions[i], sizes[i]); var item = GetItemPosition(i);
if (item > width) { break; }
count += 1;
} }
}
public IEnumerator<LayoutItem> GetEnumerator() count += halfBuffer;
{
return new LayoutEnumerator(this);
}
public IEnumerator<LayoutItem> GetEnumerator(int startIndex) return new Range(first, Math.Min(Count, first + count));
{
return new LayoutEnumerator(startIndex, this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
} }
} }
} }

View file

@ -1,3 +1,5 @@
using System;
using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
namespace KitsuneCafe.UI namespace KitsuneCafe.UI
@ -10,6 +12,8 @@ namespace KitsuneCafe.UI
public float ItemSize { get; set; } public float ItemSize { get; set; }
public float GutterSize { get; set; } public float GutterSize { get; set; }
public int Count { get; set; }
public int Buffer { get; set; }
public FixedLayout(FlowDirection direction, float itemSize, float gutterSize) public FixedLayout(FlowDirection direction, float itemSize, float gutterSize)
{ {
@ -32,7 +36,18 @@ namespace KitsuneCafe.UI
public void Update(int itemCount, VisualElement _container) public void Update(int itemCount, VisualElement _container)
{ {
Count = itemCount;
ContentSize = itemCount * (ItemSize + GutterSize); ContentSize = itemCount * (ItemSize + GutterSize);
} }
public Range GetVisibleRange(float offset, float containerSize)
{
var size = ItemSize + GutterSize;
var first = Mathf.FloorToInt(offset / (ItemSize + GutterSize));
var count = Mathf.CeilToInt(containerSize / size) + Buffer;
var last = Math.Min(Count, first + count);
return new Range(first, last);
}
} }
} }

View file

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using UnityEngine.UIElements; using UnityEngine.UIElements;
namespace KitsuneCafe.UI namespace KitsuneCafe.UI
@ -6,9 +8,11 @@ namespace KitsuneCafe.UI
{ {
FlowDirection Direction { get; } FlowDirection Direction { get; }
float ContentSize { get; } float ContentSize { get; }
int Buffer { get; }
float GetItemPosition(int index); float GetItemPosition(int index);
float GetItemSize(int index); float GetItemSize(int index);
Range GetVisibleRange(float offset, float size);
void Update(int itemCount, VisualElement container); void Update(int itemCount, VisualElement container);
} }
} }

View file

@ -112,6 +112,53 @@ namespace KitsuneCafe.UI
} }
} }
private int bufferCount = 8;
[UxmlAttribute, CreateProperty, Delayed]
public int BufferCount
{
get => bufferCount;
set
{
if (bufferCount != value)
{
bufferCount = value;
if (virtualizationController.Layout is FixedLayout fixedLayout)
{
fixedLayout.Buffer = value;
}
else if (virtualizationController.Layout is DynamicLayout dynamicLayout)
{
dynamicLayout.Buffer = value;
}
}
}
}
private float gutter = 0;
[UxmlAttribute, CreateProperty, Delayed]
public float Gutter
{
get => gutter;
set
{
if (gutter != value)
{
gutter = value;
if (virtualizationController.Layout is FixedLayout fixedLayout)
{
fixedLayout.GutterSize = value;
}
else if (virtualizationController.Layout is DynamicLayout dynamicLayout)
{
dynamicLayout.GutterSize = value;
}
}
}
}
[CreateProperty] [CreateProperty]
public ICollectionDataSource DataSource public ICollectionDataSource DataSource
{ {
@ -152,8 +199,8 @@ namespace KitsuneCafe.UI
{ {
return isDynamicSize switch return isDynamicSize switch
{ {
true => new DynamicLayout(Direction, itemSize), true => new DynamicLayout(Direction, itemSize) { Buffer = bufferCount, GutterSize = gutter },
false => new FixedLayout(Direction, itemSize) false => new FixedLayout(Direction, itemSize) { Buffer = bufferCount, GutterSize = gutter }
}; };
} }

View file

@ -71,9 +71,6 @@ namespace KitsuneCafe.UI
private readonly BiDictionary<int, VisualElement> visibleItems = new(); private readonly BiDictionary<int, VisualElement> visibleItems = new();
private float parentSize = 0; private float parentSize = 0;
private float lastKnownScrollOffset = 0; private float lastKnownScrollOffset = 0;
private readonly int itemCountBuffer = 2;
private readonly Subject<UpdateRequest> requestSubject = new(); private readonly Subject<UpdateRequest> requestSubject = new();
private IDisposable subscriptions; private IDisposable subscriptions;
@ -102,16 +99,6 @@ namespace KitsuneCafe.UI
}); });
subscriptions = Disposable.Combine(full, partial); subscriptions = Disposable.Combine(full, partial);
// subscriptions = Observable.Merge(full, partial)
// // var updateSubscription = requestSubject.ThrottleLastFrame(1)
// .Subscribe(request =>
// {
// if (request is PartialUpdate req)
// {
// UpdateFromIndex(req.Index);
// }
// else { UpdateVisibleItems(); }
// });
Layout.Update(DataSource.Length, Container); Layout.Update(DataSource.Length, Container);
ScheduleUpdate(); ScheduleUpdate();
@ -238,33 +225,6 @@ namespace KitsuneCafe.UI
} }
} }
private Range GetDynamicItems(DynamicLayout layout, float scrollOffset)
{
var first = layout.GetFirstVisibleIndex(scrollOffset);
var count = 1;
var position = layout.GetItemPosition(first);
var width = position + parentSize;
foreach (var item in layout.Enumerate(first))
{
if (item.Position > width) { break; }
count += 1;
position = item.Position;
}
return new Range(first, Math.Min(DataSource.Length, first + count));
}
private Range GetFixedItems(FixedLayout layout, float scrollOffset)
{
var size = layout.ItemSize + layout.GutterSize;
var first = Mathf.FloorToInt(scrollOffset / (layout.ItemSize + layout.GutterSize));
var count = Mathf.CeilToInt(parentSize / size) + itemCountBuffer;
var last = Math.Min(DataSource.Length, first + count);
return new Range(first, last);
}
private void UpdateVisibleItems() private void UpdateVisibleItems()
{ {
if (DataSource == null || DataSource.Length == 0 || Mathf.Approximately(parentSize, 0)) if (DataSource == null || DataSource.Length == 0 || Mathf.Approximately(parentSize, 0))
@ -272,15 +232,9 @@ namespace KitsuneCafe.UI
return; return;
} }
var scrollOffset = lastKnownScrollOffset; var scrollOffset = lastKnownScrollOffset;
var range = Layout switch var range = layout.GetVisibleRange(scrollOffset, parentSize);
{
FixedLayout fl => GetFixedItems(fl, scrollOffset),
DynamicLayout dl => GetDynamicItems(dl, scrollOffset),
_ => throw new NotImplementedException(),
};
var firstIndex = range.Start.Value; var firstIndex = range.Start.Value;
var lastIndex = range.End.Value; var lastIndex = range.End.Value;

View file

@ -1,3 +1,3 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False"> <ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<KitsuneCafe.UI.RecycleView name="recycle-view" direction="Horizontal" a="Stupid" is-dynamic-size="true" default-item-size="50" fixed-item-size="22" item-size="22" style="flex-grow: 1; flex-direction: row;" /> <KitsuneCafe.UI.RecycleView name="recycle-view" direction="Horizontal" a="Stupid" is-dynamic-size="true" default-item-size="50" fixed-item-size="22" gutter="0" style="flex-grow: 1; flex-direction: row;" />
</ui:UXML> </ui:UXML>