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

---
 .../CollectionMulticolumnView.tsx                  | 205 +++++++++++++++++++++
 1 file changed, 205 insertions(+)
 create mode 100644 src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx

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

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
-- 
cgit v1.2.3-70-g09d2


From 9e90f5cb1d481889def9fe22b7e5d152777ea95d Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 18 Jan 2020 16:44:31 -0500
Subject: cleanup and commenting

---
 .../CollectionMulticolumnView.tsx                  | 92 +++++++++++++++-------
 1 file changed, 62 insertions(+), 30 deletions(-)

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

diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index e57e5eb26..d2209224f 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -16,16 +16,13 @@ import ResizeBar from './MulticolumnResizer';
 type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
 const MulticolumnDocument = makeInterface(documentSchema);
 
-interface Unresolved {
-    target: Doc;
+interface WidthSpecifier {
     magnitude: number;
     unit: string;
 }
 
 interface LayoutData {
-    unresolved: Unresolved[];
-    numFixed: number;
-    numRatio: number;
+    widthSpecifiers: WidthSpecifier[];
     starSum: number;
 }
 
@@ -35,42 +32,57 @@ const resizerWidth = 4;
 @observer
 export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
 
+    /**
+     * @returns the list of layout documents whose width unit is
+     * *, denoting that it will be displayed with a ratio, not fixed pixel, value
+     */
     @computed
     private get ratioDefinedDocs() {
         return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === "*");
     }
 
+    /**
+     * This loops through all childLayoutPairs and extracts the values for widthUnit
+     * and widthMagnitude, ignoring any that are malformed. Additionally, it then
+     * normalizes the ratio values so that one * value is always 1, with the remaining
+     * values proportionate to that easily readable metric.
+     * @returns the list of the resolved width specifiers (unit and magnitude pairs)
+     * as well as the sum of the * coefficients, i.e. the ratio magnitudes
+     */
     @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);
+        let starSum = 0;
+        const widthSpecifiers: WidthSpecifier[] = [];
+        this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => {
+            const unit = StrCast(widthUnit);
+            const magnitude = NumCast(widthMagnitude);
             if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
-                if (unit === "*") {
-                    starSum += magnitude;
-                    numRatio++;
-                } else {
-                    numFixed++;
-                }
-                unresolved.push({ target: layout, magnitude, unit });
+                (unit === "*") && (starSum += magnitude);
+                widthSpecifiers.push({ 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
-        }
-
+            /**
+             * Otherwise, the child document is ignored and the remaining
+             * space is allocated as if the document were absent from the child list
+             */
+        });
 
+        /**
+         * Here, since these values are all relative, adjustments during resizing or
+         * manual updating can, though their ratios remain the same, cause the values
+         * themselves to drift toward zero. Thus, whenever we change any of the values,
+         * we normalize everything (dividing by the smallest magnitude).
+         */
         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);
+                if (minimum !== 0) {
+                    ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum);
+                }
             }
         });
 
-        return { unresolved, numRatio, numFixed, starSum };
+        return { widthSpecifiers, starSum };
     }
 
     /**
@@ -83,12 +95,12 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
      */
     @computed
     private get totalFixedAllocation(): number | undefined {
-        return this.resolvedLayoutInformation?.unresolved.reduce(
+        return this.resolvedLayoutInformation?.widthSpecifiers.reduce(
             (sum, { magnitude, unit }) => sum + (unit === "px" ? magnitude : 0), 0);
     }
 
     /**
-     * This returns the total quantity, in pixels, that 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.
@@ -98,14 +110,14 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
      */
     @computed
     private get totalRatioAllocation(): number | undefined {
-        const layoutInfoLen = this.resolvedLayoutInformation?.unresolved.length;
+        const layoutInfoLen = this.ratioDefinedDocs.length;
         if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
             return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
         }
     }
 
     /**
-     * This returns the total quantity, in pixels, that
+     * @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,
@@ -123,8 +135,19 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
         }
     }
 
+    /**
+     * This wrapper function exists to prevent mobx from
+     * needlessly rerendering the internal ContentFittingDocumentViews
+     */
     private getColumnUnitLength = () => this.columnUnitLength;
 
+    /**
+     * @param layout the document whose transform we'd like to compute
+     * Given a layout document, this function
+     * returns the resolved width it has requested, in pixels.
+     * @returns the stored column width if already in pixels,
+     * or the ratio width evaluated to a pixel value
+     */
     private lookupPixels = (layout: Doc): number => {
         const columnUnitLength = this.columnUnitLength;
         if (columnUnitLength === undefined) {
@@ -137,6 +160,12 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
         return width;
     }
 
+    /**
+     * @returns the transform that will correctly place
+     * the document decorations box, shifted to the right by
+     * the sum of all the resolved column widths of the
+     * documents before the target. 
+     */
     private lookupIndividualTransform = (layout: Doc) => {
         const columnUnitLength = this.columnUnitLength;
         if (columnUnitLength === undefined) {
@@ -145,14 +174,17 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
         let offset = 0;
         for (const { layout: candidate } of this.childLayoutPairs) {
             if (candidate === layout) {
-                const shift = offset;
-                return this.props.ScreenToLocalTransform().translate(-shift, 0);
+                return this.props.ScreenToLocalTransform().translate(-offset, 0);
             }
             offset += this.lookupPixels(candidate) + resizerWidth;
         }
         return Transform.Identity(); // type coersion, this case should never be hit
     }
 
+    /**
+     * @returns the resolved list of rendered child documents, displayed
+     * at their resolved pixel widths, each separated by a resizer. 
+     */
     @computed
     private get contents(): JSX.Element[] | null {
         const { childLayoutPairs } = this;
-- 
cgit v1.2.3-70-g09d2


From ca7faa46af2bcbc6651891c8f1430dedea55b8be Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 18 Jan 2020 17:33:24 -0500
Subject: switched mcview labels to editable views

---
 .../CollectionMulticolumnView.scss                 |  6 ++--
 .../CollectionMulticolumnView.tsx                  | 19 +++++++-----
 .../collectionMulticolumn/MulticolumnResizer.tsx   | 11 +++----
 .../MulticolumnWidthLabel.tsx                      | 34 ++++++++++++++++++++--
 4 files changed, 53 insertions(+), 17 deletions(-)

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

diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
index 062b8ac11..f57ba438a 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -8,8 +8,10 @@
         display: flex;
         flex-direction: column;
 
-        .display {
-            text-align: center;
+        .label-wrapper {
+            display: flex;
+            flex-direction: row;
+            justify-content: center;
             height: 20px;
         }
 
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index d2209224f..70e56183c 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -26,7 +26,12 @@ interface LayoutData {
     starSum: number;
 }
 
-const resolvedUnits = ["*", "px"];
+export const WidthUnit = {
+    Pixel: "px",
+    Ratio: "*"
+};
+
+const resolvedUnits = Object.values(WidthUnit);
 const resizerWidth = 4;
 
 @observer
@@ -38,7 +43,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
      */
     @computed
     private get ratioDefinedDocs() {
-        return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === "*");
+        return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio);
     }
 
     /**
@@ -57,7 +62,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
             const unit = StrCast(widthUnit);
             const magnitude = NumCast(widthMagnitude);
             if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
-                (unit === "*") && (starSum += magnitude);
+                (unit === WidthUnit.Ratio) && (starSum += magnitude);
                 widthSpecifiers.push({ magnitude, unit });
             }
             /**
@@ -74,7 +79,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
          */
         setTimeout(() => {
             const { ratioDefinedDocs } = this;
-            if (ratioDefinedDocs.length > 1) {
+            if (this.childLayoutPairs.length) {
                 const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude)));
                 if (minimum !== 0) {
                     ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum);
@@ -96,7 +101,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
     @computed
     private get totalFixedAllocation(): number | undefined {
         return this.resolvedLayoutInformation?.widthSpecifiers.reduce(
-            (sum, { magnitude, unit }) => sum + (unit === "px" ? magnitude : 0), 0);
+            (sum, { magnitude, unit }) => sum + (unit === WidthUnit.Pixel ? magnitude : 0), 0);
     }
 
     /**
@@ -110,7 +115,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
      */
     @computed
     private get totalRatioAllocation(): number | undefined {
-        const layoutInfoLen = this.ratioDefinedDocs.length;
+        const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length;
         if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
             return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
         }
@@ -154,7 +159,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
             return 0; // we're still waiting on promises to resolve
         }
         let width = NumCast(layout.widthMagnitude);
-        if (StrCast(layout.widthUnit) === "*") {
+        if (StrCast(layout.widthUnit) === WidthUnit.Ratio) {
             width *= columnUnitLength;
         }
         return width;
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index aab98ecfd..11e210958 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -3,6 +3,7 @@ import { observer } from "mobx-react";
 import { observable, action } from "mobx";
 import { Doc } from "../../../../new_fields/Doc";
 import { NumCast, StrCast } from "../../../../new_fields/Types";
+import { WidthUnit } from "./CollectionMulticolumnView";
 
 interface ResizerProps {
     width: number;
@@ -46,12 +47,12 @@ export default class ResizeBar extends React.Component<ResizerProps> {
         if (unitLength) {
             if (toNarrow) {
                 const { widthUnit, widthMagnitude } = toNarrow;
-                const scale = widthUnit === "*" ? unitLength : 1;
+                const scale = widthUnit === WidthUnit.Ratio ? 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;
+                const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1;
                 toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale;
             }
         }
@@ -60,17 +61,17 @@ export default class ResizeBar extends React.Component<ResizerProps> {
     private get isActivated() {
         const { toLeft, toRight } = this.props;
         if (toLeft && toRight) {
-            if (StrCast(toLeft.widthUnit) === "px" && StrCast(toRight.widthUnit) === "px") {
+            if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) {
                 return false;
             }
             return true;
         } else if (toLeft) {
-            if (StrCast(toLeft.widthUnit) === "px") {
+            if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) {
                 return false;
             }
             return true;
         } else if (toRight) {
-            if (StrCast(toRight.widthUnit) === "px") {
+            if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) {
                 return false;
             }
             return true;
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
index e33d4c169..b394fed62 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
@@ -3,6 +3,8 @@ import { observer } from "mobx-react";
 import { computed } from "mobx";
 import { Doc } from "../../../../new_fields/Doc";
 import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types";
+import { EditableView } from "../../EditableView";
+import { WidthUnit } from "./CollectionMulticolumnView";
 
 interface WidthLabelProps {
     layout: Doc;
@@ -16,9 +18,35 @@ 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>;
+        const getUnit = () => StrCast(layout.widthUnit);
+        const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3));
+        return (
+            <div className={"label-wrapper"}>
+                <EditableView
+                    GetValue={getMagnitude}
+                    SetValue={value => {
+                        const converted = Number(value);
+                        if (!isNaN(converted) && converted > 0) {
+                            layout.widthMagnitude = converted;
+                            return true;
+                        }
+                        return false;
+                    }}
+                    contents={getMagnitude()}
+                />
+                <EditableView
+                    GetValue={getUnit}
+                    SetValue={value => {
+                        if (Object.values(WidthUnit).includes(value)) {
+                            layout.widthUnit = value;
+                            return true;
+                        }
+                        return false;
+                    }}
+                    contents={getUnit()}
+                />
+            </div>
+        );
     }
 
     render() {
-- 
cgit v1.2.3-70-g09d2

