diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 664ad2859..13dd61874 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -11,7 +11,7 @@ import { Input, } from "@/app/block/blockutil"; import { Button } from "@/app/element/button"; -import { useWidth } from "@/app/hook/useWidth"; +import { useDimensionsWithCallbackRef } from "@/app/hook/useDimensions"; import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { @@ -294,8 +294,8 @@ const ConnStatusOverlay = React.memo( const connName = blockData.meta?.connection; const connStatus = jotai.useAtomValue(getConnStatusAtom(connName)); const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom); - const overlayRef = React.useRef(null); - const width = useWidth(overlayRef); + const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30); + const width = domRect?.width; const [showError, setShowError] = React.useState(false); const blockNum = jotai.useAtomValue(nodeModel.blockNum); @@ -334,7 +334,7 @@ const ConnStatusOverlay = React.memo( } return ( -
+
{showIcon && } diff --git a/frontend/app/element/menu.tsx b/frontend/app/element/menu.tsx index d32260082..db6e77e11 100644 --- a/frontend/app/element/menu.tsx +++ b/frontend/app/element/menu.tsx @@ -1,12 +1,11 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { useHeight } from "@/app/hook/useHeight"; -import { useWidth } from "@/app/hook/useWidth"; import clsx from "clsx"; import React, { memo, useEffect, useLayoutEffect, useRef, useState } from "react"; import ReactDOM from "react-dom"; +import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; import "./menu.less"; type MenuItem = { @@ -143,9 +142,9 @@ const Menu = memo( const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); const menuRef = useRef(null); const subMenuRefs = useRef<{ [key: string]: React.RefObject }>({}); - - const width = useWidth(scopeRef); - const height = useHeight(scopeRef); + const domRect = useDimensionsWithExistingRef(scopeRef, 30); + const width = domRect?.width ?? 0; + const height = domRect?.height ?? 0; items.forEach((_, idx) => { const key = `${idx}`; diff --git a/frontend/app/hook/useDimensions.tsx b/frontend/app/hook/useDimensions.tsx index ac9f9086f..4e0b066c3 100644 --- a/frontend/app/hook/useDimensions.tsx +++ b/frontend/app/hook/useDimensions.tsx @@ -1,67 +1,99 @@ -import useResizeObserver from "@react-hook/resize-observer"; -import { useCallback, useRef, useState } from "react"; +import * as React from "react"; +import { useCallback, useState } from "react"; import { debounce } from "throttle-debounce"; -/** - * Get the current dimensions for the specified element, and whether it is currently changing size. Update when the element resizes. - * @param ref The reference to the element to observe. - * @param delay The debounce delay to use for updating the dimensions. - * @returns The dimensions of the element, and direction in which the dimensions are changing. - */ -const useDimensions = (ref: React.RefObject, delay = 0) => { - const [dimensions, setDimensions] = useState<{ - height: number | null; - width: number | null; - widthDirection?: string; - heightDirection?: string; - }>({ - height: null, - width: null, - }); - - const previousDimensions = useRef<{ height: number | null; width: number | null }>({ - height: null, - width: null, - }); - - const updateDimensions = useCallback((entry: ResizeObserverEntry) => { - const parentHeight = entry.contentRect.height; - const parentWidth = entry.contentRect.width; - - let widthDirection = ""; - let heightDirection = ""; - - if (previousDimensions.current.width !== null && previousDimensions.current.height !== null) { - if (parentWidth > previousDimensions.current.width) { - widthDirection = "expanding"; - } else if (parentWidth < previousDimensions.current.width) { - widthDirection = "shrinking"; - } else { - widthDirection = "unchanged"; - } - - if (parentHeight > previousDimensions.current.height) { - heightDirection = "expanding"; - } else if (parentHeight < previousDimensions.current.height) { - heightDirection = "shrinking"; - } else { - heightDirection = "unchanged"; - } - } - - previousDimensions.current = { height: parentHeight, width: parentWidth }; - - setDimensions({ height: parentHeight, width: parentWidth, widthDirection, heightDirection }); +// returns a callback ref, a ref object (that is set from the callback), and the width +// pass debounceMs of null to not debounce +export function useDimensionsWithCallbackRef( + debounceMs: number = null +): [(node: T) => void, React.RefObject, DOMRectReadOnly] { + const [domRect, setDomRect] = useState(null); + const [htmlElem, setHtmlElem] = useState(null); + const rszObjRef = React.useRef(null); + const oldHtmlElem = React.useRef(null); + const ref = React.useRef(null); + const refCallback = useCallback((node: T) => { + setHtmlElem(node); + ref.current = node; }, []); - - const fUpdateDimensions = useCallback(delay > 0 ? debounce(delay, updateDimensions) : updateDimensions, [ - updateDimensions, - delay, + const setDomRectDebounced = React.useCallback(debounceMs == null ? setDomRect : debounce(debounceMs, setDomRect), [ + debounceMs, + setDomRect, ]); + React.useEffect(() => { + if (!rszObjRef.current) { + rszObjRef.current = new ResizeObserver((entries) => { + for (const entry of entries) { + if (domRect == null) { + setDomRect(entry.contentRect); + } else { + setDomRectDebounced(entry.contentRect); + } + } + }); + } + if (htmlElem) { + rszObjRef.current.observe(htmlElem); + oldHtmlElem.current = htmlElem; + } + return () => { + if (oldHtmlElem.current) { + rszObjRef.current?.unobserve(oldHtmlElem.current); + oldHtmlElem.current = null; + } + }; + }, [htmlElem]); + React.useEffect(() => { + return () => { + rszObjRef.current?.disconnect(); + }; + }, []); + return [refCallback, ref, domRect]; +} - useResizeObserver(ref, fUpdateDimensions); - - return dimensions; -}; - -export { useDimensions }; +// will not react to ref changes +// pass debounceMs of null to not debounce +export function useDimensionsWithExistingRef( + ref: React.RefObject, + debounceMs: number = null +): DOMRectReadOnly { + const [domRect, setDomRect] = useState(null); + const rszObjRef = React.useRef(null); + const oldHtmlElem = React.useRef(null); + const setDomRectDebounced = React.useCallback(debounceMs == null ? setDomRect : debounce(debounceMs, setDomRect), [ + debounceMs, + setDomRect, + ]); + React.useEffect(() => { + if (!rszObjRef.current) { + rszObjRef.current = new ResizeObserver((entries) => { + for (const entry of entries) { + if (domRect == null) { + setDomRect(entry.contentRect); + } else { + setDomRectDebounced(entry.contentRect); + } + } + }); + } + if (ref.current) { + rszObjRef.current.observe(ref.current); + oldHtmlElem.current = ref.current; + } + return () => { + if (oldHtmlElem.current) { + rszObjRef.current?.unobserve(oldHtmlElem.current); + oldHtmlElem.current = null; + } + }; + }, [ref.current]); + React.useEffect(() => { + return () => { + rszObjRef.current?.disconnect(); + }; + }, []); + if (ref.current != null) { + return ref.current.getBoundingClientRect(); + } + return null; +} diff --git a/frontend/app/hook/useHeight.tsx b/frontend/app/hook/useHeight.tsx deleted file mode 100644 index e6ce41c31..000000000 --- a/frontend/app/hook/useHeight.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import useResizeObserver from "@react-hook/resize-observer"; -import { useCallback, useState } from "react"; -import { debounce } from "throttle-debounce"; - -/** - * Get the height of the specified element and update it when the element resizes. - * @param ref The reference to the element to observe. - * @param delay The debounce delay to use for updating the height. - * @returns The current height of the element, or null if the element is not yet mounted. - */ -const useHeight = (ref: React.RefObject, delay = 0) => { - const [height, setHeight] = useState(null); - - const updateHeight = useCallback((entry: ResizeObserverEntry) => { - setHeight(entry.contentRect.height); - }, []); - - const fUpdateHeight = useCallback(delay > 0 ? debounce(delay, updateHeight) : updateHeight, [updateHeight, delay]); - - useResizeObserver(ref, fUpdateHeight); - - return height; -}; - -export { useHeight }; diff --git a/frontend/app/hook/useWidth.tsx b/frontend/app/hook/useWidth.tsx deleted file mode 100644 index c7007d7f4..000000000 --- a/frontend/app/hook/useWidth.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import useResizeObserver from "@react-hook/resize-observer"; -import { useCallback, useState } from "react"; -import { debounce } from "throttle-debounce"; - -/** - * Get the width of the specified element and update it when the element resizes. - * @param ref The reference to the element to observe. - * @param delay The debounce delay to use for updating the width. - * @returns The current width of the element, or null if the element is not yet mounted. - */ -const useWidth = (ref: React.RefObject, delay = 0) => { - const [width, setWidth] = useState(null); - - const updateWidth = useCallback((entry: ResizeObserverEntry) => { - setWidth(entry.contentRect.width); - }, []); - - const fUpdateWidth = useCallback(delay > 0 ? debounce(delay, updateWidth) : updateWidth, [updateWidth, delay]); - - useResizeObserver(ref, fUpdateWidth); - - return width; -}; - -export { useWidth }; diff --git a/frontend/app/modals/typeaheadmodal.tsx b/frontend/app/modals/typeaheadmodal.tsx index 64f21c9dd..307fd919f 100644 --- a/frontend/app/modals/typeaheadmodal.tsx +++ b/frontend/app/modals/typeaheadmodal.tsx @@ -3,7 +3,7 @@ import { Input } from "@/app/element/input"; import { InputDecoration } from "@/app/element/inputdecoration"; -import { useDimensions } from "@/app/hook/useDimensions"; +import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; import { makeIconClass } from "@/util/util"; import clsx from "clsx"; import React, { forwardRef, useLayoutEffect, useRef } from "react"; @@ -99,7 +99,9 @@ const TypeAheadModal = ({ autoFocus, selectIndex, }: TypeAheadModalProps) => { - const { width, height } = useDimensions(blockRef); + const domRect = useDimensionsWithExistingRef(blockRef, 30); + const width = domRect?.width ?? 0; + const height = domRect?.height ?? 0; const modalRef = useRef(null); const inputRef = useRef(null); const realInputRef = useRef(null); diff --git a/frontend/app/view/cpuplot/cpuplot.tsx b/frontend/app/view/cpuplot/cpuplot.tsx index e769427fe..e773f9a3a 100644 --- a/frontend/app/view/cpuplot/cpuplot.tsx +++ b/frontend/app/view/cpuplot/cpuplot.tsx @@ -1,8 +1,6 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { useHeight } from "@/app/hook/useHeight"; -import { useWidth } from "@/app/hook/useWidth"; import { getConnStatusAtom, globalStore, WOS } from "@/store/global"; import * as util from "@/util/util"; import * as Plot from "@observablehq/plot"; @@ -11,6 +9,7 @@ import * as htl from "htl"; import * as jotai from "jotai"; import * as React from "react"; +import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { WindowRpcClient } from "@/app/store/wshrpcutil"; @@ -231,8 +230,9 @@ function CpuPlotView({ model, blockId }: CpuPlotViewProps) { const CpuPlotViewInner = React.memo(({ model }: CpuPlotViewProps) => { const containerRef = React.useRef(); const plotData = jotai.useAtomValue(model.dataAtom); - const parentHeight = useHeight(containerRef); - const parentWidth = useWidth(containerRef); + const domRect = useDimensionsWithExistingRef(containerRef, 30); + const parentHeight = domRect?.height ?? 0; + const parentWidth = domRect?.width ?? 0; const yvals = jotai.useAtomValue(model.metrics); React.useEffect(() => { diff --git a/frontend/app/view/preview/csvview.tsx b/frontend/app/view/preview/csvview.tsx index 3a72d67b1..2079ce8d7 100644 --- a/frontend/app/view/preview/csvview.tsx +++ b/frontend/app/view/preview/csvview.tsx @@ -1,7 +1,6 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { useHeight } from "@/app/hook/useHeight"; import { useTableNav } from "@table-nav/react"; import { createColumnHelper, @@ -14,6 +13,7 @@ import { clsx } from "clsx"; import Papa from "papaparse"; import { useEffect, useMemo, useRef, useState } from "react"; +import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; import "./csvview.less"; const MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB in bytes @@ -54,7 +54,8 @@ const CSVView = ({ parentRef, filename, content }: CSVViewProps) => { const [tableLoaded, setTableLoaded] = useState(false); const { listeners } = useTableNav(); - const parentHeight = useHeight(parentRef); + const domRect = useDimensionsWithExistingRef(parentRef, 30); + const parentHeight = domRect?.height ?? 0; const cacheKey = `${filename}`; csvCacheRef.current.set(cacheKey, content); diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index a0c7fc1ff..73c8a098b 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -1,7 +1,6 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { useHeight } from "@/app/hook/useHeight"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, createBlock, getApi } from "@/app/store/global"; import type { PreviewModel } from "@/app/view/preview/preview"; @@ -27,6 +26,7 @@ import { quote as shellQuote } from "shell-quote"; import { OverlayScrollbars } from "overlayscrollbars"; +import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; import "./directorypreview.less"; interface DirectoryTableProps { @@ -352,8 +352,8 @@ function TableBody({ const warningBoxRef = useRef(null); const osInstanceRef = useRef(null); const rowRefs = useRef([]); - - const parentHeight = useHeight(parentRef); + const domRect = useDimensionsWithExistingRef(parentRef, 30); + const parentHeight = domRect?.height ?? 0; const conn = jotai.useAtomValue(model.connection); useEffect(() => { @@ -363,7 +363,6 @@ function TableBody({ const warningBoxHeight = warningBoxRef.current?.offsetHeight ?? 0; const maxHeightLessHeader = parentHeight - warningBoxHeight; const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight); - setBodyHeight(tbodyHeight); } }, [data, parentHeight]); diff --git a/frontend/app/view/waveai/waveai.less b/frontend/app/view/waveai/waveai.less index 6e55a6594..93d5911e7 100644 --- a/frontend/app/view/waveai/waveai.less +++ b/frontend/app/view/waveai/waveai.less @@ -55,6 +55,7 @@ background-color: rgb(from var(--highlight-bg-color) r g b / 0.1); margin-right: auto; padding: 10px; + max-width: 85%; .markdown { width: 100%; @@ -71,6 +72,7 @@ &.chat-msg-user { margin-left: auto; padding: 10px; + max-width: 85%; background-color: rgb(from var(--accent-color) r g b / 0.15); } diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 384dba4c9..bfff55358 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -4,7 +4,6 @@ import { Button } from "@/app/element/button"; import { Markdown } from "@/app/element/markdown"; import { TypingIndicator } from "@/app/element/typingindicator"; -import { useDimensions } from "@/app/hook/useDimensions"; import { RpcApi } from "@/app/store/wshclientapi"; import { WindowRpcClient } from "@/app/store/wshrpcutil"; import { atoms, fetchWaveFile, globalStore, WOS } from "@/store/global"; @@ -260,10 +259,7 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
-
+
@@ -278,7 +274,7 @@ const ChatItem = ({ chatItem }: ChatItemProps) => { } return ( <> -
+
@@ -439,10 +435,8 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => { const [selectedBlockIdx, setSelectedBlockIdx] = useState(null); const termFontSize: number = 14; - const windowDims = useDimensions(chatWindowRef); const msgWidths = {}; const locked = useAtomValue(model.locked); - msgWidths["--aichat-msg-width"] = windowDims.width * 0.85; // a weird workaround to initialize ansynchronously useEffect(() => {