virtualizedViewManager.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. const DIVIDER_WIDTH = 6;
  2. type ViewColumn = {
  3. column_refs: (HTMLElement | undefined)[];
  4. width: number;
  5. };
  6. type Matrix2D = [number, number, number, number, number, number];
  7. /**
  8. * Tracks the state of the virtualized view and manages the resizing of the columns.
  9. * Children components should call `registerColumnRef` and `registerDividerRef` to register
  10. * their respective refs.
  11. */
  12. export class VirtualizedViewManager {
  13. width: number = 0;
  14. container: HTMLElement | null = null;
  15. dividerRef: HTMLElement | null = null;
  16. resizeObserver: ResizeObserver | null = null;
  17. dividerStartVec: [number, number] | null = null;
  18. spanDrawMatrix: Matrix2D = [1, 0, 0, 1, 0, 0];
  19. spanScalingFactor: number = 1;
  20. minSpanScalingFactor: number = 0.02;
  21. spanSpace: [number, number] = [0, 1000];
  22. spanView: [number, number] = [0, 1000];
  23. columns: {
  24. list: ViewColumn;
  25. span_list: ViewColumn;
  26. };
  27. span_bars: ({ref: HTMLElement; space: [number, number]} | undefined)[] = [];
  28. constructor(columns: {
  29. list: ViewColumn;
  30. span_list: ViewColumn;
  31. }) {
  32. this.columns = columns;
  33. this.onDividerMouseDown = this.onDividerMouseDown.bind(this);
  34. this.onDividerMouseUp = this.onDividerMouseUp.bind(this);
  35. this.onDividerMouseMove = this.onDividerMouseMove.bind(this);
  36. }
  37. onContainerRef(container: HTMLElement | null) {
  38. if (container) {
  39. this.initialize(container);
  40. } else {
  41. this.teardown();
  42. }
  43. }
  44. registerDividerRef(ref: HTMLElement | null) {
  45. if (!ref) {
  46. if (this.dividerRef) {
  47. this.dividerRef.removeEventListener('mousedown', this.onDividerMouseDown);
  48. }
  49. this.dividerRef = null;
  50. return;
  51. }
  52. this.dividerRef = ref;
  53. this.dividerRef.style.width = `${DIVIDER_WIDTH}px`;
  54. this.dividerRef.style.transform = `translateX(${
  55. this.width * (this.columns.list.width - (2 * DIVIDER_WIDTH) / this.width)
  56. }px)`;
  57. ref.addEventListener('mousedown', this.onDividerMouseDown, {passive: true});
  58. }
  59. onDividerMouseDown(event: MouseEvent) {
  60. if (!this.container) {
  61. return;
  62. }
  63. this.dividerStartVec = [event.clientX, event.clientY];
  64. this.container.style.userSelect = 'none';
  65. this.container.addEventListener('mouseup', this.onDividerMouseUp, {passive: true});
  66. this.container.addEventListener('mousemove', this.onDividerMouseMove, {
  67. passive: true,
  68. });
  69. }
  70. onDividerMouseUp(event: MouseEvent) {
  71. if (!this.container || !this.dividerStartVec) {
  72. return;
  73. }
  74. const distance = event.clientX - this.dividerStartVec[0];
  75. const distancePercentage = distance / this.width;
  76. this.columns.list.width = this.columns.list.width + distancePercentage;
  77. this.columns.span_list.width = this.columns.span_list.width - distancePercentage;
  78. this.container.style.userSelect = 'auto';
  79. this.dividerStartVec = null;
  80. this.container.removeEventListener('mouseup', this.onDividerMouseUp);
  81. this.container.removeEventListener('mousemove', this.onDividerMouseMove);
  82. }
  83. onDividerMouseMove(event: MouseEvent) {
  84. if (!this.dividerStartVec || !this.dividerRef) {
  85. return;
  86. }
  87. const distance = event.clientX - this.dividerStartVec[0];
  88. const distancePercentage = distance / this.width;
  89. this.computeSpanDrawMatrix(
  90. this.width,
  91. this.columns.span_list.width - distancePercentage
  92. );
  93. this.dividerRef.style.transform = `translateX(${
  94. this.width * (this.columns.list.width + distancePercentage) - DIVIDER_WIDTH / 2
  95. }px)`;
  96. const listWidth = this.columns.list.width * 100 + distancePercentage * 100 + '%';
  97. const spanWidth = this.columns.span_list.width * 100 - distancePercentage * 100 + '%';
  98. for (let i = 0; i < this.columns.list.column_refs.length; i++) {
  99. const list = this.columns.list.column_refs[i];
  100. if (list) {
  101. list.style.width = listWidth;
  102. }
  103. const span = this.columns.span_list.column_refs[i];
  104. if (span) {
  105. span.style.width = spanWidth;
  106. }
  107. const span_bar = this.span_bars[i];
  108. if (span_bar) {
  109. span_bar.ref.style.transform = this.computeSpanMatrixTransform(span_bar.space);
  110. }
  111. }
  112. }
  113. registerSpanBarRef(ref: HTMLElement | null, space: [number, number], index: number) {
  114. this.span_bars[index] = ref ? {ref, space} : undefined;
  115. }
  116. registerColumnRef(column: string, ref: HTMLElement | null, index: number) {
  117. if (!this.columns[column]) {
  118. throw new TypeError('Invalid column');
  119. }
  120. this.columns[column].column_refs[index] = ref ?? undefined;
  121. }
  122. initialize(container: HTMLElement) {
  123. this.teardown();
  124. this.container = container;
  125. this.resizeObserver = new ResizeObserver(entries => {
  126. const entry = entries[0];
  127. if (!entry) {
  128. throw new Error('ResizeObserver entry is undefined');
  129. }
  130. this.width = entry.contentRect.width;
  131. this.computeSpanDrawMatrix(this.width, this.columns.span_list.width);
  132. if (this.dividerRef) {
  133. this.dividerRef.style.transform = `translateX(${
  134. this.width * this.columns.list.width - DIVIDER_WIDTH / 2
  135. }px)`;
  136. }
  137. });
  138. this.resizeObserver.observe(container);
  139. }
  140. initializeSpanSpace(spanSpace: [number, number], spanView?: [number, number]) {
  141. this.spanSpace = [...spanSpace];
  142. this.spanView = spanView ?? [...spanSpace];
  143. this.computeSpanDrawMatrix(this.width, this.columns.span_list.width);
  144. }
  145. computeSpanDrawMatrix(width: number, span_column_width: number): Matrix2D {
  146. // https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix
  147. // biome-ignore format: off
  148. const mat3: Matrix2D = [
  149. 1, 0, 0,
  150. 1, 0, 0,
  151. ];
  152. if (this.spanSpace[1] === 0 || this.spanView[1] === 0) {
  153. return mat3;
  154. }
  155. const spanColumnWidth = width * span_column_width;
  156. const viewToSpace = this.spanSpace[1] / this.spanView[1];
  157. const physicalToView = spanColumnWidth / this.spanView[1];
  158. mat3[0] = viewToSpace * physicalToView;
  159. this.spanScalingFactor = viewToSpace;
  160. this.minSpanScalingFactor = window.devicePixelRatio / this.width;
  161. this.spanDrawMatrix = mat3;
  162. return mat3;
  163. }
  164. computeSpanMatrixTransform(span_space: [number, number]): string {
  165. const scale = Math.max(
  166. this.minSpanScalingFactor,
  167. (span_space[1] / this.spanView[1]) * this.spanScalingFactor
  168. );
  169. const x = span_space[0] - this.spanView[0];
  170. const translateInPixels = x * this.spanDrawMatrix[0];
  171. return `matrix(${scale},0,0,1,${translateInPixels},0)`;
  172. }
  173. draw() {}
  174. teardown() {
  175. if (this.resizeObserver) {
  176. this.resizeObserver.disconnect();
  177. }
  178. }
  179. }