mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Move tab bar to top edge (#72)
This commit is contained in:
parent
484d58b88d
commit
4714b88be7
@ -51,13 +51,6 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 35px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.error-boundary {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
@ -192,7 +192,6 @@ const AppInner = () => {
|
||||
if (client == null || windowData == null) {
|
||||
return (
|
||||
<div className="mainapp">
|
||||
<div className="titlebar"></div>
|
||||
<CenteredDiv>invalid configuration, client or window was not loaded</CenteredDiv>
|
||||
</div>
|
||||
);
|
||||
@ -243,7 +242,6 @@ const AppInner = () => {
|
||||
return (
|
||||
<div className="mainapp" onContextMenu={handleContextMenu}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="titlebar"></div>
|
||||
<Workspace />
|
||||
</DndProvider>
|
||||
</div>
|
||||
|
7
frontend/app/element/windowdrag.less
Normal file
7
frontend/app/element/windowdrag.less
Normal file
@ -0,0 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.window-drag {
|
||||
-webkit-app-region: drag;
|
||||
z-index: 100;
|
||||
}
|
22
frontend/app/element/windowdrag.tsx
Normal file
22
frontend/app/element/windowdrag.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
import "./windowdrag.less";
|
||||
|
||||
interface WindowDragProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const WindowDrag = forwardRef<HTMLDivElement, WindowDragProps>(({ children, className }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={clsx(`window-drag`, className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export { WindowDrag };
|
@ -53,6 +53,10 @@
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.vertical-line.first {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
|
@ -1,5 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Button } from "@/element/button";
|
||||
import { ContextMenuModel } from "@/store/contextmenu";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import { clsx } from "clsx";
|
||||
@ -10,16 +12,17 @@ import "./tab.less";
|
||||
interface TabProps {
|
||||
id: string;
|
||||
active: boolean;
|
||||
isFirst: boolean;
|
||||
isBeforeActive: boolean;
|
||||
isDragging: boolean;
|
||||
onSelect: () => void;
|
||||
onClose: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onLoaded: () => void;
|
||||
}
|
||||
|
||||
const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
({ id, active, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
|
||||
({ id, active, isFirst, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
|
||||
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
|
||||
const [originalName, setOriginalName] = useState("");
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
@ -42,10 +45,8 @@ const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDoubleClick = (event?: React.MouseEvent<any, any>) => {
|
||||
if (event != null) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
const handleDoubleClick = (event) => {
|
||||
event.stopPropagation();
|
||||
setIsEditable(true);
|
||||
editableTimeoutRef.current = setTimeout(() => {
|
||||
if (editableRef.current) {
|
||||
@ -105,35 +106,15 @@ const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
function handleContextMenu(e: React.MouseEvent<HTMLElement>) {
|
||||
let menu: ContextMenuItem[] = [];
|
||||
menu.push({
|
||||
label: "Edit Name",
|
||||
click: () => {
|
||||
handleDoubleClick(null);
|
||||
},
|
||||
});
|
||||
menu.push({
|
||||
type: "separator",
|
||||
});
|
||||
menu.push({
|
||||
label: "Close",
|
||||
click: () => {
|
||||
onClose(e);
|
||||
},
|
||||
});
|
||||
ContextMenuModel.showContextMenu(menu, e);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx("tab", { active, isDragging, "before-active": isBeforeActive })}
|
||||
onMouseDown={onDragStart}
|
||||
onClick={onSelect}
|
||||
onContextMenu={handleContextMenu}
|
||||
data-tab-id={id}
|
||||
>
|
||||
{isFirst && <div className="vertical-line first" />}
|
||||
<div
|
||||
ref={editableRef}
|
||||
className={clsx("name", { focused: isEditable })}
|
||||
|
@ -4,21 +4,25 @@
|
||||
.tab-bar-wrapper {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
|
||||
.tab-bar {
|
||||
position: relative; // Needed for absolute positioning of child tabs
|
||||
min-height: 34px; // Adjust as necessary to fit the height of tabs
|
||||
width: calc(100vw - 36px); // 36 is the width of add tab button
|
||||
margin-left: 100px;
|
||||
height: 34px;
|
||||
// 36 is the width of add tab button
|
||||
// 100 is offset from the left, for macOS window controls and dragging
|
||||
// 50 right offset for dragging
|
||||
// minus 1px for last tab right border
|
||||
width: calc(100vw - 185px);
|
||||
}
|
||||
|
||||
.add-tab-btn {
|
||||
width: 36px;
|
||||
height: 34px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%); // overridden in js
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
@ -28,15 +32,24 @@
|
||||
background-color: var(--main-bg-color);
|
||||
}
|
||||
|
||||
.window-drag {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
|
||||
&.left {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
// Customize scrollbar styles
|
||||
.os-theme-dark,
|
||||
.os-theme-light {
|
||||
box-sizing: border-box;
|
||||
--os-size: 3px;
|
||||
--os-size: 2px;
|
||||
--os-padding-perpendicular: 0px;
|
||||
--os-padding-axis: 0px;
|
||||
--os-track-border-radius: 3px;
|
||||
--os-track-border-radius: 2px;
|
||||
--os-handle-interactive-area-offset: 0px;
|
||||
--os-handle-border-radius: 3px;
|
||||
--os-handle-border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WindowDrag } from "@/element/windowdrag";
|
||||
import { deleteLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
||||
import { debounce } from "@/faraday/lib/utils";
|
||||
import { atoms } from "@/store/global";
|
||||
@ -41,8 +42,8 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
||||
const [draggingTab, setDraggingTab] = useState<string>();
|
||||
const [tabsLoaded, setTabsLoaded] = useState({});
|
||||
const [scrollable, setScrollable] = useState(false);
|
||||
const [tabWidth, setTabWidth] = useState(TAB_DEFAULT_WIDTH);
|
||||
// const [scrollable, setScrollable] = useState(false);
|
||||
// const [tabWidth, setTabWidth] = useState(TAB_DEFAULT_WIDTH);
|
||||
|
||||
const tabBarRef = useRef<HTMLDivElement>(null);
|
||||
const tabsWrapperRef = useRef<HTMLDivElement>(null);
|
||||
@ -61,6 +62,9 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
dragged: false,
|
||||
});
|
||||
const osInstanceRef = useRef<OverlayScrollbars>(null);
|
||||
const draggerRightRef = useRef<HTMLDivElement>(null);
|
||||
const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH);
|
||||
const scrollableRef = useRef<boolean>(false);
|
||||
|
||||
const windowData = useAtomValue(atoms.waveWindow);
|
||||
const { activetabid } = windowData;
|
||||
@ -105,18 +109,21 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
setDragStartPositions(newStartPositions);
|
||||
}, []);
|
||||
|
||||
const debouncedSetTabWidth = debounce((width) => setTabWidth(width), 100);
|
||||
const debouncedSetScrollable = debounce((scrollable) => setScrollable(scrollable), 100);
|
||||
// const debouncedSetTabWidth = debounce((width) => setTabWidth(width), 100);
|
||||
// const debouncedSetScrollable = debounce((scrollable) => setScrollable(scrollable), 100);
|
||||
const debouncedUpdateTabPositions = debounce(() => updateTabPositions(), 100);
|
||||
|
||||
const handleResizeTabs = useCallback(() => {
|
||||
const tabBar = tabBarRef.current;
|
||||
if (tabBar === null) return;
|
||||
|
||||
const tabBarWidth = tabBar.getBoundingClientRect().width;
|
||||
const tabBarRect = tabBar.getBoundingClientRect();
|
||||
const tabBarWidth = tabBarRect.width;
|
||||
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;
|
||||
|
||||
@ -144,11 +151,11 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
|
||||
// Update the state with the new tab width if it has changed
|
||||
if (newTabWidth !== tabWidth) {
|
||||
debouncedSetTabWidth(newTabWidth);
|
||||
tabWidthRef.current = newTabWidth;
|
||||
}
|
||||
// Update the state with the new scrollable state if it has changed
|
||||
if (newScrollable !== scrollable) {
|
||||
debouncedSetScrollable(newScrollable);
|
||||
scrollableRef.current = newScrollable;
|
||||
}
|
||||
// Initialize/destroy overlay scrollbars
|
||||
if (newScrollable) {
|
||||
@ -159,21 +166,29 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the position of the Add Tab button if needed
|
||||
// Update Add Tab button position if needed
|
||||
const addButton = addBtnRef.current;
|
||||
const lastTabRef = tabRefs.current[tabRefs.current.length - 1];
|
||||
if (addButton && lastTabRef && lastTabRef.current) {
|
||||
const lastTabRect = lastTabRef.current.getBoundingClientRect();
|
||||
addButton.style.position = "absolute";
|
||||
if (newScrollable) {
|
||||
addButton.style.transform = `translateX(${document.documentElement.clientWidth - addButton.offsetWidth}px) translateY(-50%)`;
|
||||
addButton.style.transform = `translateX(${tabBarRect.left + tabBarWidth + 1}px)`;
|
||||
} else {
|
||||
addButton.style.transform = `translateX(${lastTabRect.right + 1}px) translateY(-50%)`;
|
||||
addButton.style.transform = `translateX(${lastTabRect.right + 1}px)`;
|
||||
}
|
||||
}
|
||||
// Update dragger right position if needed
|
||||
const draggerRight = draggerRightRef.current;
|
||||
if (draggerRight && addButton) {
|
||||
const addButtonRect = addButton.getBoundingClientRect();
|
||||
const targetPos = addButtonRect.left + addButtonRect.width;
|
||||
draggerRight.style.transform = `translateX(${targetPos}px)`;
|
||||
draggerRight.style.width = `${document.documentElement.offsetWidth - targetPos}px`;
|
||||
}
|
||||
|
||||
debouncedUpdateTabPositions();
|
||||
}, [tabIds, tabWidth, scrollable]);
|
||||
}, [tabIds]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", () => handleResizeTabs());
|
||||
@ -219,6 +234,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
|
||||
const getNewTabIndex = (currentX: number, tabIndex: number, dragDirection: string) => {
|
||||
let newTabIndex = tabIndex;
|
||||
const tabWidth = tabWidthRef.current;
|
||||
if (dragDirection === "+") {
|
||||
// Dragging to the right
|
||||
for (let i = tabIndex + 1; i < tabIds.length; i++) {
|
||||
@ -250,21 +266,26 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
}
|
||||
let currentX = event.clientX - initialOffsetX - totalScrollOffset;
|
||||
let tabBarRectWidth = tabBarRef.current.getBoundingClientRect().width;
|
||||
// for macos, it's offset to make space for the window buttons
|
||||
const tabBarRectLeftOffset = tabBarRef.current.getBoundingClientRect().left;
|
||||
const incrementDecrement = tabBarRectLeftOffset * 0.05;
|
||||
const dragDirection = getDragDirection(currentX);
|
||||
const scrollable = scrollableRef.current;
|
||||
const tabWidth = tabWidthRef.current;
|
||||
|
||||
// Scroll the tab bar if the dragged tab overflows the container bounds
|
||||
if (scrollable) {
|
||||
const { viewport } = osInstanceRef.current.elements();
|
||||
const currentScrollLeft = viewport.scrollLeft;
|
||||
|
||||
if (event.clientX <= 0) {
|
||||
viewport.scrollLeft = Math.max(0, currentScrollLeft - 5); // Scroll left
|
||||
if (event.clientX <= tabBarRectLeftOffset) {
|
||||
viewport.scrollLeft = Math.max(0, currentScrollLeft - incrementDecrement); // Scroll left
|
||||
if (viewport.scrollLeft !== currentScrollLeft) {
|
||||
// Only adjust if the scroll actually changed
|
||||
draggingTabDataRef.current.totalScrollOffset += currentScrollLeft - viewport.scrollLeft;
|
||||
}
|
||||
} else if (event.clientX >= tabBarRectWidth) {
|
||||
viewport.scrollLeft = Math.min(viewport.scrollWidth, currentScrollLeft + 5); // Scroll right
|
||||
} else if (event.clientX >= tabBarRectWidth + tabBarRectLeftOffset) {
|
||||
viewport.scrollLeft = Math.min(viewport.scrollWidth, currentScrollLeft + incrementDecrement); // Scroll right
|
||||
if (viewport.scrollLeft !== currentScrollLeft) {
|
||||
// Only adjust if the scroll actually changed
|
||||
draggingTabDataRef.current.totalScrollOffset -= viewport.scrollLeft - currentScrollLeft;
|
||||
@ -343,6 +364,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
|
||||
// Update the final position of the dragged tab
|
||||
const draggingTab = tabIds[tabIndex];
|
||||
const tabWidth = tabWidthRef.current;
|
||||
const finalLeftPosition = tabIndex * tabWidth;
|
||||
const ref = tabRefs.current.find((ref) => ref.current.dataset.tabId === draggingTab);
|
||||
if (ref.current) {
|
||||
@ -418,14 +440,14 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
services.ObjectService.AddTabToWorkspace(newTabName, true);
|
||||
|
||||
scrollToNewTabTimeoutIdRef.current = setTimeout(() => {
|
||||
if (scrollable) {
|
||||
if (scrollableRef.current) {
|
||||
const { viewport } = osInstanceRef.current.elements();
|
||||
viewport.scrollLeft = tabIds.length * tabWidth;
|
||||
viewport.scrollLeft = tabIds.length * tabWidthRef.current;
|
||||
}
|
||||
}, 30);
|
||||
};
|
||||
|
||||
const handleCloseTab = (event: React.MouseEvent<HTMLElement, MouseEvent>, tabId: string) => {
|
||||
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, tabId: string) => {
|
||||
event.stopPropagation();
|
||||
services.WindowService.CloseTab(tabId);
|
||||
deleteLayoutStateAtomForTab(tabId);
|
||||
@ -445,10 +467,11 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
return tabIds.indexOf(tabId) === tabIds.indexOf(activetabid) - 1;
|
||||
};
|
||||
|
||||
const tabsWrapperWidth = tabIds.length * tabWidth;
|
||||
const tabsWrapperWidth = tabIds.length * tabWidthRef.current;
|
||||
|
||||
return (
|
||||
<div className="tab-bar-wrapper">
|
||||
<WindowDrag className="left" />
|
||||
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
|
||||
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: tabsWrapperWidth }}>
|
||||
{tabIds.map((tabId, index) => (
|
||||
@ -456,6 +479,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
key={tabId}
|
||||
ref={tabRefs.current[index]}
|
||||
id={tabId}
|
||||
isFirst={index === 0}
|
||||
onSelect={() => handleSelectTab(tabId)}
|
||||
active={activetabid === tabId}
|
||||
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
|
||||
@ -470,6 +494,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
<div ref={addBtnRef} className="add-tab-btn" onClick={handleAddTab}>
|
||||
<i className="fa fa-solid fa-plus fa-fw" />
|
||||
</div>
|
||||
<WindowDrag ref={draggerRightRef} className="right" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
2
frontend/types/gotypes.d.ts
vendored
2
frontend/types/gotypes.d.ts
vendored
@ -352,4 +352,4 @@ declare global {
|
||||
|
||||
}
|
||||
|
||||
export {}
|
||||
export {}
|
Loading…
Reference in New Issue
Block a user