Move tab bar to top edge (#72)

This commit is contained in:
Red J Adaya 2024-06-24 03:03:09 +08:00 committed by GitHub
parent 484d58b88d
commit 4714b88be7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 109 additions and 66 deletions

View File

@ -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);
}

View File

@ -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>

View 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;
}

View 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 };

View File

@ -53,6 +53,10 @@
background-color: var(--border-color);
}
.vertical-line.first {
left: 0;
}
.close {
visibility: hidden;
position: absolute;

View File

@ -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) {
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 })}

View File

@ -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;
}
}

View File

@ -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>
);
};