From 03dafe7743dc3e70f9e936aec8bf2e49efbfb041 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 17 Jan 2020 17:09:29 -0500
Subject: changed view types around to be stored on extension doc.  added stuff
 to stackingview to create views of fields using ';"

---
 src/client/views/collections/CollectionView.tsx | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

(limited to 'src/client/views/collections/CollectionView.tsx')

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 4bd456233..db4da30d1 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,7 +1,7 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEye } from '@fortawesome/free-regular-svg-icons';
 import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx';
 import { observer } from "mobx-react";
 import * as React from 'react';
 import Lightbox from 'react-image-lightbox-with-rotate';
@@ -10,7 +10,7 @@ import { DateField } from '../../../new_fields/DateField';
 import { Doc, DocListCast } from '../../../new_fields/Doc';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { listSpec } from '../../../new_fields/Schema';
-import { BoolCast, Cast, StrCast } from '../../../new_fields/Types';
+import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types';
 import { ImageField } from '../../../new_fields/URLField';
 import { TraceMobx } from '../../../new_fields/util';
 import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
@@ -50,7 +50,8 @@ export enum CollectionViewType {
     Pivot,
     Linear,
     Staff,
-    Multicolumn
+    Multicolumn,
+    Timeline
 }
 
 export namespace CollectionViewType {
@@ -90,8 +91,18 @@ export class CollectionView extends Touchable<FieldViewProps> {
     @observable private static _safeMode = false;
     public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
 
+    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
+    @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
     get collectionViewType(): CollectionViewType | undefined {
-        const viewField = Cast(this.props.Document.viewType, "number");
+        if (!this.extensionDoc) return CollectionViewType.Invalid;
+        NumCast(this.props.Document.viewType) && setTimeout(() => {
+            if (this.props.Document.viewType) {
+                this.extensionDoc!.viewType = NumCast(this.props.Document.viewType);
+            }
+            Doc.GetProto(this.props.Document).viewType = this.props.Document.viewType = undefined;
+        });
+        const viewField = NumCast(this.extensionDoc.viewType, Cast(this.props.Document.viewType, "number"));
         if (CollectionView._safeMode) {
             if (viewField === CollectionViewType.Freeform) {
                 return CollectionViewType.Tree;
-- 
cgit v1.2.3-70-g09d2


From d9e8815ad25b413b825ae72267b85782e787407c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Fri, 17 Jan 2020 21:33:55 -0500
Subject: created files for mcview subcomponents, removed excess zeros in width
 labels and added colored feedback for resize mode. Need to implement more
 useful version of 'global' resize mode

---
 .../collections/CollectionMulticolumnView.scss     |  31 --
 .../collections/CollectionMulticolumnView.tsx      | 331 ---------------------
 src/client/views/collections/CollectionView.tsx    |   2 +-
 .../CollectionMulticolumnView.scss                 |  31 ++
 .../CollectionMulticolumnView.tsx                  | 205 +++++++++++++
 .../collectionMulticolumn/MulticolumnResizer.tsx   | 115 +++++++
 .../MulticolumnWidthLabel.tsx                      |  28 ++
 7 files changed, 380 insertions(+), 363 deletions(-)
 delete mode 100644 src/client/views/collections/CollectionMulticolumnView.scss
 delete mode 100644 src/client/views/collections/CollectionMulticolumnView.tsx
 create mode 100644 src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
 create mode 100644 src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
 create mode 100644 src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
 create mode 100644 src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx

(limited to 'src/client/views/collections/CollectionView.tsx')

diff --git a/src/client/views/collections/CollectionMulticolumnView.scss b/src/client/views/collections/CollectionMulticolumnView.scss
deleted file mode 100644
index 5fbc35df6..000000000
--- a/src/client/views/collections/CollectionMulticolumnView.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-.collectionMulticolumnView_contents {
-    display: flex;
-    width: 100%;
-    height: 100%;
-    overflow: hidden;
-
-    .document-wrapper {
-        display: flex;
-        flex-direction: column;
-
-        .display {
-            text-align: center;
-            height: 20px;
-        }
-
-    }
-
-    .resizer {
-        background: black;
-        cursor: ew-resize;
-        transition: 0.5s opacity ease;
-        display: flex;
-        flex-direction: column;
-
-        .internal {
-            width: 100%;
-            height: 100%;
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionMulticolumnView.tsx b/src/client/views/collections/CollectionMulticolumnView.tsx
deleted file mode 100644
index ef15b29eb..000000000
--- a/src/client/views/collections/CollectionMulticolumnView.tsx
+++ /dev/null
@@ -1,331 +0,0 @@
-import { observer } from 'mobx-react';
-import { makeInterface } from '../../../new_fields/Schema';
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import * as React from "react";
-import { Doc } from '../../../new_fields/Doc';
-import { NumCast, StrCast, BoolCast } from '../../../new_fields/Types';
-import { ContentFittingDocumentView } from './../nodes/ContentFittingDocumentView';
-import { Utils } from '../../../Utils';
-import "./collectionMulticolumnView.scss";
-import { computed, trace, observable, action } from 'mobx';
-import { Transform } from '../../util/Transform';
-
-type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
-const MulticolumnDocument = makeInterface(documentSchema);
-
-interface Unresolved {
-    target: Doc;
-    magnitude: number;
-    unit: string;
-}
-
-interface LayoutData {
-    unresolved: Unresolved[];
-    numFixed: number;
-    numRatio: number;
-    starSum: number;
-}
-
-const resolvedUnits = ["*", "px"];
-const resizerWidth = 4;
-const resizerOpacity = 1;
-
-@observer
-export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
-
-    @computed
-    private get ratioDefinedDocs() {
-        return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === "*");
-    }
-
-    @computed
-    private get resolvedLayoutInformation(): LayoutData {
-        const unresolved: Unresolved[] = [];
-        let starSum = 0, numFixed = 0, numRatio = 0;
-
-        for (const { layout } of this.childLayoutPairs) {
-            const unit = StrCast(layout.widthUnit);
-            const magnitude = NumCast(layout.widthMagnitude);
-            if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
-                if (unit === "*") {
-                    starSum += magnitude;
-                    numRatio++;
-                } else {
-                    numFixed++;
-                }
-                unresolved.push({ target: layout, magnitude, unit });
-            }
-            // otherwise, the particular configuration entry is ignored and the remaining
-            // space is allocated as if the document were absent from the configuration list
-        }
-
-
-        setTimeout(() => {
-            const { ratioDefinedDocs } = this;
-            if (ratioDefinedDocs.length > 1) {
-                const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude)));
-                ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum);
-            }
-        });
-
-        return { unresolved, numRatio, numFixed, starSum };
-    }
-
-    /**
-     * This returns the total quantity, in pixels, that this
-     * view needs to reserve for child documents that have
-     * (with higher priority) requested a fixed pixel width.
-     * 
-     * If the underlying resolvedLayoutInformation returns null
-     * because we're waiting on promises to resolve, this value will be undefined as well.
-     */
-    @computed
-    private get totalFixedAllocation(): number | undefined {
-        return this.resolvedLayoutInformation?.unresolved.reduce(
-            (sum, { magnitude, unit }) => sum + (unit === "px" ? magnitude : 0), 0);
-    }
-
-    /**
-     * This returns the total quantity, in pixels, that this
-     * view needs to reserve for child documents that have
-     * (with lower priority) requested a certain relative proportion of the
-     * remaining pixel width not allocated for fixed widths.
-     * 
-     * If the underlying totalFixedAllocation returns undefined
-     * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
-     */
-    @computed
-    private get totalRatioAllocation(): number | undefined {
-        const layoutInfoLen = this.resolvedLayoutInformation?.unresolved.length;
-        if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
-            return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
-        }
-    }
-
-    /**
-     * This returns the total quantity, in pixels, that
-     * 1* (relative / star unit) is worth. For example,
-     * if the configuration has three documents, with, respectively,
-     * widths of 2*, 2* and 1*, and the panel width returns 1000px,
-     * this accessor returns 1000 / (2 + 2 + 1), or 200px.
-     * Elsewhere, this is then multiplied by each relative-width
-     * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px).
-     * 
-     * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined
-     * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
-     */
-    @computed
-    private get columnUnitLength(): number | undefined {
-        if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) {
-            return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum;
-        }
-    }
-
-    private getColumnUnitLength = () => this.columnUnitLength;
-
-    private lookupPixels = (layout: Doc): number => {
-        const columnUnitLength = this.columnUnitLength;
-        if (columnUnitLength === undefined) {
-            return 0; // we're still waiting on promises to resolve
-        }
-        let width = NumCast(layout.widthMagnitude);
-        if (StrCast(layout.widthUnit) === "*") {
-            width *= columnUnitLength;
-        }
-        return width;
-    }
-
-    private lookupIndividualTransform = (layout: Doc) => {
-        const columnUnitLength = this.columnUnitLength;
-        if (columnUnitLength === undefined) {
-            return Transform.Identity(); // we're still waiting on promises to resolve
-        }
-        let offset = 0;
-        for (const { layout: candidate } of this.childLayoutPairs) {
-            if (candidate === layout) {
-                const shift = offset;
-                return this.props.ScreenToLocalTransform().translate(-shift, 0);
-            }
-            offset += this.lookupPixels(candidate) + resizerWidth;
-        }
-        return Transform.Identity(); // type coersion, this case should never be hit
-    }
-
-    @computed
-    private get contents(): JSX.Element[] | null {
-        trace();
-        const { childLayoutPairs } = this;
-        const { Document, PanelHeight } = this.props;
-        const collector: JSX.Element[] = [];
-        for (let i = 0; i < childLayoutPairs.length; i++) {
-            const { layout } = childLayoutPairs[i];
-            collector.push(
-                <div className={"document-wrapper"}>
-                    <ContentFittingDocumentView
-                        {...this.props}
-                        key={Utils.GenerateGuid()}
-                        Document={layout}
-                        DataDocument={layout.resolvedDataDoc as Doc}
-                        PanelWidth={() => this.lookupPixels(layout)}
-                        PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)}
-                        getTransform={() => this.lookupIndividualTransform(layout)}
-                    />
-                    <WidthLabel
-                        layout={layout}
-                        collectionDoc={Document}
-                    />
-                </div>,
-                <ResizeBar
-                    width={resizerWidth}
-                    key={Utils.GenerateGuid()}
-                    columnUnitLength={this.getColumnUnitLength}
-                    toLeft={layout}
-                    toRight={childLayoutPairs[i + 1]?.layout}
-                />
-            );
-        }
-        collector.pop(); // removes the final extraneous resize bar
-        return collector;
-    }
-
-    render(): JSX.Element {
-        return (
-            <div
-                className={"collectionMulticolumnView_contents"}
-                ref={this.createDropTarget}
-            >
-                {this.contents}
-            </div>
-        );
-    }
-
-}
-
-interface ResizerProps {
-    width: number;
-    columnUnitLength(): number | undefined;
-    toLeft?: Doc;
-    toRight?: Doc;
-}
-
-enum ResizeMode {
-    Global,
-    Pinned,
-    Undefined
-}
-
-@observer
-class ResizeBar extends React.Component<ResizerProps> {
-    @observable private isHoverActive = false;
-    @observable private isResizingActive = false;
-    private resizeMode = ResizeMode.Undefined;
-
-    private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => {
-        e.stopPropagation();
-        e.preventDefault();
-        this.resizeMode = mode;
-        window.removeEventListener("pointermove", this.onPointerMove);
-        window.removeEventListener("pointerup", this.onPointerUp);
-        window.addEventListener("pointermove", this.onPointerMove);
-        window.addEventListener("pointerup", this.onPointerUp);
-        this.isResizingActive = true;
-    }
-
-    private onPointerMove = ({ movementX }: PointerEvent) => {
-        const { toLeft, toRight, columnUnitLength } = this.props;
-        const movingRight = movementX > 0;
-        const toNarrow = movingRight ? toRight : toLeft;
-        const toWiden = movingRight ? toLeft : toRight;
-        const unitLength = columnUnitLength();
-        if (unitLength) {
-            if (toNarrow) {
-                const { widthUnit, widthMagnitude } = toNarrow;
-                const scale = widthUnit === "*" ? unitLength : 1;
-                toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale;
-            }
-            if (this.resizeMode === ResizeMode.Pinned && toWiden) {
-                const { widthUnit, widthMagnitude } = toWiden;
-                const scale = widthUnit === "*" ? unitLength : 1;
-                toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale;
-            }
-        }
-    }
-
-    private get isActivated() {
-        const { toLeft, toRight } = this.props;
-        if (toLeft && toRight) {
-            if (StrCast(toLeft.widthUnit) === "px" && StrCast(toRight.widthUnit) === "px") {
-                return false;
-            }
-            return true;
-        } else if (toLeft) {
-            if (StrCast(toLeft.widthUnit) === "px") {
-                return false;
-            }
-            return true;
-        } else if (toRight) {
-            if (StrCast(toRight.widthUnit) === "px") {
-                return false;
-            }
-            return true;
-        }
-        return false;
-    }
-
-    @action
-    private onPointerUp = () => {
-        this.resizeMode = ResizeMode.Undefined;
-        this.isResizingActive = false;
-        this.isHoverActive = false;
-        window.removeEventListener("pointermove", this.onPointerMove);
-        window.removeEventListener("pointerup", this.onPointerUp);
-    }
-
-    render() {
-        return (
-            <div
-                className={"resizer"}
-                style={{
-                    width: this.props.width,
-                    opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0
-                }}
-                onPointerEnter={action(() => this.isHoverActive = true)}
-                onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
-            >
-                <div
-                    className={"internal"}
-                    onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)}
-                />
-                <div
-                    className={"internal"}
-                    onPointerDown={e => this.registerResizing(e, ResizeMode.Global)}
-                />
-            </div>
-        );
-    }
-
-}
-
-interface WidthLabelProps {
-    layout: Doc;
-    collectionDoc: Doc;
-    decimals?: number;
-}
-
-@observer
-class WidthLabel extends React.Component<WidthLabelProps> {
-
-    @computed
-    private get contents() {
-        const { layout, decimals } = this.props;
-        const magnitude = NumCast(layout.widthMagnitude).toFixed(decimals ?? 3);
-        const unit = StrCast(layout.widthUnit);
-        return <span className={"display"}>{magnitude} {unit}</span>;
-    }
-
-    render() {
-        return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null);
-    }
-
-}
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index db4da30d1..a665b678b 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -27,7 +27,7 @@ import { CollectionDockingView } from "./CollectionDockingView";
 import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
 import { CollectionLinearView } from './CollectionLinearView';
-import { CollectionMulticolumnView } from './CollectionMulticolumnView';
+import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
 import { CollectionPivotView } from './CollectionPivotView';
 import { CollectionSchemaView } from "./CollectionSchemaView";
 import { CollectionStackingView } from './CollectionStackingView';
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
new file mode 100644
index 000000000..062b8ac11
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -0,0 +1,31 @@
+.collectionMulticolumnView_contents {
+    display: flex;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+
+    .document-wrapper {
+        display: flex;
+        flex-direction: column;
+
+        .display {
+            text-align: center;
+            height: 20px;
+        }
+
+    }
+
+    .resizer {
+        cursor: ew-resize;
+        transition: 0.5s opacity ease;
+        display: flex;
+        flex-direction: column;
+
+        .internal {
+            width: 100%;
+            height: 100%;
+            transition: 0.5s background-color ease;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
new file mode 100644
index 000000000..e57e5eb26
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -0,0 +1,205 @@
+import { observer } from 'mobx-react';
+import { makeInterface } from '../../../../new_fields/Schema';
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
+import * as React from "react";
+import { Doc } from '../../../../new_fields/Doc';
+import { NumCast, StrCast, BoolCast } from '../../../../new_fields/Types';
+import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
+import { Utils } from '../../../../Utils';
+import "./collectionMulticolumnView.scss";
+import { computed, trace, observable, action } from 'mobx';
+import { Transform } from '../../../util/Transform';
+import WidthLabel from './MulticolumnWidthLabel';
+import ResizeBar from './MulticolumnResizer';
+
+type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
+const MulticolumnDocument = makeInterface(documentSchema);
+
+interface Unresolved {
+    target: Doc;
+    magnitude: number;
+    unit: string;
+}
+
+interface LayoutData {
+    unresolved: Unresolved[];
+    numFixed: number;
+    numRatio: number;
+    starSum: number;
+}
+
+const resolvedUnits = ["*", "px"];
+const resizerWidth = 4;
+
+@observer
+export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
+
+    @computed
+    private get ratioDefinedDocs() {
+        return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === "*");
+    }
+
+    @computed
+    private get resolvedLayoutInformation(): LayoutData {
+        const unresolved: Unresolved[] = [];
+        let starSum = 0, numFixed = 0, numRatio = 0;
+
+        for (const { layout } of this.childLayoutPairs) {
+            const unit = StrCast(layout.widthUnit);
+            const magnitude = NumCast(layout.widthMagnitude);
+            if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
+                if (unit === "*") {
+                    starSum += magnitude;
+                    numRatio++;
+                } else {
+                    numFixed++;
+                }
+                unresolved.push({ target: layout, magnitude, unit });
+            }
+            // otherwise, the particular configuration entry is ignored and the remaining
+            // space is allocated as if the document were absent from the configuration list
+        }
+
+
+        setTimeout(() => {
+            const { ratioDefinedDocs } = this;
+            if (ratioDefinedDocs.length > 1) {
+                const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude)));
+                ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum);
+            }
+        });
+
+        return { unresolved, numRatio, numFixed, starSum };
+    }
+
+    /**
+     * This returns the total quantity, in pixels, that this
+     * view needs to reserve for child documents that have
+     * (with higher priority) requested a fixed pixel width.
+     * 
+     * If the underlying resolvedLayoutInformation returns null
+     * because we're waiting on promises to resolve, this value will be undefined as well.
+     */
+    @computed
+    private get totalFixedAllocation(): number | undefined {
+        return this.resolvedLayoutInformation?.unresolved.reduce(
+            (sum, { magnitude, unit }) => sum + (unit === "px" ? magnitude : 0), 0);
+    }
+
+    /**
+     * This returns the total quantity, in pixels, that this
+     * view needs to reserve for child documents that have
+     * (with lower priority) requested a certain relative proportion of the
+     * remaining pixel width not allocated for fixed widths.
+     * 
+     * If the underlying totalFixedAllocation returns undefined
+     * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
+     */
+    @computed
+    private get totalRatioAllocation(): number | undefined {
+        const layoutInfoLen = this.resolvedLayoutInformation?.unresolved.length;
+        if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
+            return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
+        }
+    }
+
+    /**
+     * This returns the total quantity, in pixels, that
+     * 1* (relative / star unit) is worth. For example,
+     * if the configuration has three documents, with, respectively,
+     * widths of 2*, 2* and 1*, and the panel width returns 1000px,
+     * this accessor returns 1000 / (2 + 2 + 1), or 200px.
+     * Elsewhere, this is then multiplied by each relative-width
+     * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px).
+     * 
+     * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined
+     * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
+     */
+    @computed
+    private get columnUnitLength(): number | undefined {
+        if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) {
+            return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum;
+        }
+    }
+
+    private getColumnUnitLength = () => this.columnUnitLength;
+
+    private lookupPixels = (layout: Doc): number => {
+        const columnUnitLength = this.columnUnitLength;
+        if (columnUnitLength === undefined) {
+            return 0; // we're still waiting on promises to resolve
+        }
+        let width = NumCast(layout.widthMagnitude);
+        if (StrCast(layout.widthUnit) === "*") {
+            width *= columnUnitLength;
+        }
+        return width;
+    }
+
+    private lookupIndividualTransform = (layout: Doc) => {
+        const columnUnitLength = this.columnUnitLength;
+        if (columnUnitLength === undefined) {
+            return Transform.Identity(); // we're still waiting on promises to resolve
+        }
+        let offset = 0;
+        for (const { layout: candidate } of this.childLayoutPairs) {
+            if (candidate === layout) {
+                const shift = offset;
+                return this.props.ScreenToLocalTransform().translate(-shift, 0);
+            }
+            offset += this.lookupPixels(candidate) + resizerWidth;
+        }
+        return Transform.Identity(); // type coersion, this case should never be hit
+    }
+
+    @computed
+    private get contents(): JSX.Element[] | null {
+        const { childLayoutPairs } = this;
+        const { Document, PanelHeight } = this.props;
+        const collector: JSX.Element[] = [];
+        for (let i = 0; i < childLayoutPairs.length; i++) {
+            const { layout } = childLayoutPairs[i];
+            collector.push(
+                <div
+                    className={"document-wrapper"}
+                    key={Utils.GenerateGuid()}
+                >
+                    <ContentFittingDocumentView
+                        {...this.props}
+                        Document={layout}
+                        DataDocument={layout.resolvedDataDoc as Doc}
+                        PanelWidth={() => this.lookupPixels(layout)}
+                        PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)}
+                        getTransform={() => this.lookupIndividualTransform(layout)}
+                    />
+                    <WidthLabel
+                        layout={layout}
+                        collectionDoc={Document}
+                    />
+                </div>,
+                <ResizeBar
+                    width={resizerWidth}
+                    key={Utils.GenerateGuid()}
+                    columnUnitLength={this.getColumnUnitLength}
+                    toLeft={layout}
+                    toRight={childLayoutPairs[i + 1]?.layout}
+                />
+            );
+        }
+        collector.pop(); // removes the final extraneous resize bar
+        return collector;
+    }
+
+    render(): JSX.Element {
+        return (
+            <div
+                className={"collectionMulticolumnView_contents"}
+                ref={this.createDropTarget}
+            >
+                {this.contents}
+            </div>
+        );
+    }
+
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
new file mode 100644
index 000000000..aab98ecfd
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -0,0 +1,115 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, StrCast } from "../../../../new_fields/Types";
+
+interface ResizerProps {
+    width: number;
+    columnUnitLength(): number | undefined;
+    toLeft?: Doc;
+    toRight?: Doc;
+}
+
+enum ResizeMode {
+    Global = "blue",
+    Pinned = "red",
+    Undefined = "black"
+}
+
+const resizerOpacity = 1;
+
+@observer
+export default class ResizeBar extends React.Component<ResizerProps> {
+    @observable private isHoverActive = false;
+    @observable private isResizingActive = false;
+    @observable private resizeMode = ResizeMode.Undefined;
+
+    @action
+    private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => {
+        e.stopPropagation();
+        e.preventDefault();
+        this.resizeMode = mode;
+        window.removeEventListener("pointermove", this.onPointerMove);
+        window.removeEventListener("pointerup", this.onPointerUp);
+        window.addEventListener("pointermove", this.onPointerMove);
+        window.addEventListener("pointerup", this.onPointerUp);
+        this.isResizingActive = true;
+    }
+
+    private onPointerMove = ({ movementX }: PointerEvent) => {
+        const { toLeft, toRight, columnUnitLength } = this.props;
+        const movingRight = movementX > 0;
+        const toNarrow = movingRight ? toRight : toLeft;
+        const toWiden = movingRight ? toLeft : toRight;
+        const unitLength = columnUnitLength();
+        if (unitLength) {
+            if (toNarrow) {
+                const { widthUnit, widthMagnitude } = toNarrow;
+                const scale = widthUnit === "*" ? unitLength : 1;
+                toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale;
+            }
+            if (this.resizeMode === ResizeMode.Pinned && toWiden) {
+                const { widthUnit, widthMagnitude } = toWiden;
+                const scale = widthUnit === "*" ? unitLength : 1;
+                toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale;
+            }
+        }
+    }
+
+    private get isActivated() {
+        const { toLeft, toRight } = this.props;
+        if (toLeft && toRight) {
+            if (StrCast(toLeft.widthUnit) === "px" && StrCast(toRight.widthUnit) === "px") {
+                return false;
+            }
+            return true;
+        } else if (toLeft) {
+            if (StrCast(toLeft.widthUnit) === "px") {
+                return false;
+            }
+            return true;
+        } else if (toRight) {
+            if (StrCast(toRight.widthUnit) === "px") {
+                return false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @action
+    private onPointerUp = () => {
+        this.resizeMode = ResizeMode.Undefined;
+        this.isResizingActive = false;
+        this.isHoverActive = false;
+        window.removeEventListener("pointermove", this.onPointerMove);
+        window.removeEventListener("pointerup", this.onPointerUp);
+    }
+
+    render() {
+        return (
+            <div
+                className={"resizer"}
+                style={{
+                    width: this.props.width,
+                    opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0
+                }}
+                onPointerEnter={action(() => this.isHoverActive = true)}
+                onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
+            >
+                <div
+                    className={"internal"}
+                    onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)}
+                    style={{ backgroundColor: this.resizeMode }}
+                />
+                <div
+                    className={"internal"}
+                    onPointerDown={e => this.registerResizing(e, ResizeMode.Global)}
+                    style={{ backgroundColor: this.resizeMode }}
+                />
+            </div>
+        );
+    }
+
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
new file mode 100644
index 000000000..e33d4c169
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { computed } from "mobx";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types";
+
+interface WidthLabelProps {
+    layout: Doc;
+    collectionDoc: Doc;
+    decimals?: number;
+}
+
+@observer
+export default class WidthLabel extends React.Component<WidthLabelProps> {
+
+    @computed
+    private get contents() {
+        const { layout, decimals } = this.props;
+        const magnitude = +NumCast(layout.widthMagnitude).toFixed(decimals ?? 3);
+        const unit = StrCast(layout.widthUnit);
+        return <span className={"display"}>{magnitude} {unit}</span>;
+    }
+
+    render() {
+        return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null);
+    }
+
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2

