From f1948385db529e7c3c58990f13922531cb66fb6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Wed, 16 Dec 2020 19:14:52 +0100
Subject: [PATCH] [ivette/eva] rows & probes styling

---
 ivette/src/dome/src/renderer/dome.tsx |   5 +-
 ivette/src/frama-c/eva/Values.tsx     | 192 ++++++++------------------
 ivette/src/frama-c/eva/cells.ts       |   1 -
 ivette/src/frama-c/eva/layout.ts      |  18 ++-
 ivette/src/frama-c/eva/probes.ts      |  16 ++-
 ivette/src/frama-c/eva/sized.tsx      | 112 +++++++++++++++
 ivette/src/frama-c/eva/style.css      |  48 ++++++-
 7 files changed, 236 insertions(+), 156 deletions(-)
 create mode 100644 ivette/src/frama-c/eva/sized.tsx

diff --git a/ivette/src/dome/src/renderer/dome.tsx b/ivette/src/dome/src/renderer/dome.tsx
index b1e1d879afc..237420fec39 100644
--- a/ivette/src/dome/src/renderer/dome.tsx
+++ b/ivette/src/dome/src/renderer/dome.tsx
@@ -490,10 +490,9 @@ export function useForceUpdate() {
 export function useUpdate(...events: Event<any>[]) {
   const fn = useForceUpdate();
   React.useEffect(() => {
-    const trigger = () => setImmediate(fn);
     if (events.length === 0) events.push(update);
-    events.forEach((evt) => evt.on(trigger));
-    return () => events.forEach((evt) => evt.off(trigger));
+    events.forEach((evt) => evt.on(fn));
+    return () => events.forEach((evt) => evt.off(fn));
   }, [fn, ...events]); // eslint-disable-line react-hooks/exhaustive-deps
   // The rule signals events is missing, probably because of « … »
 }
diff --git a/ivette/src/frama-c/eva/Values.tsx b/ivette/src/frama-c/eva/Values.tsx
index b9a984f3f43..c67600aaf12 100644
--- a/ivette/src/frama-c/eva/Values.tsx
+++ b/ivette/src/frama-c/eva/Values.tsx
@@ -5,6 +5,7 @@
 // React & Dome
 import React from 'react';
 import * as Dome from 'dome';
+import { classes } from 'dome/misc/utils';
 import { VariableSizeList } from 'react-window';
 import { Vfill, Hpack, Filler } from 'dome/layout/boxes';
 import { Label, Code } from 'dome/controls/labels';
@@ -23,8 +24,9 @@ import * as Ast from 'frama-c/api/kernel/ast';
 import * as Values from 'frama-c/api/plugins/eva/values';
 
 // Locals
-
-import { Size, callback, sizeof } from './cells';
+import { SizedArea, HSIZER, WSIZER } from './sized';
+import { callback, sizeof } from './cells';
+import { RowKind } from './layout';
 import { Probe } from './probes';
 import { Model } from './model';
 import './style.css';
@@ -65,125 +67,52 @@ function ProbePanel(props: ProbePanelProps) {
   );
 }
 
-// --------------------------------------------------------------------------
-// --- Value Cell
-// --------------------------------------------------------------------------
-
-class Streamer {
-  private readonly v0: number;
-  private readonly vs: number[] = [];
-  private v?: number;
-  constructor(v0: number) {
-    this.v0 = v0;
-  }
-
-  push(v: number) {
-    const { vs } = this;
-    vs.push(Math.round(v));
-    if (vs.length > 200) vs.shift();
-  }
-
-  mean(): number {
-    if (this.v === undefined) {
-      const { vs } = this;
-      const n = vs.length;
-      if (n > 0) {
-        const m = vs.reduce((s, v) => s + v, 0) / n;
-        this.v = Math.round(m + 0.5);
-      } else {
-        this.v = this.v0;
-      }
-    }
-    return this.v;
-  }
-}
-
-class FontSizer {
-  a = 0;
-  b = 0;
-  k: Streamer;
-  p: Streamer;
-
-  constructor(k: number, p: number) {
-    this.k = new Streamer(k);
-    this.p = new Streamer(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.push(k);
-      this.p.push(p);
-    }
-    this.a = x;
-    this.b = y;
-  }
-
-  capacity(y: number) {
-    const k = this.k.mean();
-    const p = this.p.mean();
-    return Math.round(0.5 + (y - p) / k);
-  }
-
-  dimension(n: number) {
-    const k = this.k.mean();
-    const p = this.p.mean();
-    return p + n * 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 { rows, cols, children } = props;
-  const refSizer = React.useCallback(
-    (ref: null | HTMLDivElement) => {
-      if (ref) {
-        const r = ref.getBoundingClientRect();
-        WSIZER.push(cols, r.width);
-        HSIZER.push(rows, r.height);
-      }
-    }, [rows, cols],
-  );
-  return (
-    <div
-      ref={refSizer}
-      className="eva-sized-area dome-text-code"
-    >
-      {children}
-    </div>
-  );
-}
-
 // --------------------------------------------------------------------------
 // --- Table Update
 // --------------------------------------------------------------------------
 
 const ChangeEvent = new Dome.Event<void>('eva-changed');
-const forceUpdate = () => ChangeEvent.emit();
+const forceUpdate = () => setImmediate(ChangeEvent.emit);
 
 // --------------------------------------------------------------------------
 // --- Table Cell
 // --------------------------------------------------------------------------
 
 interface TableCellProps {
+  kind: RowKind;
   probe: Probe;
 }
 
 function TableCell(props: TableCellProps) {
-  const { probe } = props;
+  Dome.useUpdate(ChangeEvent);
+  const { probe, kind } = props;
+  const minWidth = WSIZER.dimension(probe.minCols);
+  const maxWidth = WSIZER.dimension(probe.maxCols);
+  const style = { minWidth, maxWidth };
+  let styling = 'dome-text-code';
+  let contents: React.ReactNode = props.probe.marker;
+  switch (kind) {
+    case 'probes':
+      if (probe.transient) {
+        styling = 'eva-transient dome-text-label';
+        contents = '« Current »';
+      } else if (probe.label) {
+        styling = 'dome-text-label';
+        contents = probe.label;
+      } else {
+        contents = <>{probe.code}</>;
+      }
+      break;
+    case 'values':
+      contents = 'VALUES';
+  }
+  const className = classes(
+    'eva-cell',
+    styling,
+  );
   return (
-    <div className="eva-cell">
-      {probe.marker}
+    <div className={className} style={style}>
+      {contents}
     </div>
   );
 }
@@ -200,30 +129,21 @@ interface TableRowProps {
 
 function TableRow(props: TableRowProps) {
   Dome.useUpdate(ChangeEvent);
-  const { data: vstate, index } = props;
-  const row = vstate.getRow(index);
+  const { data: model, index } = props;
+  const row = model.getRow(index);
   if (!row) return null;
-  let className = '';
-  switch (row.kind) {
-    case 'probes':
-      className = 'eva-row eva-row-probes';
-      break;
-    case 'values':
-    case 'callstack':
-      className = 'eva-row eva-row-values';
-      break;
-  }
-  const contents = row.probes.map((p) => (
-    <TableCell key={p.marker} probe={p} />
+  const { kind, probes } = row;
+  const className = `eva-${kind}`;
+  const contents = probes.map((p) => (
+    <TableCell kind={kind} key={p.marker} probe={p} />
   ));
   return (
-    <div
-      style={props.style}
-    >
-      <Hpack className={className}>
+    <Hpack className={className} style={props.style}>
+      <div className="eva-row">
         {contents}
-      </Hpack>
-    </div>
+      </div>
+      <Filler />
+    </Hpack>
   );
 }
 
@@ -237,11 +157,11 @@ interface Dimension {
 }
 
 interface ValuesPanelProps extends Dimension {
-  vstate: Model;
+  model: Model;
 }
 
 function ValuesPanel(props: ValuesPanelProps) {
-  const { vstate, width, height } = props;
+  const { model, width, height } = props;
   const listRef = React.useRef<VariableSizeList>(null);
   // --- reset line cache
   const forceLayout = React.useCallback(
@@ -252,24 +172,24 @@ function ValuesPanel(props: ValuesPanelProps) {
   );
   // --- compute line height
   const getRowHeight = React.useCallback(
-    (k: number) => HSIZER.dimension(vstate.getRowHeight(k)),
-    [vstate],
+    (k: number) => HSIZER.dimension(model.getRowHeight(k)),
+    [model],
   );
   // --- compute layout
   const margin = WSIZER.capacity(width);
   const rowHeight = HSIZER.dimension(1);
-  vstate.setLayout({ margin }, forceLayout);
+  model.setLayout({ margin }, forceLayout);
   // --- render list
   return (
     <VariableSizeList
       ref={listRef}
-      itemCount={vstate.getRowCount()}
-      itemKey={vstate.getRowKey}
+      itemCount={model.getRowCount()}
+      itemKey={model.getRowKey}
       itemSize={getRowHeight}
       estimatedItemSize={rowHeight}
       width={width}
       height={height}
-      itemData={vstate}
+      itemData={model}
     >
       {TableRow}
     </VariableSizeList>
@@ -281,14 +201,14 @@ function ValuesPanel(props: ValuesPanelProps) {
 // --------------------------------------------------------------------------
 
 function ValuesComponent() {
-  const vstate = React.useMemo(() => new Model(forceUpdate), []);
+  const model = React.useMemo(() => new Model(forceUpdate), []);
   Dome.useUpdate(ChangeEvent);
   Server.useSignal(Values.changed, forceUpdate);
   const [selection] = States.useSelection();
   const target = Ast.jMarker(selection?.current?.marker);
-  const probe = vstate.focus(target);
+  const probe = model.focus(target);
   const makeWindow = (size: Dimension) => (
-    <ValuesPanel vstate={vstate} {...size} />
+    <ValuesPanel model={model} {...size} />
   );
   const rank = probe?.rank;
   const stmt = rank ? `@S${rank}` : undefined;
diff --git a/ivette/src/frama-c/eva/cells.ts b/ivette/src/frama-c/eva/cells.ts
index 3611997bd13..95a2dd5e3c2 100644
--- a/ivette/src/frama-c/eva/cells.ts
+++ b/ivette/src/frama-c/eva/cells.ts
@@ -20,7 +20,6 @@ export interface StateCallbacks {
 
 export interface Size { cols: number; rows: number }
 
-export const LABEL = 12; /* number of chars for labels */
 export const EMPTY: Size = { cols: 0, rows: 0 };
 
 export function sizeof(text?: string): Size {
diff --git a/ivette/src/frama-c/eva/layout.ts b/ivette/src/frama-c/eva/layout.ts
index 2e54293d3e3..931ed11b4bc 100644
--- a/ivette/src/frama-c/eva/layout.ts
+++ b/ivette/src/frama-c/eva/layout.ts
@@ -2,7 +2,7 @@
 /* --- Layout                                                             ---*/
 /* --------------------------------------------------------------------------*/
 
-import { Size, EMPTY, LABEL, addH, ValueCache } from './cells';
+import { Size, EMPTY, addH, ValueCache } from './cells';
 import { Probe } from './probes';
 
 export interface LayoutProps {
@@ -23,13 +23,16 @@ export interface Row {
 /* --- Layout Enfine                                                      ---*/
 /* --------------------------------------------------------------------------*/
 
+const HCROP = 18;
+const VCROP = 1;
+
 export class LayoutEngine {
 
   // --- Setup
 
   private readonly cache: ValueCache;
-  private readonly wcrop: number;
   private readonly hcrop: number;
+  private readonly vcrop: number;
   private readonly margin: number;
 
   constructor(
@@ -38,8 +41,8 @@ export class LayoutEngine {
   ) {
     this.cache = cache;
     const zoom = Math.max(0, props?.zoom ?? 0);
-    this.hcrop = 1 + zoom;
-    this.wcrop = LABEL + 2 * zoom;
+    this.vcrop = VCROP + 2 * zoom;
+    this.hcrop = HCROP + zoom;
     this.margin = props?.margin ?? 80;
     this.push = this.push.bind(this);
   }
@@ -51,16 +54,17 @@ export class LayoutEngine {
 
   crop(s: Size): Size {
     return {
-      cols: Math.max(LABEL, Math.min(s.cols, this.wcrop)),
-      rows: Math.max(1, Math.min(s.rows, this.hcrop)),
+      cols: Math.max(HCROP, Math.min(s.cols, this.hcrop)),
+      rows: Math.max(VCROP, Math.min(s.rows, this.vcrop)),
     };
   }
 
   push(p: Probe) {
     const probeSize = this.cache.getProbeSize(p.marker);
     const s = this.crop(probeSize);
+    p.minCols = s.cols;
+    p.maxCols = Math.max(p.minCols, probeSize.cols);
     if (s.cols + this.rowSize.cols > this.margin) this.flush();
-    p.colwidth = s.cols;
     this.rowSize = addH(this.rowSize, s);
     this.buffer.push(p);
   }
diff --git a/ivette/src/frama-c/eva/probes.ts b/ivette/src/frama-c/eva/probes.ts
index 28f4a4f82a0..19976dd4b58 100644
--- a/ivette/src/frama-c/eva/probes.ts
+++ b/ivette/src/frama-c/eva/probes.ts
@@ -8,7 +8,7 @@ import * as Values from 'frama-c/api/plugins/eva/values';
 import * as Ast from 'frama-c/api/kernel/ast';
 
 // Model
-import { StateCallbacks, LABEL } from './cells';
+import { StateCallbacks } from './cells';
 
 /* --------------------------------------------------------------------------*/
 /* --- Probe Labelling                                                    ---*/
@@ -17,6 +17,7 @@ import { StateCallbacks, LABEL } from './cells';
 const Ka = 'A'.charCodeAt(0);
 const Kz = 'Z'.charCodeAt(0);
 const LabelRing: string[] = [];
+const LabelSize = 12;
 let La = Ka;
 let Lk = 0;
 
@@ -49,7 +50,8 @@ export class Probe {
   code?: string;
   stmt?: string;
   rank?: number;
-  colwidth: number = LABEL;
+  minCols: number = LabelSize;
+  maxCols: number = LabelSize;
 
   constructor(state: StateCallbacks, marker: Ast.marker) {
     this.marker = marker;
@@ -73,10 +75,14 @@ export class Probe {
       .finally(this.state.forceUpdate);
   }
 
+  // --------------------------------------------------------------------------
+  // --- Internal State
+  // --------------------------------------------------------------------------
+
   setPersistent() {
     if (this.transient && this.code) {
       this.transient = false;
-      if (this.code.length > LABEL)
+      if (this.code.length > LabelSize)
         this.label = newLabel();
       this.state.forceLayout();
     }
@@ -93,6 +99,10 @@ export class Probe {
     }
   }
 
+  // --------------------------------------------------------------------------
+  // --- Ordering
+  // --------------------------------------------------------------------------
+
   static order(p: Probe, q: Probe): number {
     const rp = p.rank ?? 0;
     const rq = q.rank ?? 0;
diff --git a/ivette/src/frama-c/eva/sized.tsx b/ivette/src/frama-c/eva/sized.tsx
new file mode 100644
index 00000000000..e092f088c71
--- /dev/null
+++ b/ivette/src/frama-c/eva/sized.tsx
@@ -0,0 +1,112 @@
+// --------------------------------------------------------------------------
+// --- Sized Cell
+// --------------------------------------------------------------------------
+
+import React from 'react';
+
+// --------------------------------------------------------------------------
+// --- Measurer
+// --------------------------------------------------------------------------
+
+export class Streamer {
+  private readonly v0: number;
+  private readonly vs: number[] = [];
+  private v?: number;
+  constructor(v0: number) {
+    this.v0 = v0;
+  }
+
+  push(v: number) {
+    const { vs } = this;
+    vs.push(Math.round(v));
+    if (vs.length > 200) vs.shift();
+  }
+
+  mean(): number {
+    if (this.v === undefined) {
+      const { vs } = this;
+      const n = vs.length;
+      if (n > 0) {
+        const m = vs.reduce((s, v) => s + v, 0) / n;
+        this.v = Math.round(m + 0.5);
+      } else {
+        this.v = this.v0;
+      }
+    }
+    return this.v;
+  }
+}
+
+export class FontSizer {
+  a = 0;
+  b = 0;
+  k: Streamer;
+  p: Streamer;
+
+  constructor(k: number, p: number) {
+    this.k = new Streamer(k);
+    this.p = new Streamer(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.push(k);
+      this.p.push(p);
+    }
+    this.a = x;
+    this.b = y;
+  }
+
+  capacity(y: number) {
+    const k = this.k.mean();
+    const p = this.p.mean();
+    return Math.round(0.5 + (y - p) / k);
+  }
+
+  dimension(n: number) {
+    const k = this.k.mean();
+    const p = this.p.mean();
+    return p + n * k;
+  }
+
+}
+
+/* --------------------------------------------------------------------------*/
+/* ---  Sizing Component                                                  ---*/
+/* --------------------------------------------------------------------------*/
+
+export const WSIZER = new FontSizer(7, 6);
+export const HSIZER = new FontSizer(14, 6);
+
+export interface SizedAreaProps {
+  cols: number;
+  rows: number;
+  children?: React.ReactNode;
+}
+
+export function SizedArea(props: SizedAreaProps) {
+  const { rows, cols, children } = props;
+  const refSizer = React.useCallback(
+    (ref: null | HTMLDivElement) => {
+      if (ref) {
+        const r = ref.getBoundingClientRect();
+        WSIZER.push(cols, r.width);
+        HSIZER.push(rows, r.height);
+      }
+    }, [rows, cols],
+  );
+  return (
+    <div
+      ref={refSizer}
+      className="eva-sized-area dome-text-code"
+    >
+      {children}
+    </div>
+  );
+}
+
+/* --------------------------------------------------------------------------*/
diff --git a/ivette/src/frama-c/eva/style.css b/ivette/src/frama-c/eva/style.css
index fc91e4d5ae9..7debdda86c7 100644
--- a/ivette/src/frama-c/eva/style.css
+++ b/ivette/src/frama-c/eva/style.css
@@ -51,21 +51,57 @@
 }
 
 /* -------------------------------------------------------------------------- */
-/* --- Table Rows                                                         --- */
+/* --- Table Rows General                                                 --- */
 /* -------------------------------------------------------------------------- */
 
-.eva-row-probes {
-    background: #cbe4cb;
+.eva-row {
+    display: flex;
+    flex: 0 1 auto;
+    height: 100%;
+    border-bottom: thin solid black;
+    border-right: thin solid black;
 }
 
-.eva-row-values {
+.eva-probes .eva-row {
+    border-top: thin solid black;
+}
+
+.eva-cell {
+    flex: 1 1 auto;
+    padding: 2px;
+    border-left: thin solid black;
+}
 
+.eva-cell:nth-child(last) {
+    border-left: none;
 }
 
-.eva-row-values:nth-child(odd) {
+.eva-probes .eva-cell {
+    text-align: center;
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Table Rows Background                                              --- */
+/* -------------------------------------------------------------------------- */
+
+.eva-probes .eva-row {
+    background: #cbe4cb;
+}
+
+.eva-values .eva-row {
+    background: #fff;
+}
+
+.eva-callstack .eva-row:nth-child(odd) {
     background: #d8edef;
 }
 
-.eva-row-values:nth-child(even) {
+.eva-callsatck .eva-row:nth-child(even) {
     background: #fff;
 }
+
+.eva-transient {
+    background: orange;
+}
+
+/* -------------------------------------------------------------------------- */
-- 
GitLab