From b71db296d9a954f8577f4a452ecd8572af9daa8a Mon Sep 17 00:00:00 2001 From: Red Adaya Date: Tue, 26 Nov 2024 19:14:11 +0800 Subject: [PATCH] smooth resize tab resizing --- frontend/app/element/popover.tsx | 100 ++++++++++++++----------- frontend/app/element/windowdrag.tsx | 5 +- frontend/app/tab/tabbar.tsx | 72 +++++++++--------- frontend/app/tab/workspaceswitcher.tsx | 8 +- 4 files changed, 98 insertions(+), 87 deletions(-) diff --git a/frontend/app/element/popover.tsx b/frontend/app/element/popover.tsx index cac445981..4feb1e36a 100644 --- a/frontend/app/element/popover.tsx +++ b/frontend/app/element/popover.tsx @@ -48,54 +48,64 @@ const isPopoverContent = ( return element.type === PopoverContent; }; -const Popover = memo(({ children, className, placement = "bottom-start", offset = 3, onDismiss }: PopoverProps) => { - const [isOpen, setIsOpen] = useState(false); +const Popover = memo( + forwardRef( + ({ children, className, placement = "bottom-start", offset = 3, onDismiss }, ref) => { + const [isOpen, setIsOpen] = useState(false); - const handleOpenChange = (open: boolean) => { - setIsOpen(open); - if (!open && onDismiss) { - onDismiss(); + const handleOpenChange = (open: boolean) => { + setIsOpen(open); + if (!open && onDismiss) { + onDismiss(); + } + }; + + const { refs, floatingStyles, context } = useFloating({ + placement, + open: isOpen, + onOpenChange: handleOpenChange, + middleware: [offsetMiddleware(offset)], + whileElementsMounted: autoUpdate, + }); + + const click = useClick(context); + const dismiss = useDismiss(context); + const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]); + + const renderChildren = Children.map(children, (child) => { + if (isValidElement(child)) { + if (isPopoverButton(child)) { + return cloneElement(child as any, { + isActive: isOpen, + ref: refs.setReference, + getReferenceProps, + // Do not overwrite onClick + }); + } + + if (isPopoverContent(child)) { + return isOpen + ? cloneElement(child as any, { + ref: refs.setFloating, + style: floatingStyles, + getFloatingProps, + }) + : null; + } + } + return child; + }); + + return ( +
+ {renderChildren} +
+ ); } - }; + ) +); - const { refs, floatingStyles, context } = useFloating({ - placement, - open: isOpen, - onOpenChange: handleOpenChange, - middleware: [offsetMiddleware(offset)], - whileElementsMounted: autoUpdate, - }); - - const click = useClick(context); - const dismiss = useDismiss(context); - const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]); - - const renderChildren = Children.map(children, (child) => { - if (isValidElement(child)) { - if (isPopoverButton(child)) { - return cloneElement(child as any, { - isActive: isOpen, - ref: refs.setReference, - getReferenceProps, - // Do not overwrite onClick - }); - } - - if (isPopoverContent(child)) { - return isOpen - ? cloneElement(child as any, { - ref: refs.setFloating, - style: floatingStyles, - getFloatingProps, - }) - : null; - } - } - return child; - }); - - return
{renderChildren}
; -}); +Popover.displayName = "Popover"; interface PopoverButtonProps extends React.ButtonHTMLAttributes { isActive?: boolean; diff --git a/frontend/app/element/windowdrag.tsx b/frontend/app/element/windowdrag.tsx index a60d2dfba..472b30ab6 100644 --- a/frontend/app/element/windowdrag.tsx +++ b/frontend/app/element/windowdrag.tsx @@ -8,12 +8,13 @@ import "./windowdrag.scss"; interface WindowDragProps { className?: string; + style?: React.CSSProperties; children?: React.ReactNode; } -const WindowDrag = forwardRef(({ children, className }, ref) => { +const WindowDrag = forwardRef(({ children, className, style }, ref) => { return ( -
+
{children}
); diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index b55684e8d..665e87374 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -103,10 +103,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { const [dragStartPositions, setDragStartPositions] = useState([]); const [draggingTab, setDraggingTab] = useState(); const [tabsLoaded, setTabsLoaded] = useState({}); - // const [scrollable, setScrollable] = useState(false); - // const [tabWidth, setTabWidth] = useState(TAB_DEFAULT_WIDTH); const [newTabId, setNewTabId] = useState(null); - // const [hoveredTabId, setHoveredTabId] = useState(null); const tabbarWrapperRef = useRef(null); const tabBarRef = useRef(null); @@ -126,6 +123,9 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { const osInstanceRef = useRef(null); const draggerRightRef = useRef(null); const draggerLeftRef = useRef(null); + const workspaceSwitcherRef = useRef(null); + const devLabelRef = useRef(null); + const appMenuButtonRef = useRef(null); const tabWidthRef = useRef(TAB_DEFAULT_WIDTH); const scrollableRef = useRef(false); const updateStatusButtonRef = useRef(null); @@ -185,33 +185,31 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { const addBtnWidth = addBtnRef.current.getBoundingClientRect().width; const updateStatusLabelWidth = updateStatusButtonRef.current?.getBoundingClientRect().width ?? 0; const configErrorWidth = configErrorButtonRef.current?.getBoundingClientRect().width ?? 0; - const spaceForTabs = - tabbarWrapperWidth - - (windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth + configErrorWidth); + const appMenuButtonWidth = appMenuButtonRef.current?.getBoundingClientRect().width ?? 0; + const workspaceSwitcherWidth = workspaceSwitcherRef.current?.getBoundingClientRect().width ?? 0; + const devLabelWidth = devLabelRef.current?.getBoundingClientRect().width ?? 0; + + const nonTabElementsWidth = + windowDragLeftWidth + + DRAGGER_RIGHT_MIN_WIDTH + + addBtnWidth + + updateStatusLabelWidth + + configErrorWidth + + appMenuButtonWidth + + workspaceSwitcherWidth + + devLabelWidth; + const spaceForTabs = tabbarWrapperWidth - nonTabElementsWidth; const numberOfTabs = tabIds.length; - const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH; - const minTotalTabWidth = numberOfTabs * TAB_MIN_WIDTH; - const tabWidth = tabWidthRef.current; - const scrollable = scrollableRef.current; - let newTabWidth = tabWidth; - let newScrollable = scrollable; - if (spaceForTabs < totalDefaultTabWidth && spaceForTabs > minTotalTabWidth) { - newTabWidth = TAB_MIN_WIDTH; - } else if (minTotalTabWidth > spaceForTabs) { - // Case where tabs cannot shrink further, make the tab bar scrollable - newTabWidth = TAB_MIN_WIDTH; - newScrollable = true; - } else if (totalDefaultTabWidth > spaceForTabs) { - // Case where resizing is needed due to limited container width - newTabWidth = spaceForTabs / numberOfTabs; - newScrollable = false; - } else { - // Case where tabs were previously shrunk or there is enough space for default width tabs - newTabWidth = TAB_DEFAULT_WIDTH; - newScrollable = false; - } + // Compute the ideal width per tab by dividing the available space by the number of tabs + let idealTabWidth = spaceForTabs / numberOfTabs; + + // Apply min/max constraints + idealTabWidth = Math.max(TAB_MIN_WIDTH, Math.min(idealTabWidth, TAB_DEFAULT_WIDTH)); + + // Determine if the tab bar needs to be scrollable + const newScrollable = idealTabWidth * numberOfTabs > spaceForTabs; // Apply the calculated width and position to all tabs tabRefs.current.forEach((ref, index) => { @@ -221,20 +219,22 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { } else { ref.current.classList.remove("animate"); } - ref.current.style.width = `${newTabWidth}px`; - ref.current.style.transform = `translate3d(${index * newTabWidth}px,0,0)`; + ref.current.style.width = `${idealTabWidth}px`; + ref.current.style.transform = `translate3d(${index * idealTabWidth}px,0,0)`; ref.current.style.opacity = "1"; } }); // Update the state with the new tab width if it has changed - if (newTabWidth !== tabWidth) { - tabWidthRef.current = newTabWidth; + if (idealTabWidth !== tabWidthRef.current) { + tabWidthRef.current = idealTabWidth; } + // Update the state with the new scrollable state if it has changed - if (newScrollable !== scrollable) { + if (newScrollable !== scrollableRef.current) { scrollableRef.current = newScrollable; } + // Initialize/destroy overlay scrollbars if (newScrollable) { osInstanceRef.current = OverlayScrollbars(tabBarRef.current, { ...(OS_OPTIONS as any) }); @@ -530,13 +530,13 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { const tabsWrapperWidth = tabIds.length * tabWidthRef.current; const devLabel = isDev() ? ( -
+
) : undefined; const appMenuButton = PLATFORM !== "darwin" && !settings["window:showmenubar"] ? ( -
+
) : undefined; @@ -545,7 +545,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { {appMenuButton} {devLabel} - {isDev() ? : null} + {isDev() ? : null}
{tabIds.map((tabId, index) => { @@ -572,7 +572,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
- +
diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index 69e9d6090..3f4343f35 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -19,7 +19,7 @@ import clsx from "clsx"; import { colord } from "colord"; import { atom, useAtom } from "jotai"; import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; -import { memo, useEffect, useRef } from "react"; +import { forwardRef, memo, useEffect, useRef } from "react"; import WorkspaceSVG from "../asset/workspace.svg"; import "./workspaceswitcher.scss"; @@ -180,7 +180,7 @@ const workspaceData: WorkspaceDataType[] = [ export const menuDataAtom = atom(workspaceData); -const WorkspaceSwitcher = () => { +const WorkspaceSwitcher = forwardRef(({}, ref) => { const [menuData, setMenuData] = useAtom(menuDataAtom); const handleTitleChange = (id: string, newTitle: string) => { @@ -399,7 +399,7 @@ const WorkspaceSwitcher = () => { } return ( - + {workspaceIcon} {/* @@ -426,6 +426,6 @@ const WorkspaceSwitcher = () => { ); -}; +}); export { WorkspaceSwitcher };