// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /*----------------------------------------------------------------------------- | Copyright (c) 2014-2019, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ import { ArrayExt } from '@lumino/algorithm'; /** * An object which manages a collection of variable sized sections. * * #### Notes * This class is an implementation detail. It is designed to manage * the variable row and column sizes for a data grid. User code will * not interact with this class directly. */ export class SectionList { /** * Construct a new section list. * * @param options - The options for initializing the list. */ constructor(options: SectionList.IOptions) { this._minimumSize = options.minimumSize || 2; this._defaultSize = Math.max( this._minimumSize, Math.floor(options.defaultSize) ); } /** * The total size of all sections in the list. * * #### Complexity * Constant. */ get length(): number { return this._length; } /** * The total number of sections in the list. * * #### Complexity * Constant. */ get count(): number { return this._count; } /** * Get the minimum size of sections in the list. * * #### Complexity * Constant. */ get minimumSize(): number { return this._minimumSize; } /** * Set the minimum size of sections in the list. * * #### Complexity * Linear on the number of resized sections. */ set minimumSize(value: number) { // Normalize the value. value = Math.max(2, Math.floor(value)); // Bail early if the value does not change. if (this._minimumSize === value) { return; } // Update the internal minimum size. this._minimumSize = value; // Update default size if larger than minimum size if (value > this._defaultSize) { this.defaultSize = value; } } /** * Get the default size of sections in the list. * * #### Complexity * Constant. */ get defaultSize(): number { return this._defaultSize; } /** * Set the default size of sections in the list. * * #### Complexity * Linear on the number of resized sections. */ set defaultSize(value: number) { // Normalize the value. value = Math.max(this._minimumSize, Math.floor(value)); // Bail early if the value does not change. if (this._defaultSize === value) { return; } // Compute the delta default size. let delta = value - this._defaultSize; // Update the internal default size. this._defaultSize = value; // Update the length. this._length += delta * (this._count - this._sections.length); // Bail early if there are no modified sections. if (this._sections.length === 0) { return; } // Recompute the offsets of the modified sections. for (let i = 0, n = this._sections.length; i < n; ++i) { // Look up the previous and current modified sections. let prev = this._sections[i - 1]; let curr = this._sections[i]; // Adjust the offset for the current section. if (prev) { let count = curr.index - prev.index - 1; curr.offset = prev.offset + prev.size + count * value; } else { curr.offset = curr.index * value; } } } /** * Clamp a size to the minimum section size * * @param size - The size to clamp. * * @returns The size or the section minimum size, whichever is larger */ clampSize(size: number): number { return Math.max(this._minimumSize, Math.floor(size)); } /** * Find the index of the section which covers the given offset. * * @param offset - The offset of the section of interest. * * @returns The index of the section which covers the given offset, * or `-1` if the offset is out of range. * * #### Complexity * Logarithmic on the number of resized sections. */ indexOf(offset: number): number { // Bail early if the offset is out of range. if (offset < 0 || offset >= this._length || this._count === 0) { return -1; } // Handle the simple case of no modified sections. if (this._sections.length === 0) { return Math.floor(offset / this._defaultSize); } // Find the modified section for the given offset. let i = ArrayExt.lowerBound(this._sections, offset, Private.offsetCmp); // Return the index of an exact match. if (i < this._sections.length && this._sections[i].offset <= offset) { return this._sections[i].index; } // Handle the case of no modified sections before the offset. if (i === 0) { return Math.floor(offset / this._defaultSize); } // Compute the index from the previous modified section. let section = this._sections[i - 1]; let span = offset - (section.offset + section.size); return section.index + Math.floor(span / this._defaultSize) + 1; } /** * Find the offset of the section at the given index. * * @param index - The index of the section of interest. * * @returns The offset of the section at the given index, or `-1` * if the index is out of range. * * #### Undefined Behavior * An `index` which is non-integral. * * #### Complexity * Logarithmic on the number of resized sections. */ offsetOf(index: number): number { // Bail early if the index is out of range. if (index < 0 || index >= this._count) { return -1; } // Handle the simple case of no modified sections. if (this._sections.length === 0) { return index * this._defaultSize; } // Find the modified section for the given index. let i = ArrayExt.lowerBound(this._sections, index, Private.indexCmp); // Return the offset of an exact match. if (i < this._sections.length && this._sections[i].index === index) { return this._sections[i].offset; } // Handle the case of no modified sections before the index. if (i === 0) { return index * this._defaultSize; } // Compute the offset from the previous modified section. let section = this._sections[i - 1]; let span = index - section.index - 1; return section.offset + section.size + span * this._defaultSize; } /** * Find the extent of the section at the given index. * * @param index - The index of the section of interest. * * @returns The extent of the section at the given index, or `-1` * if the index is out of range. * * #### Undefined Behavior * An `index` which is non-integral. * * #### Complexity * Logarithmic on the number of resized sections. */ extentOf(index: number): number { // Bail early if the index is out of range. if (index < 0 || index >= this._count) { return -1; } // Handle the simple case of no modified sections. if (this._sections.length === 0) { return (index + 1) * this._defaultSize - 1; } // Find the modified section for the given index. let i = ArrayExt.lowerBound(this._sections, index, Private.indexCmp); // Return the offset of an exact match. if (i < this._sections.length && this._sections[i].index === index) { return this._sections[i].offset + this._sections[i].size - 1; } // Handle the case of no modified sections before the index. if (i === 0) { return (index + 1) * this._defaultSize - 1; } // Compute the offset from the previous modified section. let section = this._sections[i - 1]; let span = index - section.index; return section.offset + section.size + span * this._defaultSize - 1; } /** * Find the size of the section at the given index. * * @param index - The index of the section of interest. * * @returns The size of the section at the given index, or `-1` * if the index is out of range. * * #### Undefined Behavior * An `index` which is non-integral. * * #### Complexity * Logarithmic on the number of resized sections. */ sizeOf(index: number): number { // Bail early if the index is out of range. if (index < 0 || index >= this._count) { return -1; } // Handle the simple case of no modified sections. if (this._sections.length === 0) { return this._defaultSize; } // Find the modified section for the given index. let i = ArrayExt.lowerBound(this._sections, index, Private.indexCmp); // Return the size of an exact match. if (i < this._sections.length && this._sections[i].index === index) { return this._sections[i].size; } // Return the default size for all other cases. return this._defaultSize; } /** * Resize a section in the list. * * @param index - The index of the section to resize. This method * is a no-op if this value is out of range. * * @param size - The new size of the section. This value will be * clamped to an integer `>= 0`. * * #### Undefined Behavior * An `index` which is non-integral. * * #### Complexity * Linear on the number of resized sections. */ resize(index: number, size: number): void { // Bail early if the index is out of range. if (index < 0 || index >= this._count) { return; } // Clamp the size to an integer >= minimum size. size = Math.max(this._minimumSize, Math.floor(size)); // Find the modified section for the given index. let i = ArrayExt.lowerBound(this._sections, index, Private.indexCmp); // Update or create the modified section as needed. let delta: number; if (i < this._sections.length && this._sections[i].index === index) { let section = this._sections[i]; delta = size - section.size; section.size = size; } else if (i === 0) { let offset = index * this._defaultSize; ArrayExt.insert(this._sections, i, { index, offset, size }); delta = size - this._defaultSize; } else { let section = this._sections[i - 1]; let span = index - section.index - 1; let offset = section.offset + section.size + span * this._defaultSize; ArrayExt.insert(this._sections, i, { index, offset, size }); delta = size - this._defaultSize; } // Adjust the length. this._length += delta; // Update all modified sections after the resized section. for (let j = i + 1, n = this._sections.length; j < n; ++j) { this._sections[j].offset += delta; } } /** * Insert sections into the list. * * @param index - The index at which to insert the sections. This * value will be clamped to the bounds of the list. * * @param count - The number of sections to insert. This method * is a no-op if this value is `<= 0`. * * #### Undefined Behavior * An `index` or `count` which is non-integral. * * #### Complexity * Linear on the number of resized sections. */ insert(index: number, count: number): void { // Bail early if there are no sections to insert. if (count <= 0) { return; } // Clamp the index to the bounds of the list. index = Math.max(0, Math.min(index, this._count)); // Add the new sections to the totals. let span = count * this._defaultSize; this._count += count; this._length += span; // Bail early if there are no modified sections to update. if (this._sections.length === 0) { return; } // Find the modified section for the given index. let i = ArrayExt.lowerBound(this._sections, index, Private.indexCmp); // Update all modified sections after the insert location. for (let n = this._sections.length; i < n; ++i) { let section = this._sections[i]; section.index += count; section.offset += span; } } /** * Remove sections from the list. * * @param index - The index of the first section to remove. This * method is a no-op if this value is out of range. * * @param count - The number of sections to remove. This method * is a no-op if this value is `<= 0`. * * #### Undefined Behavior * An `index` or `count` which is non-integral. * * #### Complexity * Linear on the number of resized sections. */ remove(index: number, count: number): void { // Bail early if there is nothing to remove. if (index < 0 || index >= this._count || count <= 0) { return; } // Clamp the count to the bounds of the list. count = Math.min(this._count - index, count); // Handle the simple case of no modified sections to update. if (this._sections.length === 0) { this._count -= count; this._length -= count * this._defaultSize; return; } // Handle the simple case of removing all sections. if (count === this._count) { this._length = 0; this._count = 0; this._sections.length = 0; return; } // Find the modified section for the start index. let i = ArrayExt.lowerBound(this._sections, index, Private.indexCmp); // Find the modified section for the end index. let j = ArrayExt.lowerBound( this._sections, index + count, Private.indexCmp ); // Remove the relevant modified sections. let removed = this._sections.splice(i, j - i); // Compute the total removed span. let span = (count - removed.length) * this._defaultSize; for (let k = 0, n = removed.length; k < n; ++k) { span += removed[k].size; } // Adjust the totals. this._count -= count; this._length -= span; // Update all modified sections after the removed span. for (let k = i, n = this._sections.length; k < n; ++k) { let section = this._sections[k]; section.index -= count; section.offset -= span; } } /** * Move sections within the list. * * @param index - The index of the first section to move. This method * is a no-op if this value is out of range. * * @param count - The number of sections to move. This method is a * no-op if this value is `<= 0`. * * @param destination - The destination index for the first section. * This value will be clamped to the allowable range. * * #### Undefined Behavior * An `index`, `count`, or `destination` which is non-integral. * * #### Complexity * Linear on the number of moved resized sections. */ move(index: number, count: number, destination: number): void { // Bail early if there is nothing to move. if (index < 0 || index >= this._count || count <= 0) { return; } // Handle the simple case of no modified sections. if (this._sections.length === 0) { return; } // Clamp the move count to the limit. count = Math.min(count, this._count - index); // Clamp the destination index to the limit. destination = Math.min(Math.max(0, destination), this._count - count); // Bail early if there is no effective move. if (index === destination) { return; } // Compute the first affected index. let i1 = Math.min(index, destination); // Look up the first affected modified section. let k1 = ArrayExt.lowerBound(this._sections, i1, Private.indexCmp); // Bail early if there are no affected modified sections. if (k1 === this._sections.length) { return; } // Compute the last affected index. let i2 = Math.max(index + count - 1, destination + count - 1); // Look up the last affected modified section. let k2 = ArrayExt.upperBound(this._sections, i2, Private.indexCmp) - 1; // Bail early if there are no affected modified sections. if (k2 < k1) { return; } // Compute the pivot index. let pivot = destination < index ? index : index + count; // Compute the count for each side of the pivot. let count1 = pivot - i1; let count2 = i2 - pivot + 1; // Compute the span for each side of the pivot. let span1 = count1 * this._defaultSize; let span2 = count2 * this._defaultSize; // Adjust the spans for the modified sections. for (let j = k1; j <= k2; ++j) { let section = this._sections[j]; if (section.index < pivot) { span1 += section.size - this._defaultSize; } else { span2 += section.size - this._defaultSize; } } // Look up the pivot section. let k3 = ArrayExt.lowerBound(this._sections, pivot, Private.indexCmp); // Rotate the modified sections if needed. if (k1 <= k3 && k3 <= k2) { ArrayExt.rotate(this._sections, k3 - k1, k1, k2); } // Adjust the modified section indices and offsets. for (let j = k1; j <= k2; ++j) { let section = this._sections[j]; if (section.index < pivot) { section.index += count2; section.offset += span2; } else { section.index -= count1; section.offset -= span1; } } } /** * Reset all modified sections to the default size. * * #### Complexity * Constant. */ reset(): void { this._sections.length = 0; this._length = this._count * this._defaultSize; } /** * Remove all sections from the list. * * #### Complexity * Constant. */ clear(): void { this._count = 0; this._length = 0; this._sections.length = 0; } private _count = 0; private _length = 0; private _minimumSize: number; private _defaultSize: number; private _sections: Private.Section[] = []; } /** * The namespace for the `SectionList` class statics. */ export namespace SectionList { /** * An options object for initializing a section list. */ export interface IOptions { /** * The size of new sections added to the list. */ defaultSize: number; /** * The minimum size of the section list. */ minimumSize?: number; } } /** * The namespace for the module implementation details. */ namespace Private { /** * An object which represents a modified section. */ export type Section = { /** * The index of the section. * * This is always `>= 0`. */ index: number; /** * The offset of the section. */ offset: number; /** * The size of the section. * * This is always `>= 0`. */ size: number; }; /** * A comparison function for searching by offset. */ export function offsetCmp(section: Section, offset: number): number { if (offset < section.offset) { return 1; } if (section.offset + section.size <= offset) { return -1; } return 0; } /** * A comparison function for searching by index. */ export function indexCmp(section: Section, index: number): number { return section.index - index; } }