From d950b902ba940cf283801b5da5ac5e30a44e95e1 Mon Sep 17 00:00:00 2001 From: rlazarini <remi.lazarini@cea.fr> Date: Fri, 29 Nov 2024 13:47:11 +0100 Subject: [PATCH] [Ivette] Sqplit : A new split bar has been added, allowing the top and bottom widths to be changed separately. --- ivette/src/dome/renderer/layout/qsplit.tsx | 221 +++++++++++++++------ ivette/src/ivette/laboratory.tsx | 14 +- ivette/src/sandbox/qsplit.tsx | 23 ++- 3 files changed, 181 insertions(+), 77 deletions(-) diff --git a/ivette/src/dome/renderer/layout/qsplit.tsx b/ivette/src/dome/renderer/layout/qsplit.tsx index 7ac25db00c9..1833cbbd976 100644 --- a/ivette/src/dome/renderer/layout/qsplit.tsx +++ b/ivette/src/dome/renderer/layout/qsplit.tsx @@ -51,12 +51,14 @@ export interface QSplitProps { C?: string; /** Q-Pane to layout in D-quarter. */ D?: string; - /** Horizontal panes ratio (range `0..1`, default `0.5`). */ - H?: number; + /** Horizontal top panes ratio (range `0..1`, default `0.5`). */ + HTOP?: number; + /** Horizontal bottom panes ratio (range `0..1`, default `0.5`). */ + HBOTTOM?: number; /** Vertical panes ratio (range `0..1`, default `0.5`). */ V?: number; /** Dragging ratios callback. */ - setPosition?: (H: number, V: number) => void; + setPosition?: (HTOP: number, HBOTTOM: 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. */ @@ -262,35 +264,80 @@ const fullOf = (A: Pid, B: Pid, C: Pid, D: Pid): Pid => { }; function QSplitEngine(props: QSplitEngineProps): JSX.Element { - const [dragX, setDragX] = React.useState<Dragging>(); + const [dragXTop, setDragXTop] = React.useState<Dragging>(); + const [dragXBottom, setDragXBottom] = React.useState<Dragging>(); const [dragY, setDragY] = React.useState<Dragging>(); const layout: QSplitLayout = new Map(); - let hsplit: React.CSSProperties = NODISPLAY; + let hsplitTop: React.CSSProperties = NODISPLAY; + let hsplitBottom: 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; + let hvsplittop: React.CSSProperties = NODISPLAY; + let hvsplitbottom: React.CSSProperties = NODISPLAY; + + const { A, B, C, D, + HTOP = 0.5, HBOTTOM = 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 maxGap = 0.005; // 0.5% max + function isSmallGap(a: number, b: number): boolean { + return (a < b && a > (b - maxGap)) || (a > b && a < (b + maxGap)); + } + + const setXTop = React.useCallback((X: number) => { + if (setPosition) { + const top = getRatio(X, width); + const bottom = isSmallGap(top, HBOTTOM) ? top : HBOTTOM; + setPosition(top, bottom, V); + } + }, [setPosition, width, HBOTTOM, V]); + const setXBottom = React.useCallback((X: number) => { + if (setPosition) { + const bottom = getRatio(X, width); + const top = isSmallGap(bottom, HTOP) ? bottom : HTOP; + setPosition(top, bottom, V); + } + }, [setPosition, width, HTOP, V]); const setY = React.useCallback((Y: number) => { - if (setPosition) setPosition(H, getRatio(Y, height)); - }, [setPosition, height, H]); + if (setPosition) setPosition(HTOP, HBOTTOM, getRatio(Y, height)); + }, [setPosition, height, HTOP, HBOTTOM]); + const setXYTop = React.useCallback((X: number, Y: number) => { + if (setPosition) { + const top = getRatio(X, width); + const bottom = isSmallGap(top, HBOTTOM) ? top : HBOTTOM; + setPosition(top, bottom, getRatio(Y, height)); + } + }, [setPosition, HBOTTOM, width, height]); + const setXYBottom = React.useCallback((X: number, Y: number) => { + if (setPosition) { + const bottom = getRatio(X, width); + const top = isSmallGap(bottom, HTOP) ? bottom : HTOP; + setPosition(top, bottom, getRatio(Y, height)); + } + }, [setPosition, HTOP, width, height]); const setXY = React.useCallback((X: number, Y: number) => { - if (setPosition) setPosition(getRatio(X, width), getRatio(Y, height)); + if (setPosition) { + const x = getRatio(X, width); + setPosition(x, x, getRatio(Y, height)); + } }, [setPosition, width, height]); - const resetX = React.useCallback(() => { - if (setPosition) setPosition(0.5, V); + const resetXTop = React.useCallback(() => { + if (setPosition) setPosition(0.5, 0.5, V); + }, [setPosition, V]); + const resetXBottom = React.useCallback(() => { + if (setPosition) setPosition(0.5, 0.5, V); }, [setPosition, V]); const resetY = React.useCallback(() => { - if (setPosition) setPosition(H, 0.5); - }, [setPosition, H]); + if (setPosition) setPosition(HTOP, HBOTTOM, 0.5); + }, [setPosition, HTOP, HBOTTOM]); const resetXY = React.useCallback(() => { - if (setPosition) setPosition(0.5, 0.5); + if (setPosition) setPosition(0.5, 0.5, 0.5); }, [setPosition]); - const X = getPosition(dragX, width, H); + + const XTop = getPosition(dragXTop, width, HTOP); + const XBottom = getPosition(dragXBottom, width, HBOTTOM); const Y = getPosition(dragY, height, V); - const RX = width - X - 1; + const RXTop = width - XTop - 1; + const RXBottom = width - XBottom - 1; const RY = height - Y - 1; const AB = sameOf(A, B); const AC = sameOf(A, C); @@ -315,77 +362,100 @@ function QSplitEngine(props: QSplitEngineProps): JSX.Element { // [ AC | BD ] // --------------------------------------- else if (AC && BD) { - hsplit = HSPLIT(X, 0, height); - DISPLAY(layout, BD, X + 1, RX, 0, height); - DISPLAY(layout, AC, 0, X, 0, height); + hsplitTop = HSPLIT(XTop, 0, height); + DISPLAY(layout, BD, XTop + 1, RXTop, 0, height); + DISPLAY(layout, AC, 0, XTop, 0, height); } // ---------------------------------------- // [ AB -- C | D ] // ---------------------------------------- else if (AB) { - hsplit = HSPLIT(X, Y, RY); + hsplitTop = HSPLIT(XTop, Y, RY); vsplit = VSPLIT(0, Y, width); - DISPLAY(layout, D, X + 1, RX, Y + 1, RY); - DISPLAY(layout, C, 0, X, Y + 1, RY); + DISPLAY(layout, D, XTop + 1, RXTop, Y + 1, RY); + DISPLAY(layout, C, 0, XTop, Y + 1, RY); DISPLAY(layout, AB, 0, width, 0, Y); } // ---------------------------------------- // [ AC | B -- D ] // ---------------------------------------- else if (AC) { - hsplit = HSPLIT(X, 0, height); - vsplit = VSPLIT(X, Y, RY); - DISPLAY(layout, D, X + 1, RX, Y + 1, RY); - DISPLAY(layout, B, X + 1, RX, 0, Y); - DISPLAY(layout, AC, 0, X, 0, height); + hsplitTop = HSPLIT(XTop, 0, height); + vsplit = VSPLIT(XTop, Y, RY); + DISPLAY(layout, D, XTop + 1, RXTop, Y + 1, RY); + DISPLAY(layout, B, XTop + 1, RXTop, 0, Y); + DISPLAY(layout, AC, 0, XTop, 0, height); } // ---------------------------------------- // [ A -- C | BD ] // ---------------------------------------- else if (BD) { - hsplit = HSPLIT(X, 0, height); - vsplit = VSPLIT(0, Y, X); - DISPLAY(layout, C, 0, X, Y + 1, RY); - DISPLAY(layout, A, 0, X, 0, Y); - DISPLAY(layout, BD, X + 1, RX, 0, height); + hsplitTop = HSPLIT(XTop, 0, height); + vsplit = VSPLIT(0, Y, XTop); + DISPLAY(layout, C, 0, XTop, Y + 1, RY); + DISPLAY(layout, A, 0, XTop, 0, Y); + DISPLAY(layout, BD, XTop + 1, RXTop, 0, height); } // ---------------------------------------- // [ A | B -- CD ] // ---------------------------------------- else if (CD) { - hsplit = HSPLIT(X, 0, Y); + hsplitTop = HSPLIT(XTop, 0, Y); vsplit = VSPLIT(0, Y, width); - DISPLAY(layout, B, X + 1, RX, 0, Y); - DISPLAY(layout, A, 0, X, 0, Y); + DISPLAY(layout, B, XTop + 1, RXTop, 0, Y); + DISPLAY(layout, A, 0, XTop, 0, Y); DISPLAY(layout, CD, 0, width, Y + 1, RY); } // ---------------------------------------- // [ A, B, C, D ] // ---------------------------------------- else { - hsplit = HSPLIT(X, 0, height); + hsplitTop = HSPLIT(XTop, 0, Y); + hsplitBottom = HSPLIT(XBottom, Y + 1, height - Y); vsplit = VSPLIT(0, Y, width); - DISPLAY(layout, D, X + 1, RX, Y + 1, RY); - DISPLAY(layout, C, 0, X, Y + 1, RY); - DISPLAY(layout, B, X + 1, RX, 0, Y); - DISPLAY(layout, A, 0, X, 0, Y); + DISPLAY(layout, D, XBottom + 1, RXBottom, Y + 1, RY); + DISPLAY(layout, C, 0, XBottom, Y + 1, RY); + DISPLAY(layout, B, XTop + 1, RXTop, 0, Y); + DISPLAY(layout, A, 0, XTop, 0, Y); } // ---------------------------------------- - if (hsplit !== NODISPLAY && vsplit !== NODISPLAY) - hvsplit = { display: 'block', left: X, top: Y }; + if (hsplitTop !== NODISPLAY) + hvsplittop = { display: 'block', left: XTop, top: Y }; + + if (hsplitBottom !== NODISPLAY && vsplit !== NODISPLAY) + hvsplitbottom = { display: 'block', left: XBottom, top: Y }; + + /** CSplitter HVSPLIT should only be displayed if XTop === XBottom and if + * all or none of the splitters slide. + */ + const noDragging = Boolean(!(dragXTop || dragXBottom || dragY)); + const allDragging = Boolean(dragXTop && dragXBottom && dragY); + const csplitter = Boolean(!hsplitBottom || + (XTop === XBottom && (noDragging || allDragging)) + ); + // ---------------------------------------- // Rendering // ---------------------------------------- return ( <QSplitContext.Provider value={layout}> <BSplitter - key='HSPLIT' + key='HSPLITTOP' + hsplit={true} + dragging={dragXTop} + setDragging={setDragXTop} + setPosition={setXTop} + resetPosition={resetXTop} + style={hsplitTop} + /> + <BSplitter + key='HSPLITBOTTOM' hsplit={true} - dragging={dragX} - setDragging={setDragX} - setPosition={setX} - resetPosition={resetX} - style={hsplit} + dragging={dragXBottom} + setDragging={setDragXBottom} + setPosition={setXBottom} + resetPosition={resetXBottom} + style={hsplitBottom} /> <BSplitter key='VSPLIT' @@ -396,16 +466,43 @@ function QSplitEngine(props: QSplitEngineProps): JSX.Element { resetPosition={resetY} style={vsplit} /> - <CSplitter - key='HVSPLIT' - dragX={dragX} - dragY={dragY} - setDragX={setDragX} - setDragY={setDragY} - setPosition={setXY} - resetPosition={resetXY} - style={hvsplit} - /> + { csplitter ? + <CSplitter + key='HVSPLITOP' + dragX={dragXTop} + dragY={dragY} + setDragX={(v: Dragging) => { + setDragXTop(v); + setDragXBottom(v); + }} + setDragY={setDragY} + setPosition={setXY} + resetPosition={resetXY} + style={hvsplittop} + /> : + <> + <CSplitter + key='HVSPLITTOP' + dragX={dragXTop} + dragY={dragY} + setDragX={setDragXTop} + setDragY={setDragY} + setPosition={setXYTop} + resetPosition={resetXY} + style={hvsplittop} + /> + <CSplitter + key='HVSPLITBOTTOM' + dragX={dragXBottom} + dragY={dragY} + setDragX={setDragXBottom} + setDragY={setDragY} + setPosition={setXYBottom} + resetPosition={resetXY} + style={hvsplitbottom} + /> + </> + } {props.children} </QSplitContext.Provider> ); diff --git a/ivette/src/ivette/laboratory.tsx b/ivette/src/ivette/laboratory.tsx index 0f85c9c160a..433d9e7510b 100644 --- a/ivette/src/ivette/laboratory.tsx +++ b/ivette/src/ivette/laboratory.tsx @@ -48,7 +48,7 @@ import * as State from 'ivette/state'; type tabKey = string; type viewId = string; -export interface Split { H: number, V: number } +export interface Split { HTOP: number, HBOTTOM: number, V: number } export interface Layout { A: compId, B: compId, C: compId, D: compId } export interface TabViewState { @@ -71,7 +71,7 @@ export interface LabViewState { sideComp: compId; // from Sidebar selection } -const defaultSplit: Split = { H: 0.5, V: 0.5 }; +const defaultSplit: Split = { HTOP: 0.5, HBOTTOM: 0.5, V: 0.5 }; const defaultLayout: Layout = { A: '', B: '', C: '', D: '' }; const LAB = new States.GlobalState<LabViewState>({ @@ -118,7 +118,8 @@ const jLayout: Json.Decoder<Layout> = const jSplit: Json.Decoder<Split> = Json.jObject({ - H: Json.jRange(0, 1, 0.5), + HTOP: Json.jRange(0, 1, 0.5), + HBOTTOM: Json.jRange(0, 1, 0.5), V: Json.jRange(0, 1, 0.5), }); @@ -1005,12 +1006,13 @@ function Pane(props: PaneProps): JSX.Element | null { export function LabView(): JSX.Element { const [state] = States.useGlobalState(LAB); const setPosition = React.useCallback( - (H: number, V: number) => LAB.setValue({ ...state, split: { H, V } }), + (HTOP: number, HBOTTOM: number, V: number) => + LAB.setValue({ ...state, split: { HTOP, HBOTTOM, V } }), [state] ); const layout = state.stack[0] ?? defaultLayout; const { A, B, C, D } = layout; - const { H, V } = state.split; + const { HTOP, HBOTTOM, V } = state.split; const panels: JSX.Element[] = []; state.panels.forEach((id) => panels.push(<Pane key={id} compId={id} />)); return ( @@ -1019,7 +1021,7 @@ export function LabView(): JSX.Element { <Notifications /> <QSplit className='labview-container' - A={A} B={B} C={C} D={D} H={H} V={V} + A={A} B={B} C={C} D={D} HTOP={HTOP} HBOTTOM={HBOTTOM} V={V} setPosition={setPosition} >{panels}</QSplit> </> diff --git a/ivette/src/sandbox/qsplit.tsx b/ivette/src/sandbox/qsplit.tsx index 46918acf7d3..e8a63a15db8 100644 --- a/ivette/src/sandbox/qsplit.tsx +++ b/ivette/src/sandbox/qsplit.tsx @@ -66,25 +66,28 @@ function Pane(props: { id: string, background: string }): JSX.Element { const round = (r: number): number => Math.round(r * 100) / 100; function QSplitSandbox(): JSX.Element { - const [H, setH] = React.useState(0.5); + const [HTOP, setHTOP] = React.useState(0.5); + const [HBOTTOM, setHBOTTOM] = 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: number, v: number) => { - setH(h); - setV(v); - }, [setH, setV]); + const setPosition = React.useCallback( + (hTop: number, hBottom: number, v: number) => { + setHTOP(hTop); + setHBOTTOM(hBottom); + setV(v); + }, [setHTOP, setHBOTTOM, setV]); const reset = (): void => { - setPosition(0.5, 0.5); + setPosition(0.5, 0.5, 0.5); setA('A'); setB('B'); setC('C'); setD('D'); }; const clear = (): void => { - setPosition(0.5, 0.5); + setPosition(0.5, 0.5, 0.5); setA(undefined); setB(undefined); setC(undefined); @@ -96,14 +99,16 @@ function QSplitSandbox(): JSX.Element { <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> + <Disp.LCD> + HTOP={round(HTOP)} HBOTTOM={round(HBOTTOM)} 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} + <QSplit A={A} B={B} C={C} D={D} HTOP={HTOP} HBOTTOM={HBOTTOM} V={V} setPosition={setPosition}> <Pane id='A' background='lightblue' /> <Pane id='B' background='lightgreen' /> -- GitLab