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: 1889780344}
- component: {fileID: 1889780343}
- component: {fileID: 1889780346}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@ -397,6 +398,50 @@ Transform:
m_Children: []
m_Father: {fileID: 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
SceneRoots:
m_ObjectHideFlags: 0

View file

@ -1,3 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@ -5,55 +6,15 @@ using UnityEngine.UIElements;
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 GutterSize { get; set; }
public FlowDirection Direction { get; 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> sizes = new();
@ -105,6 +66,7 @@ namespace KitsuneCafe.UI
sizes.RemoveRange(itemCount, sizes.Count - itemCount);
}
Count = itemCount;
ContentSize = LastPositionOrDefault();
}
@ -112,7 +74,7 @@ namespace KitsuneCafe.UI
{
var position = initialPosition;
for (int i = startingIndex; i < positions.Count; i++)
for (int i = startingIndex; i < Count; i++)
{
positions[i] = position;
position += GetItemSize(i) + GutterSize;
@ -134,13 +96,13 @@ namespace KitsuneCafe.UI
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;
}
@ -149,28 +111,29 @@ namespace KitsuneCafe.UI
return positions.Count - 1;
}
public IEnumerable<LayoutItem> Enumerate(int startIndex = 0)
public Range GetVisibleRange(float offset, float size)
{
var len = positions.Count;
for (int i = startIndex; i < len; i++)
var halfBuffer = Mathf.CeilToInt(Buffer / 2);
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()
{
return new LayoutEnumerator(this);
}
count += halfBuffer;
public IEnumerator<LayoutItem> GetEnumerator(int startIndex)
{
return new LayoutEnumerator(startIndex, this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
return new Range(first, Math.Min(Count, first + count));
}
}
}

View file

@ -1,3 +1,5 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
@ -10,6 +12,8 @@ namespace KitsuneCafe.UI
public float ItemSize { 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)
{
@ -32,7 +36,18 @@ namespace KitsuneCafe.UI
public void Update(int itemCount, VisualElement _container)
{
Count = itemCount;
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;
namespace KitsuneCafe.UI
@ -6,9 +8,11 @@ namespace KitsuneCafe.UI
{
FlowDirection Direction { get; }
float ContentSize { get; }
int Buffer { get; }
float GetItemPosition(int index);
float GetItemSize(int index);
Range GetVisibleRange(float offset, float size);
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]
public ICollectionDataSource DataSource
{
@ -152,8 +199,8 @@ namespace KitsuneCafe.UI
{
return isDynamicSize switch
{
true => new DynamicLayout(Direction, itemSize),
false => new FixedLayout(Direction, itemSize)
true => new DynamicLayout(Direction, itemSize) { Buffer = bufferCount, GutterSize = gutter },
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 float parentSize = 0;
private float lastKnownScrollOffset = 0;
private readonly int itemCountBuffer = 2;
private readonly Subject<UpdateRequest> requestSubject = new();
private IDisposable subscriptions;
@ -102,16 +99,6 @@ namespace KitsuneCafe.UI
});
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);
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()
{
if (DataSource == null || DataSource.Length == 0 || Mathf.Approximately(parentSize, 0))
@ -272,15 +232,9 @@ namespace KitsuneCafe.UI
return;
}
var scrollOffset = lastKnownScrollOffset;
var range = Layout switch
{
FixedLayout fl => GetFixedItems(fl, scrollOffset),
DynamicLayout dl => GetDynamicItems(dl, scrollOffset),
_ => throw new NotImplementedException(),
};
var range = layout.GetVisibleRange(scrollOffset, parentSize);
var firstIndex = range.Start.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">
<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>