From df8ae563feccbf73f31fb5910ad6b0eb8d23570f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Fri, 11 Dec 2020 17:49:42 +0100
Subject: [PATCH] [ivette/eva] sized text code area

---
 ivette/.eslintrc.js               |   2 +
 ivette/src/frama-c/eva/Values.tsx |  82 +++++++++++++++++++++--
 ivette/src/frama-c/eva/style.css  |  10 ++-
 ivette/src/frama-c/eva/vmodel.ts  | 104 ++++++++++++++++++++++++------
 4 files changed, 174 insertions(+), 24 deletions(-)

diff --git a/ivette/.eslintrc.js b/ivette/.eslintrc.js
index 9f7e0ae0e26..322375d876f 100644
--- a/ivette/.eslintrc.js
+++ b/ivette/.eslintrc.js
@@ -24,6 +24,8 @@ module.exports = {
     "react/display-name": "off",
     // Do not enforce component methods order
     "react/sort-comp": "off",
+    // We do not use propTypes
+    "react/require-default-props": "off",
     // Be more strict on usage of useMemo and useRef
     "react-hooks/exhaustive-deps": "error",
     // Allow type any, even if it should be avoided
diff --git a/ivette/src/frama-c/eva/Values.tsx b/ivette/src/frama-c/eva/Values.tsx
index 238241d5a9a..f0834324f6e 100644
--- a/ivette/src/frama-c/eva/Values.tsx
+++ b/ivette/src/frama-c/eva/Values.tsx
@@ -22,7 +22,7 @@ import * as Values from 'frama-c/api/plugins/eva/values';
 
 // Locals
 
-import { callback, Size, VState } from './vmodel';
+import { VState, Size, callback, sizeof } from './vmodel';
 import './style.css';
 
 // --------------------------------------------------------------------------
@@ -40,10 +40,13 @@ interface ProbePanelProps {
 
 function ProbePanel(props: ProbePanelProps) {
   const { transient = false, label, code, stmt } = props;
+  const { width, height } = sizeof(code);
   return code ? (
     <Hpack className="eva-probe">
       <Label className="eva-probe-label">{label && `${label}:`}</Label>
-      <Code className="eva-probe-code">{code}</Code>
+      <div className="eva-probe-code">
+        <SizedArea width={width} height={height}>{code}</SizedArea>
+      </div>
       <Code className="eva-probe-stmt">{stmt}</Code>
       <IconButton
         kind={transient ? 'positive' : 'negative'}
@@ -55,6 +58,71 @@ function ProbePanel(props: ProbePanelProps) {
   ) : null;
 }
 
+// --------------------------------------------------------------------------
+// --- Value Cell
+// --------------------------------------------------------------------------
+
+class FontSizer {
+  a = 0;
+  b = 0;
+  k: number;
+  p: number;
+  constructor(k: number, p: number) {
+    this.k = k;
+    this.p = p;
+  }
+
+  push(x: number, y: number) {
+    const a0 = this.a;
+    const b0 = this.b;
+    if (x !== a0 && a0 !== 0) {
+      const k = (y - b0) / (x - a0);
+      const p = y - k * x;
+      this.k = Math.round(k);
+      this.p = Math.round(p);
+    }
+    this.a = x;
+    this.b = y;
+  }
+
+  capacity(y: number) {
+    return Math.round(0.5 + (y - this.p) / this.k);
+  }
+
+  compute(n: number) {
+    return this.p + n * this.k;
+  }
+
+}
+
+const WSIZER = new FontSizer(7, 6);
+const HSIZER = new FontSizer(14, 6);
+
+interface SizedAreaProps extends Size {
+  children?: React.ReactNode;
+}
+
+function SizedArea(props: SizedAreaProps) {
+  const { height, width, children } = props;
+  const refSizer = React.useCallback(
+    (ref: null | HTMLDivElement) => {
+      if (ref) {
+        const r = ref.getBoundingClientRect();
+        WSIZER.push(width, r.width);
+        HSIZER.push(height, r.height);
+      }
+    }, [height, width],
+  );
+  return (
+    <div
+      ref={refSizer}
+      className="eva-sized-area dome-text-code"
+    >
+      {children}
+    </div>
+  );
+}
+
 // --------------------------------------------------------------------------
 // --- Values Row
 // --------------------------------------------------------------------------
@@ -80,12 +148,18 @@ interface ValuesPanelProps extends Size {
 
 function ValuesPanel(props: ValuesPanelProps) {
   const { vstate, width, height } = props;
-  vstate.setLayout({ width });
+  const getRowHeight = React.useCallback(
+    (k: number) => HSIZER.compute(vstate.getRowHeight(k))
+    , [vstate]);
+  const wmax = WSIZER.capacity(width);
+  const hmax = HSIZER.capacity(height);
+  const layout = { wmax, hmax };
+  vstate.setLayout(layout);
   return (
     <VariableSizeList
       itemCount={vstate.getRowCount()}
       itemKey={vstate.getRowKey}
-      itemSize={vstate.getRowHeight}
+      itemSize={getRowHeight}
       width={width}
       height={height}
       itemData={vstate}
diff --git a/ivette/src/frama-c/eva/style.css b/ivette/src/frama-c/eva/style.css
index ec171dc09fc..c252ac66bc1 100644
--- a/ivette/src/frama-c/eva/style.css
+++ b/ivette/src/frama-c/eva/style.css
@@ -23,7 +23,13 @@
 
 .eva-probe-stmt {
     color: grey;
-    margin-left: 0px;
-    margin-right: 2px;
+    margin-left: 3px;
+    margin-right: 3px;
     margin-top: 3px;
 }
+
+.eva-sized-area {
+    padding: 3px;
+    white-space: pre;
+    overflow: visible;
+}
diff --git a/ivette/src/frama-c/eva/vmodel.ts b/ivette/src/frama-c/eva/vmodel.ts
index 929ab084f95..e90b4606d2d 100644
--- a/ivette/src/frama-c/eva/vmodel.ts
+++ b/ivette/src/frama-c/eva/vmodel.ts
@@ -3,7 +3,7 @@
 // --------------------------------------------------------------------------
 
 // External Libs
-import { debounce } from 'lodash';
+import { throttle } from 'lodash';
 import equal from 'react-fast-compare';
 
 // Frama-C
@@ -23,12 +23,45 @@ export interface StateCallbacks {
   forceLayout: callback;
 }
 
-export interface Size { width: number; height: number }
-
 /* --------------------------------------------------------------------------*/
 /* --- Cell Properties                                                    ---*/
 /* --------------------------------------------------------------------------*/
 
+const LABEL = 12; /* number of chars for labels */
+const EMPTY = { width: 0, height: 0 };
+
+export interface Size { width: number; height: number }
+
+export function sizeof(text?: string): Size {
+  if (!text) return EMPTY;
+  const lines = text.split('\n');
+  return {
+    height: lines.length,
+    width: lines.reduce((w, l) => Math.max(w, l.length), 0),
+  };
+}
+
+export function merge(a: Size, b: Size): Size {
+  return {
+    width: Math.max(a.width, b.width),
+    height: Math.max(a.height, b.height),
+  };
+}
+
+export function addH(a: Size, b: Size, padding = 0): Size {
+  return {
+    width: a.width + b.width + padding,
+    height: Math.max(a.height, b.height),
+  };
+}
+
+export function addV(a: Size, b: Size, padding = 0): Size {
+  return {
+    width: Math.max(a.width, b.width),
+    height: a.height + b.height + padding,
+  };
+}
+
 /* --------------------------------------------------------------------------*/
 /* --- Row Properties                                                     ---*/
 /* --------------------------------------------------------------------------*/
@@ -39,11 +72,13 @@ export class Row {
 
   key: string;
   kind: RowKind;
+  size: Size;
   height = 0;
 
   constructor(kind: RowKind, key: string) {
     this.key = key;
     this.kind = kind;
+    this.size = EMPTY;
   }
 
 }
@@ -54,7 +89,6 @@ export class Row {
 
 const Ka = 'A'.charCodeAt(0);
 const Kz = 'Z'.charCodeAt(0);
-const LabelSize = 6;
 const LabelRing: string[] = [];
 let La = Ka;
 let Lk = 0;
@@ -116,7 +150,7 @@ export class Probe implements StateCallbacks {
   setPersistent() {
     if (this.transient && this.code) {
       this.transient = false;
-      if (this.code.length > LabelSize)
+      if (this.code.length > LABEL)
         this.label = newLabel();
       this.forceLayout();
     }
@@ -141,24 +175,45 @@ export class Probe implements StateCallbacks {
 
 export interface LayoutProps {
   zoom?: number;
-  width?: number;
+  wmax: number;
+  hmax: number;
 }
 
 class LayoutEngine {
 
   // --- Setup
 
-  /* private */ readonly zoom: number;
-  /* private */ readonly width: number;
-  private readonly rows: Row[] = [];
-  constructor(props?: LayoutProps) {
-    this.zoom = props?.zoom ?? 0;
-    this.width = props?.width ?? 0;
+  /* private */ readonly wcrop: number;
+  /* private */ readonly hcrop: number;
+  /* private */ readonly wmax: number;
+  /* private */ readonly hmax: number;
+  /* private */ readonly remanent?: Probe;
+
+  constructor(
+    props: undefined | LayoutProps,
+  ) {
+    const zoom = Math.max(0, props?.zoom ?? 0);
+    this.hcrop = zoom;
+    this.wcrop = LABEL + 2 * zoom;
+    this.wmax = props?.wmax ?? 80;
+    this.hmax = props?.hmax ?? 60;
   }
 
-  // --- Final Rows
+  // --- Buffer
+
+  private buffer?: Row;
+  private readonly rows: Row[] = [];
 
-  flush() { return this.rows; }
+  // --- Flushes current rows
+
+  flush() {
+    const p = this.buffer;
+    if (p) {
+      this.rows.push(p);
+      this.buffer = undefined;
+    }
+    return this.rows;
+  }
 
 }
 
@@ -173,13 +228,15 @@ export class VState implements StateCallbacks {
     this.forceLayout = this.forceLayout.bind(this);
     this.forceReload = this.forceReload.bind(this);
     this.computeLayout = this.computeLayout.bind(this);
-    this.setLayout = debounce(this.setLayout.bind(this), 600);
+    this.setLayout = throttle(this.setLayout.bind(this), 300);
     this.getRowKey = this.getRowKey.bind(this);
+    this.getRowCount = this.getRowCount.bind(this);
     this.getRowHeight = this.getRowHeight.bind(this);
   }
 
   // --- Probes
   private focused?: Probe;
+  private remanent?: Probe; // last transient
   private probes = new Map<string, Probe>();
 
   getProbe(m: string): Probe {
@@ -195,7 +252,12 @@ export class VState implements StateCallbacks {
   focus(m: string | undefined): Probe | undefined {
     if (m) {
       const p = this.getProbe(m);
-      if (p.stmt) this.focused = p;
+      if (p.stmt) {
+        this.focused = p;
+        if (p.transient) this.remanent = p;
+      } else {
+        this.focused = undefined;
+      }
     }
     return this.focused;
   }
@@ -214,8 +276,14 @@ export class VState implements StateCallbacks {
   }
 
   private computeLayout() {
-    const engine = new LayoutEngine(this.layout);
+    const probes: Probe[] = [];
     this.forcedLayout = false;
+    this.probes.forEach((p) => {
+      if (p.code || !p.transient || p === this.remanent) {
+        probes.push(p);
+      }
+    });
+    const engine = new LayoutEngine(this.layout);
     this.rows = engine.flush();
     this.forceUpdate();
   }
@@ -234,7 +302,7 @@ export class VState implements StateCallbacks {
     return row ? row.height : 0;
   }
 
-  // --- Debounced
+  // --- Throttled
   setLayout(ly?: LayoutProps) {
     if (!equal(this.layout, ly)) {
       this.layout = ly;
-- 
GitLab