Skip to content

Added list view component #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 13, 2025
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ obj/
riderModule.iml
/_ReSharper.Caches/
.idea
.vs
/**/*.csproj.user
22 changes: 13 additions & 9 deletions ReactiveSDK/Components/Keyed/SegmentedControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,22 @@ private set {
}

_selectedKey = value;
KeySelectedCb?.Invoke(value);

SelectedKeyChangedEvent?.Invoke(value);
WhenKeySelected?.Invoke(value);
_selectedKeyChanged?.Invoke(value);
NotifyPropertyChanged();
}
}

public Action<TKey>? KeySelectedCb { get; set; }
public Action<TKey, TCell>? CellSpawnedCb { get; set; }
public Action<TKey, TCell>? CellDespawnedCb { get; set; }
public Action<TKey>? WhenKeySelected { get; set; }
public Action<TKey, TCell>? WhenCellSpawned { get; set; }
public Action<TKey, TCell>? WhenCellDespawned { get; set; }

event Action<TKey>? IKeyedControl<TKey>.SelectedKeyChangedEvent {
add => _selectedKeyChanged += value;
remove => _selectedKeyChanged -= value;
}

public event Action<TKey>? SelectedKeyChangedEvent;
private Action<TKey>? _selectedKeyChanged;

private readonly ReactivePool<TKey, TCell> _cells = new();
private readonly ObservableDictionary<TKey, TParam> _items = new();
Expand All @@ -64,7 +68,7 @@ private void SpawnCell(TKey key) {

_layout.Children.Add(cell);

CellSpawnedCb?.Invoke(key, cell);
WhenCellSpawned?.Invoke(key, cell);
OnCellSpawned(key, cell);

if (_selectedCell == null) {
Expand All @@ -83,7 +87,7 @@ private void DespawnCell(TKey key) {
_layout.Children.Remove(cell);
_cells.Despawn(cell);

CellDespawnedCb?.Invoke(key, cell);
WhenCellDespawned?.Invoke(key, cell);
OnCellDespawned(key, cell);
}

Expand Down
58 changes: 58 additions & 0 deletions ReactiveSDK/Components/ListView/ListCell.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using JetBrains.Annotations;
using UnityEngine;

namespace Reactive.Components;

[PublicAPI]
public class ListCell<TItem> : ReactiveComponent, IListCell<TItem> {
public delegate IReactiveComponent Constructor(INotifyValueChanged<TItem> item);

#region Factory

public ListCell(Constructor constructor) : this() {
this.constructor = constructor;
}

public ListCell() : base(false) { }

internal Constructor? constructor;
private IReactiveComponent? _constructedComponent;

protected override GameObject Construct() {
if (constructor == null) {
return base.Construct();
}

_constructedComponent = constructor(_observableItem!);
return _constructedComponent.Use(null);
}

protected override void OnInitialize() {
if (_constructedComponent is ReactiveComponent comp) {
ExposeLayoutFirstComponent(comp);
}
}

#endregion

#region Cell

public TItem Item => _observableItem!;
public INotifyValueChanged<TItem> ObservableItem => _observableItem!;

private ObservableValue<TItem>? _observableItem;

void IListCell<TItem>.Init(TItem item) {
if (!IsInitialized) {
_observableItem = new(item);
ConstructAndInit();
}
_observableItem!.Value = item;

OnInit(item);
}

protected virtual void OnInit(TItem item) { }

#endregion
}
113 changes: 113 additions & 0 deletions ReactiveSDK/Components/ListView/ListView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;

namespace Reactive.Components {
/// <summary>
/// A <see cref="ListView{LItem,LCell}"/> overload with an ability to make cells in-place.
/// </summary>
[PublicAPI]
public class ListView<TItem> : ListView<TItem, ListCell<TItem>> {
/// <summary>
/// Defines a cell constructor. Must be specified.
/// </summary>
public ListCell<TItem>.Constructor? ConstructCell { get; set; }

protected override void OnInstantiate() {
cellsPool.Construct = CreateCell;
}

private ListCell<TItem> CreateCell() {
if (ConstructCell == null) {
throw new UninitializedComponentException("The ConstructCell property must be specified");
}

return new ListCell<TItem>(ConstructCell);
}
}

/// <summary>
/// A kind of Table which spawns cells directly in the layout flow.
/// </summary>
[PublicAPI]
public class ListView<TItem, TCell> : ReactiveComponent, ILayoutDriver where TCell : IListCell<TItem>, IReactiveComponent, new() {
#region Layout Driver

// Avoid using collection expression as it create a new instance of List each time.
ICollection<ILayoutItem> ILayoutDriver.Children => Array.Empty<ILayoutItem>();

public ILayoutController? LayoutController {
get => _container.LayoutController;
set => _container.LayoutController = value;
}

#endregion

#region ListView

/// <summary>
/// A collection of added items.
/// </summary>
public IReadOnlyList<TItem> Items {
get => _items;
set {
_items = value;
Refresh();
}
}

private IReadOnlyList<TItem> _items = new List<TItem>();

public void Refresh() {
RefreshCells();
OnRefresh();
WhenRefreshed?.Invoke(this);
}

#endregion

#region Cells

internal readonly ReactivePool<TCell> cellsPool = new() { DetachOnDespawn = true };
private Layout _container = null!;

private void RefreshCells() {
cellsPool.DespawnAll();

foreach (var item in _items) {
var cell = cellsPool.Spawn(false);
cell.Init(item);
cell.Enabled = true;

OnCellConstruct(cell);
WhenCellConstructed?.Invoke(cell);

_container.Children.Add(cell);
}
}

#endregion

#region Abstraction

public Action<ListView<TItem, TCell>>? WhenRefreshed;
public Action<TCell>? WhenCellConstructed;

protected virtual void OnRefresh() { }
protected virtual void OnCellConstruct(TCell cell) { }

#endregion

#region Construct

protected sealed override GameObject Construct() {
return new Layout()
.AsFlexGroup(direction: Yoga.FlexDirection.Column)
.Bind(ref _container)
.Use();
}

#endregion
}
}
14 changes: 12 additions & 2 deletions ReactiveSDK/Components/Table/Table.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
Expand Down Expand Up @@ -164,13 +165,17 @@ IReadOnlyList<TItem> ITable<TItem>.Items {

public void Refresh(bool clearSelection = true) {
OnEarlyRefresh();
WhenEarlyRefreshed?.Invoke(this);

RefreshFilter();
RefreshContentSize();
RefreshVisibleCells(0f);
ScrollContentIfNeeded();
RefreshVisibility();
if (clearSelection) ClearSelection();
OnRefresh();

WhenRefreshed?.Invoke(this);
}

public void QueueRefreshCellSize() {
Expand Down Expand Up @@ -296,7 +301,8 @@ private void RefreshVisibleCells(float pos) {
var cell = GetOrSpawnCell(i - _visibleCellsStartIndex, item);

OnCellConstruct(cell);

WhenCellConstructed?.Invoke(cell);

//updating state
if (_selectionRefreshNeeded) {
var selected = _selectedIndexes.Contains(i);
Expand All @@ -307,7 +313,7 @@ private void RefreshVisibleCells(float pos) {
_cachedIndexes[cell] = i;
}
_selectionRefreshNeeded = false;

//despawning redundant cells
i -= _visibleCellsStartIndex;
while (cellsPool.SpawnedComponents.Count > i) {
Expand Down Expand Up @@ -349,6 +355,10 @@ private TCell GetOrSpawnCell(int index, TItem item) {

#region Abstraction

public Action<Table<TItem, TCell>>? WhenEarlyRefreshed;
public Action<Table<TItem, TCell>>? WhenRefreshed;
public Action<TCell>? WhenCellConstructed;

protected IEnumerable<KeyValuePair<TCell, TItem>> SpawnedCells => _cachedIndexes
.Select(pair => new KeyValuePair<TCell, TItem>((TCell)pair.Key, _filteredItems[pair.Value]));

Expand Down
40 changes: 6 additions & 34 deletions ReactiveSDK/Components/Table/TableCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,30 @@

namespace Reactive.Components {
[PublicAPI]
public class TableCell<TItem> : ReactiveComponent, ITableCell<TItem> {
public delegate IReactiveComponent Constructor(INotifyValueChanged<TItem> item, ObservableValue<bool> selected);
public class TableCell<TItem> : ListCell<TItem>, ITableCell<TItem> {
public new delegate IReactiveComponent Constructor(INotifyValueChanged<TItem> item, ObservableValue<bool> selected);

#region Factory

public TableCell(Constructor constructor) : this() {
_constructor = constructor;
public TableCell(Constructor constructor) {
this.constructor = x => constructor(x, _observableSelected);
}

public TableCell() : base(false) { }

private Constructor? _constructor;

protected override GameObject Construct() {
if (_constructor == null) {
return base.Construct();
}

var component = _constructor(_observableItem!, _observableSelected!);
return component.Use(null);
}
public TableCell() { }

#endregion

#region TableCell
#region Cell

event Action<ITableCell<TItem>, bool>? ITableCell<TItem>.CellAskedToChangeSelectionEvent {
add => CellAskedToChangeSelectionEvent += value;
remove => CellAskedToChangeSelectionEvent -= value;
}

private event Action<ITableCell<TItem>, bool>? CellAskedToChangeSelectionEvent;
private ObservableValue<TItem>? _observableItem;
private ObservableValue<bool> _observableSelected = new(false);
private bool _canSelect = true;

void ITableCell<TItem>.Init(TItem item) {
if (!IsInitialized) {
_observableItem = new(item);
ConstructAndInit();
} else {
_observableItem!.Value = item;
}

OnInit(item);
}

void ITableCell<TItem>.OnCellStateChange(bool selected) {
_canSelect = false;

Expand All @@ -66,14 +43,9 @@ void ITableCell<TItem>.OnCellStateChange(bool selected) {

#region Abstraction

public TItem Item => _observableItem!;
public bool Selected => _observableSelected;

public INotifyValueChanged<TItem> ObservableItem => _observableItem!;
public INotifyValueChanged<bool> ObservableSelected => _observableSelected;

protected virtual void OnInit(TItem item) { }

protected virtual void OnCellStateChange(bool selected) { }

protected void SelectSelf(bool select) {
Expand Down
2 changes: 1 addition & 1 deletion ReactiveSDK/Models/Keyed/IKeyedControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Reactive.Components {
public interface IKeyedControl<TKey> {
TKey SelectedKey { get; }

event Action<TKey>? SelectedKeyChangedEvent;
event Action<TKey>? SelectedKeyChangedEvent;

void Select(TKey key);
}
Expand Down
8 changes: 8 additions & 0 deletions ReactiveSDK/Models/Table/IListCell.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using JetBrains.Annotations;

namespace Reactive.Components;

[PublicAPI]
public interface IListCell<in TItem> {
void Init(TItem item);
}
5 changes: 3 additions & 2 deletions ReactiveSDK/Models/Table/ITableCell.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System;
using JetBrains.Annotations;

namespace Reactive.Components {
public interface ITableCell<in TItem> {
[PublicAPI]
public interface ITableCell<in TItem> : IListCell<TItem> {
event Action<ITableCell<TItem>, bool>? CellAskedToChangeSelectionEvent;

void Init(TItem item);
void OnCellStateChange(bool selected);
}
}
2 changes: 1 addition & 1 deletion reactive-ui