diff --git a/ivette/.gitignore b/ivette/.gitignore index d77ab83c7bc84b3d2b1435f00e2ddb2042d5df65..0ad2d3aec30181df58b19a1867020acd77d3bc04 100644 --- a/ivette/.gitignore +++ b/ivette/.gitignore @@ -13,6 +13,7 @@ yarn-error.log /dist /doc/html /src/renderer/loader.ts +/src/renderer/sandbox.ts /src/dome/doc/guides/icons.md /Makefile.plugins diff --git a/ivette/CONTRIBUTING.md b/ivette/CONTRIBUTING.md index 234508561fb240a0bd9647dae6ab67c4d639d702..d17eec67bd6ea0411c6e5ad9aeb18549787ca98b 100644 --- a/ivette/CONTRIBUTING.md +++ b/ivette/CONTRIBUTING.md @@ -54,6 +54,10 @@ Useful extensions: - `ESlint` provides support for lint errors and warnings; - `ES7 React/Redux/GraphQL/React-Native snippets` provides boilerplate snippets; +# Sandboxing + +It is possible to add visual tests and playgrounds inside `src/sandbox` directory. +Please read the associated [src/sandbox/README.md](src/sandbox/README.md) instructions. # Coding rules diff --git a/ivette/Makefile b/ivette/Makefile index 2fdd3467f40a35bb3f38e68c34ab9e0390436648..61a31e5f29c3d8212ae4b664bacabd92e72444d9 100644 --- a/ivette/Makefile +++ b/ivette/Makefile @@ -65,19 +65,29 @@ tsc: dome-pkg dome-templ # -------------------------------------------------------------------------- LOADER=src/renderer/loader.ts +SANDBOX=src/renderer/sandbox.ts PACKAGES=$(shell find src -name "pkg.json") +SANDBOXES=$(shell find src/sandbox -name "*.tsx") lint: pkg dome-pkg: pkg dome-app: pkg dome-dev: pkg dome-dist: pkg -pkg: $(LOADER) + +pkg: $(LOADER) $(SANDBOX) + $(LOADER): $(PACKAGES) ./configure.js ./Makefile - @rm -f $(LOADER) + @rm -f $@ @echo "[Ivette] configure packages" - @node ./configure.js $(LOADER) $(PACKAGES) - @chmod -f a-w $(LOADER) + @node ./configure.js $@ $(PACKAGES) + @chmod -f a-w $@ + +$(SANDBOX): $(SANDBOXES) ./sandboxer.js ./Makefile + @rm -f $@ + @echo "[Ivette] configure sandboxes" + @node ./sandboxer.js $@ $(SANDBOXES) + @chmod -f a-w $@ # -------------------------------------------------------------------------- # --- Frama-C Source Distrib diff --git a/ivette/Makefile.distrib b/ivette/Makefile.distrib index dba6fa37ba930252cee8289297bda4fd21ecfefe..0f933db596ee0febd3157aa739797a507e871fee 100644 --- a/ivette/Makefile.distrib +++ b/ivette/Makefile.distrib @@ -18,6 +18,7 @@ DISTRIB_FILES += ivette/electron-builder.json DISTRIB_FILES += ivette/electron-webpack.json DISTRIB_FILES += ivette/ivette-macos.sh DISTRIB_FILES += ivette/package.json +DISTRIB_FILES += ivette/sandboxer.js DISTRIB_FILES += ivette/src/dome/.gitignore DISTRIB_FILES += ivette/src/dome/CONTRIBUTING.md DISTRIB_FILES += ivette/src/dome/CONTRIBUTORS.md @@ -112,6 +113,7 @@ DISTRIB_FILES += ivette/src/dome/renderer/layout/boxes.tsx DISTRIB_FILES += ivette/src/dome/renderer/layout/dispatch.tsx DISTRIB_FILES += ivette/src/dome/renderer/layout/forms.tsx DISTRIB_FILES += ivette/src/dome/renderer/layout/grids.js +DISTRIB_FILES += ivette/src/dome/renderer/layout/qsplit.tsx DISTRIB_FILES += ivette/src/dome/renderer/layout/splitters.tsx DISTRIB_FILES += ivette/src/dome/renderer/layout/style.css DISTRIB_FILES += ivette/src/dome/renderer/light.css @@ -194,7 +196,6 @@ DISTRIB_FILES += ivette/src/frama-c/server.ts DISTRIB_FILES += ivette/src/frama-c/states.ts DISTRIB_FILES += ivette/src/ivette/index.tsx DISTRIB_FILES += ivette/src/ivette/prefs.tsx -DISTRIB_FILES += ivette/src/ivette/sandbox.tsx DISTRIB_FILES += ivette/src/main/index.js DISTRIB_FILES += ivette/src/renderer/Application.tsx DISTRIB_FILES += ivette/src/renderer/Controller.tsx @@ -203,6 +204,8 @@ DISTRIB_FILES += ivette/src/renderer/Laboratory.tsx DISTRIB_FILES += ivette/src/renderer/Preferences.tsx DISTRIB_FILES += ivette/src/renderer/index.js DISTRIB_FILES += ivette/src/renderer/style.css +DISTRIB_FILES += ivette/src/sandbox/README.md +DISTRIB_FILES += ivette/src/sandbox/qsplit.tsx DISTRIB_FILES += ivette/tests/eva-1.i DISTRIB_FILES += ivette/tests/eva-2.i DISTRIB_FILES += ivette/tsconfig.json diff --git a/ivette/distrib.sh b/ivette/distrib.sh index cb04515a5d49ff8171f697c142803af338512e42..38012edac621ff293d5ea0b1f3964f68d9adc5a4 100755 --- a/ivette/distrib.sh +++ b/ivette/distrib.sh @@ -26,7 +26,7 @@ Distribute() { *) echo "DISTRIB_FILES += $src/$f" >> $Distrib case $f in - *.sh | *.json | */dome/doc/* | configure.js | .* | webpack*.js ) + *.sh | *.json | */dome/doc/* | configure.js | sandboxer.js | .* | webpack*.js ) echo "$f: .ignore" >> $Headers ;; *Make* | *.js* | *.ts* | *.ml*) diff --git a/ivette/headers/header_spec.txt b/ivette/headers/header_spec.txt index be8c7cf2f9ef17c8ccf162b16983bca82a71848d..361c89dfb300142bb207d7bdcf5f4010d130f83b 100644 --- a/ivette/headers/header_spec.txt +++ b/ivette/headers/header_spec.txt @@ -17,6 +17,7 @@ electron-builder.json: .ignore electron-webpack.json: .ignore ivette-macos.sh: .ignore package.json: .ignore +sandboxer.js: .ignore src/dome/.gitignore: .ignore src/dome/CONTRIBUTING.md: .ignore src/dome/CONTRIBUTORS.md: .ignore @@ -111,6 +112,7 @@ src/dome/renderer/layout/boxes.tsx: CEA_LGPL src/dome/renderer/layout/dispatch.tsx: CEA_LGPL src/dome/renderer/layout/forms.tsx: CEA_LGPL src/dome/renderer/layout/grids.js: CEA_LGPL +src/dome/renderer/layout/qsplit.tsx: CEA_LGPL src/dome/renderer/layout/splitters.tsx: CEA_LGPL src/dome/renderer/layout/style.css: .ignore src/dome/renderer/light.css: .ignore @@ -193,7 +195,6 @@ src/frama-c/server.ts: CEA_LGPL src/frama-c/states.ts: CEA_LGPL src/ivette/index.tsx: CEA_LGPL src/ivette/prefs.tsx: CEA_LGPL -src/ivette/sandbox.tsx: CEA_LGPL src/main/index.js: CEA_LGPL src/renderer/Application.tsx: CEA_LGPL src/renderer/Controller.tsx: CEA_LGPL @@ -202,6 +203,8 @@ src/renderer/Laboratory.tsx: CEA_LGPL src/renderer/Preferences.tsx: CEA_LGPL src/renderer/index.js: CEA_LGPL src/renderer/style.css: .ignore +src/sandbox/README.md: .ignore +src/sandbox/qsplit.tsx: CEA_LGPL tests/eva-1.i: .ignore tests/eva-2.i: .ignore tsconfig.json: .ignore diff --git a/ivette/sandboxer.js b/ivette/sandboxer.js new file mode 100644 index 0000000000000000000000000000000000000000..dc1af7ba1f9cc582e4ab18bfc4f848ba0d16747f --- /dev/null +++ b/ivette/sandboxer.js @@ -0,0 +1,24 @@ +// -------------------------------------------------------------------------- +// --- Configure Sandboxes +// --- Called by [make pkg] +// -------------------------------------------------------------------------- + +const path = require('path'); +const fs = require('fs'); + +const loader = process.argv[2]; +const inputFiles = process.argv.slice(3); +let buffer = '// Ivette Sandboxes Loader (generated)\n'; + +inputFiles.forEach((file) => { + try { + const box = path.relative('./src',file); + console.log(`[Ivette] sandbox ${box}`); + buffer += `import '../${box}';\n`; + } catch(err) { + console.error(`[Dome] Error ${file}: ${err}`); + process.exit(1); + } +}); + +fs.writeFileSync(loader, buffer); diff --git a/ivette/src/dome/renderer/layout/qsplit.tsx b/ivette/src/dome/renderer/layout/qsplit.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5a21b802c46968a89371e53156778fc8065b4410 --- /dev/null +++ b/ivette/src/dome/renderer/layout/qsplit.tsx @@ -0,0 +1,489 @@ +/* ************************************************************************ */ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2022 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/* ************************************************************************ */ + +/* -------------------------------------------------------------------------- */ +/* --- Quarter-based Splitter --- */ +/* -------------------------------------------------------------------------- */ + +/** + @packageDocumentation + @module dome/layout/qsplit +*/ + +import * as React from 'react'; +import * as Utils from 'dome/misc/utils'; +import { DraggableCore, DraggableEventHandler } from 'react-draggable'; +import { AutoSizer, Size } from 'react-virtualized'; + +/* -------------------------------------------------------------------------- */ +/* --- Q-Split Properties --- */ +/* -------------------------------------------------------------------------- */ + +export interface QSplitProps { + /** Q-Split additional class. */ + className?: string; + /** Q-Split additional style. */ + style?: React.CSSProperties; + /** Q-Pane to layout in A-quarter. */ + A?: string; + /** Q-Pane to layout in B-quarter. */ + B?: string; + /** Q-Pane to layout in C-quarter. */ + C?: string; + /** Q-Pane to layout in D-quarter. */ + D?: string; + /** Horizontal panes ratio (range `0..1`, default `0.5`). */ + H?: number; + /** Vertical panes ratio (range `0..1`, default `0.5`). */ + V?: number; + /** Dragging ratios callback. */ + setPosition?: (H: number, V: number) => void; + /** Q-Split contents. Shall be (possibly packed) Q-Panes. + Other components would be layout as they are in the + positionned `<div/>` of the Q-Split. */ + children?: React.ReactNode; +} + +/* -------------------------------------------------------------------------- */ +/* --- Split Bars --- */ +/* -------------------------------------------------------------------------- */ + +type DragPos = { position: number, anchor: number, offset: number }; +type Dragging = undefined | DragPos; + +const getDragPosition = + (d: DragPos): number => d.position + d.offset - d.anchor; + +interface BSplitterProps { + hsplit: boolean; + style: React.CSSProperties; + dragging: Dragging; + setDragging: (dragging: Dragging) => void; + setPosition: (P: number) => void; + resetPosition: () => void; +} + +const HPOS = 'dome-xSplitter-hpos-R'; +const VPOS = 'dome-xSplitter-vpos-R'; +const HVPOS = 'dome-xSplitter-hvpos'; +const HANDLE = '.dome-xSplitter-grab'; +const HGRAB = 'dome-xSplitter-grab dome-xSplitter-hgrab'; +const VGRAB = 'dome-xSplitter-grab dome-xSplitter-vgrab'; +const HVGRAB = 'dome-xSplitter-grab dome-xSplitter-hvgrab'; +const DRAGGING = 'dome-color-dragging'; +const DRAGZONE = 'dome-color-dragzone'; + +function BSplitter(props: BSplitterProps): JSX.Element { + const { hsplit, style, dragging } = props; + + const onStart: DraggableEventHandler = + (_evt, data) => { + const startPos = hsplit ? data.node.offsetLeft : data.node.offsetTop; + const anchor = hsplit ? data.x : data.y; + props.setDragging({ position: startPos, offset: anchor, anchor }); + }; + + const onDrag: DraggableEventHandler = + (_evt, data) => { + if (dragging) { + const offset = hsplit ? data.x : data.y; + props.setDragging({ ...dragging, offset }); + } + }; + + const onStop: DraggableEventHandler = + (evt, _data) => { + if (evt.metaKey || evt.altKey || evt.ctrlKey) { + props.resetPosition(); + } else if (dragging) { + props.setPosition(getDragPosition(dragging)); + } + props.setDragging(undefined); + }; + + const dragger = Utils.classes( + hsplit ? HGRAB : VGRAB, + dragging ? DRAGGING : DRAGZONE, + ); + + const css = hsplit ? HPOS : VPOS; + + return ( + <DraggableCore + handle={HANDLE} + onStart={onStart} + onDrag={onDrag} + onStop={onStop} + > + <div + className={css} + style={style} + > + <div className={dragger} /> + </div> + </DraggableCore> + ); +} + +/* -------------------------------------------------------------------------- */ +/* --- Split Node --- */ +/* -------------------------------------------------------------------------- */ + +interface CSplitterProps { + style: React.CSSProperties; + dragX: Dragging; + dragY: Dragging; + setDragX: (dx: Dragging) => void; + setDragY: (dy: Dragging) => void; + resetPosition: () => void; + setPosition: (X: number, Y: number) => void; +} + +function CSplitter(props: CSplitterProps): JSX.Element { + const { style, dragX, dragY } = props; + + const onStart: DraggableEventHandler = + (_evt, data) => { + const startX = data.node.offsetLeft; + const startY = data.node.offsetTop; + const anchorX = data.x; + const anchorY = data.y; + props.setDragX({ position: startX, offset: anchorX, anchor: anchorX }); + props.setDragY({ position: startY, offset: anchorY, anchor: anchorY }); + }; + + const onDrag: DraggableEventHandler = + (_evt, data) => { + if (dragX) props.setDragX({ ...dragX, offset: data.x }); + if (dragY) props.setDragY({ ...dragY, offset: data.y }); + }; + + const onStop: DraggableEventHandler = + (evt, _data) => { + if (evt.metaKey || evt.altKey || evt.ctrlKey) { + props.resetPosition(); + } else if (dragX && dragY) { + const X = getDragPosition(dragX); + const Y = getDragPosition(dragY); + props.setPosition(X, Y); + } + props.setDragX(undefined); + props.setDragY(undefined); + }; + + const dragging = dragX !== undefined && dragY !== undefined; + const dragger = Utils.classes(HVGRAB, dragging ? DRAGGING : DRAGZONE); + return ( + <DraggableCore + handle={HANDLE} + onStart={onStart} + onDrag={onDrag} + onStop={onStop} + > + <div + className={HVPOS} + style={style} + > + <div className={dragger} /> + </div> + </DraggableCore> + ); +} + +/* -------------------------------------------------------------------------- */ +/* --- Q-Split Engine --- */ +/* -------------------------------------------------------------------------- */ + +type QSplitLayout = Map<string, React.CSSProperties>; +const QSplitContext = React.createContext<QSplitLayout>(new Map()); +const NODISPLAY: React.CSSProperties = { display: 'none' }; + +const HSPLIT = ( + left: number, + top: number, + height: number, +): React.CSSProperties => ({ display: 'block', left, top, height }); + +const VSPLIT = ( + left: number, + top: number, + width: number, +): React.CSSProperties => ({ display: 'block', left, top, width }); + +const DISPLAY = ( + layout: QSplitLayout, + id: string | undefined, + left: number, + width: number, + top: number, + height: number, +): void => { + if (id) layout.set(id, { display: 'block', left, width, top, height }); +}; + +interface QSplitEngineProps extends QSplitProps { size: Size } + +const inRange = (P: number, D: number): number => Math.max(0, Math.min(P, D)); + +const getRatio = (P: number, D: number): number => inRange(P, D) / D; + +const getPosition = (d: Dragging, D: number, R: number): number => + d ? inRange(getDragPosition(d), D) : Math.round(D * R); + +type Pid = string | undefined; +type Sid = string | undefined | null; // null means Top + +const sameOf = (P: Pid, Q: Pid): Pid => { + if (P === Q) return P; + if (!P) return Q; + if (!Q) return P; + return undefined; +}; + +const merge = (U: Sid, V: Sid): Sid => { + if (U === V) return U; + if (U === undefined) return V; + if (V === undefined) return U; + return null; +}; + +const fullOf = (A: Pid, B: Pid, C: Pid, D: Pid): Pid => { + const S = merge(A, merge(B, merge(C, D))); + return (S === null ? undefined : S); +}; + +function QSplitEngine(props: QSplitEngineProps): JSX.Element { + const [dragX, setDragX] = React.useState<Dragging>(); + const [dragY, setDragY] = React.useState<Dragging>(); + const layout: QSplitLayout = new Map(); + let hsplit: React.CSSProperties = NODISPLAY; + let vsplit: React.CSSProperties = NODISPLAY; + let hvsplit: React.CSSProperties = NODISPLAY; + const { A, B, C, D, H = 0.5, V = 0.5, size, setPosition } = props; + const { width, height } = size; + const setX = React.useCallback((X: number) => { + if (setPosition) setPosition(getRatio(X, width), V); + }, [setPosition, width, V]); + const setY = React.useCallback((Y: number) => { + if (setPosition) setPosition(H, getRatio(Y, height)); + }, [setPosition, height, H]); + const setXY = React.useCallback((X: number, Y: number) => { + if (setPosition) setPosition(getRatio(X, width), getRatio(Y, height)); + }, [setPosition, width, height]); + const resetX = React.useCallback(() => { + if (setPosition) setPosition(0.5, V); + }, [setPosition, V]); + const resetY = React.useCallback(() => { + if (setPosition) setPosition(H, 0.5); + }, [setPosition, H]); + const resetXY = React.useCallback(() => { + if (setPosition) setPosition(0.5, 0.5); + }, [setPosition]); + const X = getPosition(dragX, width, H); + const Y = getPosition(dragY, height, V); + const RX = width - X - 1; + const RY = height - Y - 1; + const AB = sameOf(A, B); + const AC = sameOf(A, C); + const BD = sameOf(B, D); + const CD = sameOf(C, D); + const ABCD = fullOf(A, B, C, D); + //---------------------------------------- + // [ A ] + //--------------------------------------- + if (ABCD) { + DISPLAY(layout, ABCD, 0, width, 0, height); + } + //---------------------------------------- + // [ A - C ] + //--------------------------------------- + else if (AB && CD) { + vsplit = VSPLIT(0, Y, width); + DISPLAY(layout, AB, 0, width, 0, Y); + DISPLAY(layout, CD, 0, width, Y + 1, RY); + } + //---------------------------------------- + // [ A | B ] + //--------------------------------------- + else if (AC && BD) { + hsplit = HSPLIT(X, 0, height); + DISPLAY(layout, AC, 0, X, 0, height); + DISPLAY(layout, BD, X + 1, RX, 0, height); + } + //---------------------------------------- + // [ A – C|D ] + //---------------------------------------- + else if (AB) { + hsplit = HSPLIT(X, Y, RY); + vsplit = VSPLIT(0, Y, width); + DISPLAY(layout, AB, 0, width, 0, Y); + DISPLAY(layout, C, 0, X, Y + 1, RY); + DISPLAY(layout, D, X + 1, RX, Y + 1, RY); + } + //---------------------------------------- + // [ A | B-D ] + //---------------------------------------- + else if (AC) { + hsplit = HSPLIT(X, 0, height); + vsplit = VSPLIT(X, Y, RY); + DISPLAY(layout, AC, 0, X, 0, height); + DISPLAY(layout, B, X + 1, RX, 0, Y); + DISPLAY(layout, D, X + 1, RX, Y + 1, RY); + } + //---------------------------------------- + // [ A-C | B ] + //---------------------------------------- + else if (BD) { + hsplit = HSPLIT(X, 0, height); + vsplit = VSPLIT(0, Y, X); + DISPLAY(layout, A, 0, X, 0, Y); + DISPLAY(layout, BD, X + 1, RX, 0, height); + DISPLAY(layout, C, 0, X, Y + 1, RY); + } + //---------------------------------------- + // [ A|B - C ] + //---------------------------------------- + else if (CD) { + hsplit = HSPLIT(X, 0, Y); + vsplit = VSPLIT(0, Y, width); + DISPLAY(layout, A, 0, X, 0, Y); + DISPLAY(layout, B, X + 1, RX, 0, Y); + DISPLAY(layout, CD, 0, width, Y + 1, RY); + } + //---------------------------------------- + // [ A, B, C, D ] + //---------------------------------------- + else { + hsplit = HSPLIT(X, 0, height); + vsplit = VSPLIT(0, Y, width); + DISPLAY(layout, A, 0, X, 0, Y); + DISPLAY(layout, B, X + 1, RX, 0, Y); + DISPLAY(layout, C, 0, X, Y + 1, RY); + DISPLAY(layout, D, X + 1, RX, Y + 1, RY); + } + //---------------------------------------- + if (hsplit !== NODISPLAY && vsplit !== NODISPLAY) + hvsplit = { display: 'block', left: X, top: Y }; + //---------------------------------------- + // Rendering + //---------------------------------------- + return ( + <QSplitContext.Provider value={layout}> + <BSplitter + key='HSPLIT' + hsplit={true} + dragging={dragX} + setDragging={setDragX} + setPosition={setX} + resetPosition={resetX} + style={hsplit} + /> + <BSplitter + key='VSPLIT' + hsplit={false} + dragging={dragY} + setDragging={setDragY} + setPosition={setY} + resetPosition={resetY} + style={vsplit} + /> + <CSplitter + key='HVSPLIT' + dragX={dragX} + dragY={dragY} + setDragX={setDragX} + setDragY={setDragY} + setPosition={setXY} + resetPosition={resetXY} + style={hvsplit} + /> + {props.children} + </QSplitContext.Provider> + ); +} + +/* -------------------------------------------------------------------------- */ +/* --- Q-Split --- */ +/* -------------------------------------------------------------------------- */ + +/** Q-Spliiter Container. + + The contained is divided into four quarters named `A`, `B`, `C` and `D` + with the following layout: + + ``` + A | B + ----- + C | D + ``` + + The horizontal and vertical split bars can be dragged to adjust the ratios. + The central node can also be dragged to adust both ratios. + + Any adjacent quarters collapse when they contain either the same component + or one component and `undefined`. The split bars are erased accordingly. + + When all quarters contain the same component or `undefined`, they all + collapse and the only component extends to the full container size. + + Other cases are a bit degenerated and lead to « incomplete » layout. + For instance, when a given component is positionned into two diagonal + corners but the adjacent quarters can not collapse, + it will be positionned into only one quarter. + */ +export function QSplit(props: QSplitProps): JSX.Element { + const CONTAINER = Utils.classes('dome-xSplitter-container', props.className); + return ( + <div className={CONTAINER} style={props.style}> + <AutoSizer> + {(size: Size) => ( + <QSplitEngine size={size} {...props} /> + )} + </AutoSizer> + </div> + ); +} + +/* -------------------------------------------------------------------------- */ +/* --- Q-Pane --- */ +/* -------------------------------------------------------------------------- */ + +export interface QPaneProps { + id: string; /** Q-Pane Identifer. */ + className?: string; /** Additional class of the Q-Pane div. */ + style?: React.CSSProperties; /** Additional style of the Q-Pane div. */ + children?: React.ReactNode; /** Q-Pane contents. */ +} + +/** + Q-Splitter Components. + + Childrens are rendered in a positionned `<div/>` with absolute coordinates. + */ +export function QPane(props: QPaneProps): JSX.Element { + const layout = React.useContext(QSplitContext); + const QPANE = Utils.classes('dome-xSplitter-pane', props.className); + const QSTYLE = Utils.styles(props.style, layout?.get(props.id) ?? NODISPLAY); + return <div className={QPANE} style={QSTYLE}>{props.children}</div>; +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/renderer/layout/splitters.tsx b/ivette/src/dome/renderer/layout/splitters.tsx index e56f47dd345caba272617b45e6cb2071ffaf9edc..032766f6f2e931d3f38cafc2b0849289bd38350c 100644 --- a/ivette/src/dome/renderer/layout/splitters.tsx +++ b/ivette/src/dome/renderer/layout/splitters.tsx @@ -274,13 +274,13 @@ function SplitterEngine(props: SplitterEngineProps): JSX.Element { {A} </div> <DraggableCore + key="split" handle={HANDLE} onStart={onStart} onDrag={onDrag} onStop={onStop} > <div - key="split" className={css.split} style={styleR} > diff --git a/ivette/src/dome/renderer/layout/style.css b/ivette/src/dome/renderer/layout/style.css index 4d6125bb7a614057aae47918d3f1ec2fde06cd41..a2e0fca13db2095fde266199646103b6c8264367 100644 --- a/ivette/src/dome/renderer/layout/style.css +++ b/ivette/src/dome/renderer/layout/style.css @@ -177,6 +177,17 @@ cursor: row-resize ; } +.dome-xSplitter-hvgrab { + position: relative; + z-index: 2 ; + top: -4px ; + left: -4px ; + height: 9px; + width: 9px; + border-radius: 4px; + cursor: move; +} + .dome-xSplitter-hpos-A { position: absolute; left: 0px; height: 100% } .dome-xSplitter-hpos-R { position: absolute; width: 1px; height: 100% } .dome-xSplitter-hpos-B { position: absolute; right: 0px; height: 100% } @@ -184,6 +195,7 @@ .dome-xSplitter-vpos-A { position: absolute; top: 0px; width: 100% } .dome-xSplitter-vpos-R { position: absolute; height: 1px; width: 100% } .dome-xSplitter-vpos-B { position: absolute; bottom: 0px; width: 100% } +.dome-xSplitter-hvpos { position: absolute; width: 1px; height: 1px } .dome-xSplitter-hline, .dome-xSplitter-vline @@ -191,6 +203,12 @@ background: var(--splitter) ; } +.dome-xSplitter-pane +{ + position: absolute; + overflow: hidden; +} + /* -------------------------------------------------------------------------- */ /* --- GridLayout Styles --- */ /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/ivette/index.tsx b/ivette/src/ivette/index.tsx index 6916d0be581172c2799d10c5964d4648934bef7d..9e4a8535bd6f2b56ed3768c854cd75beec1107e4 100644 --- a/ivette/src/ivette/index.tsx +++ b/ivette/src/ivette/index.tsx @@ -36,7 +36,6 @@ import { DefineElement } from 'dome/layout/dispatch'; import { GridItem, GridHbox, GridVbox } from 'dome/layout/grids'; import * as Lab from 'ivette@lab'; import * as Ext from 'ivette@ext'; -import Sandbox from './sandbox'; /* --------------------------------------------------------------------------*/ /* --- Items ---*/ @@ -205,12 +204,22 @@ export function registerStatusbar(status: ToolProps): void { /* --------------------------------------------------------------------------*/ if (DEVEL) { - registerComponent({ - id: 'ivette.sandbox', + registerGroup({ + id: 'sandbox', label: 'Sandbox', - title: 'Ivette Sandbox Component (only in DEVEL mode)', - children: <Sandbox />, + title: 'Ivette Sandbox Components (only in DEVEL mode)', }); + registerView({ + id: 'sandbox', + rank: -2, + label: 'Sandbox', + title: 'Sandbox Playground (only in DEVEL mode)', + layout: [], + }); +} + +export function registerSandbox(props: ComponentProps): void { + if (DEVEL) registerComponent({ ...props, group: 'sandbox' }); } // -------------------------------------------------------------------------- diff --git a/ivette/src/ivette/sandbox.tsx b/ivette/src/ivette/sandbox.tsx deleted file mode 100644 index 23d89ccb4c5446d88ed9f262eddd8f47c72c74a9..0000000000000000000000000000000000000000 --- a/ivette/src/ivette/sandbox.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* CEA (Commissariat à l'énergie atomique et aux énergies */ -/* alternatives) */ -/* */ -/* you can redistribute it and/or modify it under the terms of the GNU */ -/* Lesser General Public License as published by the Free Software */ -/* Foundation, version 2.1. */ -/* */ -/* It is distributed in the hope that it will be useful, */ -/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ -/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ -/* GNU Lesser General Public License for more details. */ -/* */ -/* See the GNU Lesser General Public License version 2.1 */ -/* for more details (enclosed in the file licenses/LGPLv2.1). */ -/* */ -/* ************************************************************************ */ - -/* -------------------------------------------------------------------------- */ -/* --- Sandbox Ivette Component. --- */ -/* --- Only appears in DEVEL mode. --- */ -/* --- Please, keep it empty. --- */ -/* -------------------------------------------------------------------------- */ - -import React from 'react'; -import { Label } from 'dome/controls/labels'; - -export default function Sandbox(): JSX.Element { - return <Label>Hello World!</Label>; -} diff --git a/ivette/src/renderer/Application.tsx b/ivette/src/renderer/Application.tsx index 42bfd85a9f252bb9d1ef723bcf49450eadd3d7f0..95835862a8eeb3dbb2c3caa42dcfe8ff35f6ddff 100644 --- a/ivette/src/renderer/Application.tsx +++ b/ivette/src/renderer/Application.tsx @@ -37,6 +37,7 @@ import * as Extensions from './Extensions'; import * as Laboratory from './Laboratory'; import * as IvettePrefs from 'ivette/prefs'; import './loader'; +import './sandbox'; // -------------------------------------------------------------------------- // --- Main View diff --git a/ivette/src/sandbox/README.md b/ivette/src/sandbox/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f9dcad96ad7f92f27f19b0022cb3065a0ddc34db --- /dev/null +++ b/ivette/src/sandbox/README.md @@ -0,0 +1,19 @@ +# Sandboxed Components + +This directory is for Sandboxed components. +Sandboxed components are only visible in DEV mode (make dev). + +The playground view « Sandbox » is also only visible in DEV mode and can be used to +play with sandboxed components _or_ any other component of the platform. + +All files with `*.tsx` extension inside this directory will be automatically loaded +and shall register sandboxe(s) by using typically: + + Ivette.registerSandbox({ + id: 'sandbox.<ident>', + label: 'My New Feature', + title: 'Testing this new amazing feature', + chidren: <MyNewFeature />, + }) + +Enjoy Sandboxing ! diff --git a/ivette/src/sandbox/qsplit.tsx b/ivette/src/sandbox/qsplit.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3aaae9b5ef12a25cfa8d18f704b174eef5d838ab --- /dev/null +++ b/ivette/src/sandbox/qsplit.tsx @@ -0,0 +1,128 @@ +/* ************************************************************************ */ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2022 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/* ************************************************************************ */ + +/* -------------------------------------------------------------------------- */ +/* --- Sandbox Ivette Component. --- */ +/* --- Only appears in DEVEL mode. --- */ +/* --- Please, keep it empty. --- */ +/* -------------------------------------------------------------------------- */ + +import React from 'react'; +import * as Ctrl from 'dome/controls/buttons'; +import * as Disp from 'dome/controls/displays'; +import * as Box from 'dome/layout/boxes'; +import { QSplit, QPane } from 'dome/layout/qsplit'; +import { registerSandbox } from 'ivette'; + +function Quarter(props: { + value?: string, + setValue: (v: string | undefined) => void, +}): JSX.Element { + const onChange = (s?: string): void => props.setValue(s ? s : undefined); + return ( + <Ctrl.Select value={props.value ?? ''} onChange={onChange}> + <option value=''>-</option> + <option value='A'>A</option> + <option value='B'>B</option> + <option value='C'>C</option> + <option value='D'>D</option> + <option value='E'>E</option> + </Ctrl.Select> + ); +} + +function Pane(props: { id: string, background: string }): JSX.Element { + const { id, background } = props; + const css: React.CSSProperties = { + width: '100%', + height: '100%', + textAlign: 'center', + background, + }; + return ( + <QPane id={id}><div style={css}>{id}</div></QPane> + ); +} + +const round = (r: number): number => Math.round(r * 100) / 100; + +function QSplitSandbox(): JSX.Element { + const [H, setH] = React.useState(0.5); + const [V, setV] = React.useState(0.5); + const [A, setA] = React.useState<string | undefined>('A'); + const [B, setB] = React.useState<string | undefined>('B'); + const [C, setC] = React.useState<string | undefined>('C'); + const [D, setD] = React.useState<string | undefined>('D'); + const setPosition = React.useCallback((h, v) => { + setH(h); + setV(v); + }, [setH, setV]); + const reset = (): void => { + setPosition(0.5, 0.5); + setA('A'); + setB('B'); + setC('C'); + setD('D'); + }; + const clear = (): void => { + setPosition(0.5, 0.5); + setA(undefined); + setB(undefined); + setC(undefined); + setD(undefined); + }; + return ( + <Box.Vfill> + <Box.Hfill> + <Ctrl.Button icon='RELOAD' label='Reset' onClick={reset} /> + <Ctrl.Button icon='TRASH' label='Clear' onClick={clear} /> + <Box.Space /> + <Disp.LCD>H={round(H)} V={round(V)}</Disp.LCD> + <Box.Space /> + <Quarter value={A} setValue={setA} /> + <Quarter value={B} setValue={setB} /> + <Quarter value={C} setValue={setC} /> + <Quarter value={D} setValue={setD} /> + </Box.Hfill> + <QSplit A={A} B={B} C={C} D={D} H={H} V={V} + setPosition={setPosition}> + <Pane id='A' background='lightblue' /> + <Pane id='B' background='lightgreen' /> + <Pane id='C' background='#8282db' /> + <Pane id='D' background='coral' /> + <Pane id='E' background='red' /> + </QSplit> + </Box.Vfill > + ); +} + +/* -------------------------------------------------------------------------- */ +/* --- Sandbox --- */ +/* -------------------------------------------------------------------------- */ + +registerSandbox({ + id: 'sandbox.qsplit', + label: 'Q-Splitters', + children: <QSplitSandbox />, +}); + +/* -------------------------------------------------------------------------- */