Prevent nodes out of viewport triggering render (#296)

* Prevent nodes out of viewport triggering render

* Update frontend/src/common/SortedCollection.ts

Co-authored-by: David <dvdplm@gmail.com>

* Tweak the comment on `setFocus`, move it closer to `ref` and `hasChangedSince`

* Switch `SortedCollection.ref()` to a getter

Co-authored-by: David <dvdplm@gmail.com>
This commit is contained in:
Maciej Hirsz
2020-11-02 19:33:51 +01:00
committed by GitHub
parent 98cd3cfa12
commit 54039faa3b
3 changed files with 53 additions and 15 deletions
+1 -1
View File
@@ -149,7 +149,7 @@ export class Connection {
public handleMessages = (messages: FeedMessage.Message[]) => { public handleMessages = (messages: FeedMessage.Message[]) => {
const { nodes, chains, sortBy, selectedColumns } = this.state; const { nodes, chains, sortBy, selectedColumns } = this.state;
const ref = nodes.ref(); const { ref } = nodes;
const updateState: UpdateBound = (state) => { const updateState: UpdateBound = (state) => {
this.state = this.update(state); this.state = this.update(state);
+40 -5
View File
@@ -97,11 +97,27 @@ export namespace SortedCollection {
export type StateRef = Opaque<number, 'SortedCollection.StateRef'>; export type StateRef = Opaque<number, 'SortedCollection.StateRef'>;
} }
interface Focus {
start: number;
end: number;
}
export class SortedCollection<Item extends { id: number }> { export class SortedCollection<Item extends { id: number }> {
// Comparator function used to sort the collection
private compare: Compare<Item>; private compare: Compare<Item>;
// Mapping item `id` to the `Item`, this uses array as a structure with
// the assumption that `id`s provided are increments from `0`, and that
// vacant `id`s will be re-used in the future.
private map = Array<Maybe<Item>>(); private map = Array<Maybe<Item>>();
// Actual sorted list of `Item`s.
private list = Array<Item>(); private list = Array<Item>();
// Internal tracker for changes, this number increments whenever the
// order of the **focused** elements in the collection changes
private changeRef = 0; private changeRef = 0;
// Marks the range of indicies that are focused for tracking.
// **Note:** `start` is inclusive, while `end` is exclusive (much like
// `Array.slice()`).
private focus: Focus = { start: 0, end: 0 };
constructor(compare: Compare<Item>) { constructor(compare: Compare<Item>) {
this.compare = compare; this.compare = compare;
@@ -114,10 +130,6 @@ export class SortedCollection<Item extends { id: number }> {
this.changeRef += 1; this.changeRef += 1;
} }
public ref(): SortedCollection.StateRef {
return this.changeRef as SortedCollection.StateRef;
}
public add(item: Item) { public add(item: Item) {
if (this.map.length <= item.id) { if (this.map.length <= item.id) {
// Grow map if item.id would be out of scope // Grow map if item.id would be out of scope
@@ -131,10 +143,12 @@ export class SortedCollection<Item extends { id: number }> {
this.map[item.id] = item; this.map[item.id] = item;
sortedInsert(item, this.list, this.compare); const index = sortedInsert(item, this.list, this.compare);
if (index < this.focus.end) {
this.changeRef += 1; this.changeRef += 1;
} }
}
public remove(id: number) { public remove(id: number) {
const item = this.map[id]; const item = this.map[id];
@@ -147,8 +161,10 @@ export class SortedCollection<Item extends { id: number }> {
this.list.splice(index, 1); this.list.splice(index, 1);
this.map[id] = null; this.map[id] = null;
if (index < this.focus.end) {
this.changeRef += 1; this.changeRef += 1;
} }
}
public get(id: number): Maybe<Item> { public get(id: number): Maybe<Item> {
return this.map[id]; return this.map[id];
@@ -184,9 +200,15 @@ export class SortedCollection<Item extends { id: number }> {
const newIndex = sortedInsert(item, this.list, this.compare); const newIndex = sortedInsert(item, this.list, this.compare);
if (newIndex !== index) { if (newIndex !== index) {
const outOfFocus =
(index < this.focus.start && newIndex < this.focus.start) ||
(index >= this.focus.end && newIndex >= this.focus.end);
if (!outOfFocus) {
this.changeRef += 1; this.changeRef += 1;
} }
} }
}
public mutAndMaybeSort( public mutAndMaybeSort(
id: number, id: number,
@@ -207,6 +229,7 @@ export class SortedCollection<Item extends { id: number }> {
public mutEachAndSort(mutator: (item: Item) => void) { public mutEachAndSort(mutator: (item: Item) => void) {
this.list.forEach(mutator); this.list.forEach(mutator);
this.list.sort(this.compare); this.list.sort(this.compare);
this.changeRef += 1;
} }
public clear() { public clear() {
@@ -216,6 +239,18 @@ export class SortedCollection<Item extends { id: number }> {
this.changeRef += 1; this.changeRef += 1;
} }
// Set a new `Focus`. Any changes to the order of items within the `Focus`
// will increment `changeRef`.
public setFocus(start: number, end: number) {
this.focus = { start, end };
}
// Get the reference to current ordering state of focused items.
public get ref(): SortedCollection.StateRef {
return this.changeRef as SortedCollection.StateRef;
}
// Check if order of focused items has changed since obtaining a `ref`.
public hasChangedSince(ref: SortedCollection.StateRef): boolean { public hasChangedSince(ref: SortedCollection.StateRef): boolean {
return this.changeRef > ref; return this.changeRef > ref;
} }
+9 -6
View File
@@ -52,11 +52,11 @@ export class List extends React.Component<List.Props, {}> {
} }
public render() { public render() {
const { selectedColumns } = this.props.appState; const { pins, sortBy, appState } = this.props;
const { pins, sortBy } = this.props; const { selectedColumns } = appState;
const { filter } = this.state; const { filter, listStart, listEnd } = this.state;
let nodes = this.props.appState.nodes.sorted(); let nodes = appState.nodes.sorted();
if (filter != null) { if (filter != null) {
nodes = nodes.filter(filter); nodes = nodes.filter(filter);
@@ -73,10 +73,13 @@ export class List extends React.Component<List.Props, {}> {
</React.Fragment> </React.Fragment>
); );
} }
// With filter present, we can no longer guarantee that focus corresponds
// to rendering view, so we put the whole list in focus
appState.nodes.setFocus(0, nodes.length);
} else {
appState.nodes.setFocus(listStart, listEnd);
} }
const { listStart, listEnd } = this.state;
const height = TH_HEIGHT + nodes.length * TR_HEIGHT; const height = TH_HEIGHT + nodes.length * TR_HEIGHT;
const transform = `translateY(${listStart * TR_HEIGHT}px)`; const transform = `translateY(${listStart * TR_HEIGHT}px)`;