Add SHRINK_ONLY responsive mode.

The current "Fit to screen" mode has the
drawback it always use 100% of available space.

We want a mode where the diff is responsive to
narrow screens but still doesn't use more
width than it would need (initial width).

Change-Id: I4a8a68cbe5c7cccbced7133dd0e1bb2b3d026f86
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 7400295..4b20072 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -213,6 +213,10 @@
   automatic_blink?: boolean;
 }
 
+export declare type DiffResponsiveMode =
+  | 'FULL_RESPONSIVE'
+  | 'SHRINK_ONLY'
+  | 'NONE';
 export declare interface RenderPreferences {
   hide_left_side?: boolean;
   disable_context_control_buttons?: boolean;
@@ -220,6 +224,7 @@
   hide_line_length_indicator?: boolean;
   use_block_expansion?: boolean;
   image_diff_prefs?: ImageDiffPreferences;
+  responsive_mode?: DiffResponsiveMode;
 }
 
 /**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
index 733c940..462c334 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -64,6 +64,10 @@
   };
 }
 
+export function getLineNumberCellWidth(prefs: DiffPreferencesInfo) {
+  return prefs.font_size * 4;
+}
+
 @customElement('gr-diff-builder')
 export class GrDiffBuilderElement extends PolymerElement {
   static get template() {
@@ -212,7 +216,7 @@
     this.$.processor.keyLocations = keyLocations;
 
     this._clearDiffContent();
-    this._builder.addColumns(this.diffElement, prefs.font_size);
+    this._builder.addColumns(this.diffElement, getLineNumberCellWidth(prefs));
 
     const isBinary = !!(this.isImageDiff || this.diff.binary);
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index da1d971..51360b9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -74,8 +74,7 @@
     return sectionEl;
   }
 
-  addColumns(outputEl: HTMLElement, fontSize: number): void {
-    const width = fontSize * 4;
+  addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
     const colgroup = document.createElement('colgroup');
 
     // Add the blame column.
@@ -84,7 +83,7 @@
 
     // Add left-side line number.
     col = this._createElement('col', 'left');
-    col.setAttribute('width', width.toString());
+    col.setAttribute('width', lineNumberWidth.toString());
     colgroup.appendChild(col);
 
     // Add left-side content.
@@ -92,7 +91,7 @@
 
     // Add right-side line number.
     col = document.createElement('col');
-    col.setAttribute('width', width.toString());
+    col.setAttribute('width', lineNumberWidth.toString());
     colgroup.appendChild(col);
 
     // Add right-side content.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
index 4ecfcbf..a16aa07 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -78,8 +78,7 @@
     return sectionEl;
   }
 
-  addColumns(outputEl: HTMLElement, fontSize: number): void {
-    const width = fontSize * 4;
+  addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
     const colgroup = document.createElement('colgroup');
 
     // Add the blame column.
@@ -88,12 +87,12 @@
 
     // Add left-side line number.
     col = document.createElement('col');
-    col.setAttribute('width', width.toString());
+    col.setAttribute('width', lineNumberWidth.toString());
     colgroup.appendChild(col);
 
     // Add right-side line number.
     col = document.createElement('col');
-    col.setAttribute('width', width.toString());
+    col.setAttribute('width', lineNumberWidth.toString());
     colgroup.appendChild(col);
 
     // Add the content.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 5634238..eb50914 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -31,7 +31,11 @@
   GrContextControlsShowConfig,
 } from '../gr-context-controls/gr-context-controls';
 import {BlameInfo} from '../../../types/common';
-import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
+import {
+  DiffInfo,
+  DiffPreferencesInfo,
+  DiffResponsiveMode,
+} from '../../../types/diff';
 import {DiffViewMode, Side} from '../../../constants/constants';
 import {DiffLayer} from '../../../types/types';
 
@@ -71,6 +75,20 @@
   }
 }
 
+export function getResponsiveMode(
+  prefs: DiffPreferencesInfo,
+  renderPrefs?: RenderPreferences
+): DiffResponsiveMode {
+  if (renderPrefs?.responsive_mode) {
+    return renderPrefs.responsive_mode;
+  }
+  // Backwards compatibility to the line_wrapping param.
+  if (prefs.line_wrapping) {
+    return 'FULL_RESPONSIVE';
+  }
+  return 'NONE';
+}
+
 export abstract class GrDiffBuilder {
   private readonly _diff: DiffInfo;
 
@@ -494,9 +512,11 @@
 
     const {beforeNumber, afterNumber} = line;
     if (beforeNumber !== 'FILE' && beforeNumber !== 'LOST') {
-      const lineLimit = !this._prefs.line_wrapping
-        ? this._prefs.line_length
-        : Infinity;
+      const responsiveMode = getResponsiveMode(this._prefs, this._renderPrefs);
+      const lineLimit =
+        responsiveMode !== 'FULL_RESPONSIVE'
+          ? this._prefs.line_length
+          : Infinity;
       const contentText = this._formatText(
         line.text,
         this._prefs.tab_size,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 3d9bba7..fdde355 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -47,7 +47,10 @@
   DiffPreferencesInfoKey,
 } from '../../../types/diff';
 import {GrDiffHighlight} from '../gr-diff-highlight/gr-diff-highlight';
-import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
+import {
+  GrDiffBuilderElement,
+  getLineNumberCellWidth,
+} from '../gr-diff-builder/gr-diff-builder-element';
 import {
   CoverageRange,
   DiffLayer,
@@ -74,7 +77,10 @@
 import {isSafari, toggleClass} from '../../../utils/dom-util';
 import {assertIsDefined} from '../../../utils/common-util';
 import {debounce, DelayedTask} from '../../../utils/async-util';
-import {DiffContextExpandedEventDetail} from '../gr-diff-builder/gr-diff-builder';
+import {
+  DiffContextExpandedEventDetail,
+  getResponsiveMode,
+} from '../gr-diff-builder/gr-diff-builder';
 
 const NO_NEWLINE_BASE = 'No newline at end of base file.';
 const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
@@ -726,33 +732,46 @@
     if (!prefs) return;
 
     this.blame = null;
+    this._updatePreferenceStyles(prefs, this.renderPrefs);
 
+    if (this.diff && !this.noRenderOnPrefsChange) {
+      this._debounceRenderDiffTable();
+    }
+  }
+
+  _updatePreferenceStyles(
+    prefs: DiffPreferencesInfo,
+    renderPrefs?: RenderPreferences
+  ) {
     const lineLength =
       this.path === COMMIT_MSG_PATH
         ? COMMIT_MSG_LINE_LENGTH
         : prefs.line_length;
+    const sideBySide = this.viewMode === 'SIDE_BY_SIDE';
     const stylesToUpdate: {[key: string]: string} = {};
 
-    if (prefs.line_wrapping) {
-      this._diffTableClass = 'full-width';
-      if (this.viewMode === 'SIDE_BY_SIDE') {
-        stylesToUpdate['--content-width'] = 'none';
-        stylesToUpdate['--line-limit'] = `${lineLength}ch`;
-      }
+    const responsiveMode = getResponsiveMode(prefs, renderPrefs);
+    const responsive =
+      responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY';
+    this._diffTableClass = responsive ? 'responsive' : '';
+    const lineLimit = `${lineLength}ch`;
+    stylesToUpdate['--line-limit'] = lineLimit;
+    stylesToUpdate['--content-width'] = responsive ? 'none' : lineLimit;
+    if (responsiveMode === 'SHRINK_ONLY') {
+      // Calculating ideal (initial) width for the whole table.
+      const contentWidth = `${sideBySide ? 2 : 1} * ${lineLimit}`;
+      const lineNumberWidth = `2 * ${getLineNumberCellWidth(prefs)}px`;
+      stylesToUpdate[
+        '--diff-max-width'
+      ] = `calc(${contentWidth} + ${lineNumberWidth})`;
     } else {
-      this._diffTableClass = '';
-      stylesToUpdate['--content-width'] = `${lineLength}ch`;
+      stylesToUpdate['--diff-max-width'] = 'none';
     }
-
     if (prefs.font_size) {
       stylesToUpdate['--font-size'] = `${prefs.font_size}px`;
     }
 
     this.updateStyles(stylesToUpdate);
-
-    if (this.diff && !this.noRenderOnPrefsChange) {
-      this._debounceRenderDiffTable();
-    }
   }
 
   _renderPrefsChanged(renderPrefs?: RenderPreferences) {
@@ -766,6 +785,9 @@
     if (renderPrefs.hide_line_length_indicator) {
       this.classList.add('hide-line-length-indicator');
     }
+    if (this.prefs) {
+      this._updatePreferenceStyles(this.prefs, renderPrefs);
+    }
     this.$.diffBuilder.updateRenderPrefs(renderPrefs);
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index d01943c..5b248da 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -50,6 +50,9 @@
       background-color: var(--diff-blank-background-color);
     }
     .diffContainer {
+      max-width: var(--diff-max-width, none);
+    }
+    .diffContainer {
       display: flex;
       font-family: var(--monospace-font-family);
       @apply --diff-container-styles;
@@ -169,10 +172,10 @@
     .image-diff .content {
       background-color: var(--diff-blank-background-color);
     }
-    .full-width {
+    .responsive {
       width: 100%;
     }
-    .full-width .contentText {
+    .responsive .contentText {
       white-space: break-spaces;
       word-wrap: break-word;
     }
@@ -423,12 +426,12 @@
       color: var(--link-color);
       text-decoration: none;
     }
-    .full-width td.blame {
+    .responsive td.blame {
       overflow: hidden;
       width: 200px;
     }
     /** Support the line length indicator **/
-    .full-width td.content .contentText {
+    .responsive td.content .contentText {
       /*
       Same strategy as in https://stackoverflow.com/questions/1179928/how-can-i-put-a-vertical-line-down-the-center-of-a-div
       */
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index 53a2915..73b587b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -36,7 +36,7 @@
 suite('gr-diff tests', () => {
   let element;
 
-  const MINIMAL_PREFS = {tab_size: 2, line_length: 80};
+  const MINIMAL_PREFS = {tab_size: 2, line_length: 80, font_size: 12};
 
   setup(() => {
 
@@ -85,7 +85,66 @@
     element = basicFixture.instantiate();
     element.prefs = {...MINIMAL_PREFS, line_wrapping: false};
     flush();
-    assert.isNotOk(getComputedStyleValue('--line-limit', element));
+    assert.equal(getComputedStyleValue('--line-limit', element), '80ch');
+  });
+  suite('FULL_RESPONSIVE mode', () => {
+    setup(() => {
+      element = basicFixture.instantiate();
+      element.prefs = {...MINIMAL_PREFS};
+      element.renderPrefs = {responsive_mode: 'FULL_RESPONSIVE'};
+    });
+
+    test('line limit is based on line_length', () => {
+      element.prefs = {...element.prefs, line_length: 100};
+      flush();
+      assert.equal(getComputedStyleValue('--line-limit', element), '100ch');
+    });
+
+    test('content-width should not be defined', () => {
+      flush();
+      assert.equal(getComputedStyleValue('--content-width', element), 'none');
+    });
+  });
+
+  suite('SHRINK_ONLY mode', () => {
+    setup(() => {
+      element = basicFixture.instantiate();
+      element.prefs = {...MINIMAL_PREFS};
+      element.renderPrefs = {responsive_mode: 'SHRINK_ONLY'};
+    });
+
+    test('line limit is based on line_length', () => {
+      element.prefs = {...element.prefs, line_length: 100};
+      flush();
+      assert.equal(getComputedStyleValue('--line-limit', element), '100ch');
+    });
+
+    test('content-width should not be defined', () => {
+      flush();
+      assert.equal(getComputedStyleValue('--content-width', element), 'none');
+    });
+
+    test('max-width considers two content columns in side-by-side', () => {
+      element.viewMode = 'SIDE_BY_SIDE';
+      flush();
+      assert.equal(getComputedStyleValue('--diff-max-width', element),
+          'calc(2 * 80ch + 2 * 48px)');
+    });
+
+    test('max-width considers one content column in unified', () => {
+      element.viewMode = 'UNIFIED_DIFF';
+      flush();
+      assert.equal(getComputedStyleValue('--diff-max-width', element),
+          'calc(1 * 80ch + 2 * 48px)');
+    });
+
+    test('max-width considers font-size', () => {
+      element.prefs = {...element.prefs, font_size: 13};
+      flush();
+      // Each line number column: 4 * 13 = 52px
+      assert.equal(getComputedStyleValue('--diff-max-width', element),
+          'calc(2 * 80ch + 2 * 52px)');
+    });
   });
 
   suite('not logged in', () => {
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 16338335..223f290 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -27,6 +27,7 @@
   DiffFileMetaInfo as DiffFileMetaInfoApi,
   DiffInfo as DiffInfoApi,
   DiffIntralineInfo,
+  DiffResponsiveMode,
   DiffPreferencesInfo as DiffPreferenceInfoApi,
   IgnoreWhitespaceType,
   MarkLength,
@@ -37,6 +38,7 @@
 export {
   ChangeType,
   DiffIntralineInfo,
+  DiffResponsiveMode,
   IgnoreWhitespaceType,
   MarkLength,
   MoveDetails,