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