Compare commits

...

5 Commits

Author SHA1 Message Date
Red Adaya c44d11cf54 wip 2024-03-26 06:32:53 +08:00
Red Adaya 6dec479133 Merge branch 'main' of github.com:wavetermdev/waveterm into red/new-tabs 2024-03-25 18:38:31 +08:00
Red Adaya 5ae6a32222 Merge branch 'main' of github.com:wavetermdev/waveterm into red/new-tabs 2024-03-19 14:23:04 +08:00
Red Adaya ef27b074db integrate new tabs 2024-03-18 16:06:12 +08:00
Red Adaya 45244a1fb9 init 2024-03-17 22:05:29 +08:00
6 changed files with 553 additions and 2 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ import * as appconst from "@/app/appconst";
import * as textmeasure from "@/util/textmeasure";
import "./screenview.less";
import "./tabs.less";
// import "./tabs.less";
import { MagicLayout } from "../../magiclayout";
dayjs.extend(localizedFormat);
+100
View File
@@ -0,0 +1,100 @@
.screen-tab {
font: var(--base-font);
font-size: var(--screentabs-font-size);
line-height: var(--screentabs-line-height);
border-top: 2px solid transparent;
background: var(--app-bg-color);
.background {
// This applies a transparency mask to the background color, as set above, so that it will blend with whatever the theme's background color is.
z-index: 1;
width: var(--screen-tab-width);
mask-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 100%);
}
&.is-active {
opacity: 1;
font-weight: var(--screentabs-selected-font-weight);
border-top: 2px solid var(--tab-color);
}
&.is-archived {
.fa.fa-archive {
margin-right: 4px;
}
}
svg.svg-icon-inner path {
fill: var(--tab-color);
}
.tabicon i {
color: var(--tab-color);
}
&.color-green,
&.color-default {
--tab-color: var(--tab-green);
}
&.color-orange {
--tab-color: var(--tab-orange);
}
&.color-red {
--tab-color: var(--tab-red);
}
&.color-yellow {
--tab-color: var(--tab-yellow);
}
&.color-blue {
--tab-color: var(--tab-blue);
}
&.color-mint {
--tab-color: var(--tab-mint);
}
&.color-cyan {
--tab-color: var(--tab-cyan);
}
&.color-white {
--tab-color: var(--tab-white);
}
&.color-violet {
--tab-color: var(--tab-violet);
}
&.color-pink {
--tab-color: var(--tab-pink);
}
.screen-tab-inner {
display: flex;
flex-direction: row;
position: absolute;
z-index: 2;
min-width: var(--screen-tab-width);
max-width: var(--screen-tab-width);
align-items: center;
cursor: pointer;
padding: 8px 8px 4px 8px; // extra 4px of tab padding to account for horizontal scrollbar (to make tab text look centered)
.front-icon {
.positional-icon-visible;
}
.tab-name {
flex-grow: 1;
}
// Only one of these will be visible at a time
.end-icons {
// This adjusts the position of the icon to account for the default 8px margin on the parent. We want the positional calculations for this icon to assume it is flush with the edge of the screen tab.
margin: 0 -5px 0 0;
line-height: normal;
.tab-index {
font-size: 12.5px;
}
}
}
.vertical-line {
border-left: 1px solid var(--app-border-color);
margin: 10px 0 8px 0;
}
&:not(:hover) .status-indicator {
.status-indicator-visible;
}
&:hover {
.actions {
.positional-icon-visible;
}
}
}
+71
View File
@@ -0,0 +1,71 @@
import React, { useRef } from "react";
import cn from "classnames";
import { ActionsIcon, StatusIndicator, CenteredIcon } from "@/common/icons/icons";
import { TabIcon } from "@/elements/tabicon";
import { GlobalModel, Screen } from "@/models";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import * as constants from "@/app/appconst";
import "./tab2.less";
type ScreenTabProps = {
screen: Screen;
activeScreenId: string;
onDragStart: (name: string, ref: React.RefObject<HTMLDivElement>) => void;
onSwitchScreen: (screenId: string) => void;
};
const ScreenTab: React.FC<ScreenTabProps> = mobxReact.observer(
({ screen, activeScreenId, onSwitchScreen, onDragStart }) => {
const ref = useRef<HTMLDivElement>(null);
const openScreenSettings = (e: any, screen: Screen): void => {
e.preventDefault();
e.stopPropagation();
mobx.action(() => {
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
})();
GlobalModel.modalsModel.pushModal(constants.SCREEN_SETTINGS);
};
const archived = screen.archived.get() ? (
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
) : null;
const statusIndicatorLevel = screen.statusIndicator.get();
const runningCommands = screen.numRunningCmds.get() > 0;
return (
<div
ref={ref}
data-screenid={screen.screenId}
className={cn(
"screen-tab",
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
"color-" + screen.getTabColor()
)}
onMouseDown={() => onDragStart(screen.name.get(), ref)}
onClick={() => onSwitchScreen(screen.screenId)}
data-screentab-name={screen.name.get()}
>
<div className="background"></div>
<div className="screen-tab-inner">
<CenteredIcon className="front-icon">
<TabIcon icon={screen.getTabIcon()} color={screen.getTabColor()} />
</CenteredIcon>
<div className="tab-name truncate">
{archived}
{screen.name.get()}
</div>
<div className="end-icons">
<StatusIndicator level={statusIndicatorLevel} runningCommands={runningCommands} />
<ActionsIcon onClick={(e) => openScreenSettings(e, screen)} />
</div>
</div>
<div className="vertical-line"></div>
</div>
);
}
);
export { ScreenTab };
+45
View File
@@ -0,0 +1,45 @@
.screen-tabs-container {
position: relative;
height: var(--screentabs-height);
.screen-tabs-container-inner {
position: relative; // Needed for absolute positioning of child tabs
white-space: nowrap;
height: 100%;
margin-right: 42px;
overflow: hidden;
}
.new-screen {
width: 42px;
height: 40px;
background-color: var(--app-bg-color);
cursor: pointer;
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
text-align: center;
line-height: 38px;
user-select: none;
}
}
// This ensures the tab bar does not collide with the floating logo. The floating logo sits above the sidebar when it is not collapsed, so no additional margin is needed in that case.
// More margin is given on macOS to account for the traffic light buttons
#main.platform-darwin.mainsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-logo-width-darwin);
}
#main:not(.platform-darwin).mainsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-logo-width);
}
// This ensures the tab bar does not collide with the right sidebar triggers.
#main.platform-darwin.rightsidebar-collapsed .screen-tabs-container {
margin-right: var(--floating-right-sidebar-triggers-width-darwin);
}
#main:not(.platform-darwin).rightsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-right-sidebar-triggers-width);
}
+335
View File
@@ -0,0 +1,335 @@
import React, { useState, useCallback, useRef, useEffect } from "react";
import { reaction } from "mobx";
import { ScreenTab } from "./tab2";
import { observer, useLocalObservable } from "mobx-react";
import { For } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "@/models";
import AddIcon from "@/assets/icons/add.svg";
import "./tabs2.less";
const DEFAULT_TAB_WIDTH = 170;
type ScreenTabsProps = {
session: Session;
};
const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
const [screens, setScreens] = useState<Screen[]>([]);
const [tabWidth, setTabWidth] = useState(DEFAULT_TAB_WIDTH);
const [_, setDraggedTab] = useState<string | null>(null);
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
const tabContainerRef = useRef<HTMLDivElement>(null);
const addBtnRef = useRef<HTMLDivElement>(null);
const mainSidebarWidth = GlobalModel.mainSidebarModel.getWidth();
const rightSidebarWidth = GlobalModel.rightSidebarModel.getWidth();
let prevDelta: number;
let prevDragDirection: string;
let draggedRemoved: boolean;
let shrunk: boolean;
const store = useLocalObservable(() => ({
get activeScreenId() {
return session?.activeScreenId.get();
},
get screens() {
let activeScreenId = store.activeScreenId;
if (!activeScreenId) {
return [];
}
let screens = GlobalModel.getSessionScreens(session.sessionId);
let filteredScreens = screens.filter(
(screen) => !screen.archived.get() || activeScreenId === screen.screenId
);
filteredScreens.sort((a, b) => a.screenIdx.get() - b.screenIdx.get());
return filteredScreens;
},
}));
useEffect(() => {
// Update tabs when screens change
const dispose = reaction(
() => store.screens,
(screens) => {
setScreens(screens);
}
);
// Clean up
return () => {
if (dispose) dispose();
};
}, [screens.length]);
const getActiveScreenId = (): string | null => {
if (session) {
return session.activeScreenId.get();
}
return null;
};
const updateTabPositions = useCallback(() => {
if (tabContainerRef.current) {
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
let newStartPositions = [];
let cumulativeLeft = 0; // Start from the left edge
tabElements.forEach((tab) => {
newStartPositions.push(cumulativeLeft);
cumulativeLeft += tab.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position
});
setDragStartPositions(newStartPositions);
}
}, [screens]);
useEffect(() => {
updateTabPositions();
}, [screens, updateTabPositions]);
const resizeTabs = useCallback(() => {
if (tabContainerRef.current) {
const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
const numberOfTabs = screens.length;
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
if (totalDefaultTabWidth > containerWidth) {
// Case where resizing is needed due to limited container width
shrunk = true;
const newTabWidth = containerWidth / numberOfTabs;
setTabWidth(newTabWidth);
screens.forEach((screen, index) => {
const tabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${screen.name.get()}"]`
) as HTMLElement;
tabElement.style.width = `${newTabWidth}px`;
tabElement.style.left = `${index * newTabWidth}px`;
});
} else if (shrunk || totalDefaultTabWidth < containerWidth) {
// Case where tabs were previously shrunk or there is enough space for default width tabs
shrunk = false;
setTabWidth(DEFAULT_TAB_WIDTH);
screens.forEach((screen, index) => {
const tabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${screen.name.get()}"]`
) as HTMLElement;
tabElement.style.width = `${DEFAULT_TAB_WIDTH}px`;
tabElement.style.left = `${index * DEFAULT_TAB_WIDTH}px`;
});
}
// Update the position of the Add Tab button
const addButtonElement = addBtnRef.current;
if (addButtonElement && tabContainerRef.current) {
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
const lastTab = tabElements[tabElements.length - 1];
if (lastTab) {
const lastTabRect = lastTab.getBoundingClientRect();
const containerRect = tabContainerRef.current.getBoundingClientRect();
// Calculate the left position relative to the tab container
addButtonElement.style.left = `${lastTabRect.right - containerRect.left}px`;
}
}
updateTabPositions();
}
}, [screens.length, updateTabPositions]);
// Resize tabs when the number of tabs or the window size changes
useEffect(() => {
resizeTabs();
window.addEventListener("resize", resizeTabs);
return () => {
window.removeEventListener("resize", resizeTabs);
};
}, [resizeTabs]);
useEffect(() => {
resizeTabs();
}, [mainSidebarWidth, rightSidebarWidth]);
const onDragStart = useCallback(
(screenId: string, ref: React.RefObject<HTMLDivElement>) => {
setDraggedTab(screenId);
let tabIndex = screens.findIndex((screen) => screen.screenId === screenId);
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
if (ref.current) {
let initialOffsetX: number | null = null;
const handleMouseMove = (event: MouseEvent) => {
if (initialOffsetX === null) {
initialOffsetX = event.clientX - tabStartX;
}
let currentX = event.clientX - initialOffsetX;
// Constrain movement within the container bounds
if (tabContainerRef.current) {
const numberOfTabs = screens.length;
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
const containerRect = tabContainerRef.current.getBoundingClientRect();
let containerRectWidth = containerRect.width;
// Set to the total default tab width if there's vacant space
if (totalDefaultTabWidth < containerRectWidth) {
containerRectWidth = totalDefaultTabWidth;
}
const minLeft = 0;
const maxRight = containerRectWidth - tabWidth;
// Adjust currentX to stay within bounds
currentX = Math.min(Math.max(currentX, minLeft), maxRight);
}
ref.current.style.transform = `translateX(${currentX - tabStartX}px)`;
ref.current.style.zIndex = "100";
let dragDirection;
if (currentX - prevDelta > 0) {
dragDirection = "+";
} else if (currentX - prevDelta === 0) {
dragDirection = prevDragDirection;
} else {
dragDirection = "-";
}
prevDelta = currentX;
prevDragDirection = dragDirection;
let newTabIndex = tabIndex;
if (dragDirection === "+") {
// Dragging to the right
for (let i = tabIndex + 1; i < screens.length; i++) {
const otherTabStart = dragStartPositions[i];
if (currentX + tabWidth > otherTabStart + tabWidth / 2) {
newTabIndex = i;
}
}
} else {
// Dragging to the left
for (let i = tabIndex - 1; i >= 0; i--) {
const otherTabEnd = dragStartPositions[i] + tabWidth;
if (currentX < otherTabEnd - tabWidth / 2) {
newTabIndex = i;
}
}
}
// Rearrange the tabs temporarily
if (newTabIndex !== tabIndex) {
const tempTabs = Array.from(screens);
// Remove the dragged tab if not already done
if (!draggedRemoved) {
screens.splice(tabIndex, 1);
draggedRemoved = true;
}
// Find current index of the dragged tab in tempTabs
const currentIndexOfDraggedTab = tabs.indexOf(name);
// Move the dragged tab to its new position
if (currentIndexOfDraggedTab !== -1) {
tabs.splice(currentIndexOfDraggedTab, 1);
}
tabs.splice(newTabIndex, 0, name);
// Update visual positions of the tabs
tabs.forEach((tempTab, index) => {
const tabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${tempTab}"]`
) as HTMLElement;
if (tempTab !== name) {
tabElement.style.left = `${index * tabWidth}px`;
}
});
tabIndex = newTabIndex;
}
};
document.addEventListener("mousemove", handleMouseMove);
const handleMouseUp = (event: MouseEvent) => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
if (ref.current) {
// Reset transform for all tabs
const tabElements = tabContainerRef.current.querySelectorAll(".screen-tab");
tabElements.forEach((tab) => {
const htmlTab = tab as HTMLElement;
htmlTab.style.transform = "";
htmlTab.style.zIndex = "0";
});
// Update the final position of the dragged tab
const draggedTab = screens[tabIndex];
const finalLeftPosition = tabIndex * tabWidth;
const draggedTabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${draggedTab.name.get()}"]`
) as HTMLElement;
if (draggedTabElement) {
draggedTabElement.style.left = `${finalLeftPosition}px`;
}
}
setDraggedTab(null);
draggedRemoved = false;
};
document.addEventListener("mouseup", handleMouseUp);
}
},
[screens, dragStartPositions]
);
const onSwitchScreen = (screenId: string) => {
if (session == null) {
return;
}
if (session.activeScreenId.get() == screenId) {
return;
}
let screen = session.getScreenById(screenId);
if (screen == null) {
return;
}
GlobalCommandRunner.switchScreen(screenId);
};
const handleNewScreen = () => {
GlobalCommandRunner.createNewScreen();
};
if (session == null) {
return null;
}
const screen: Screen | null = null;
const activeScreenId = getActiveScreenId();
return (
<div className="screen-tabs-container">
<div className="screen-tabs-container-inner" ref={tabContainerRef}>
<For each="screen" of={tabs}>
<ScreenTab
key={screen.screenId}
screen={screen}
activeScreenId={activeScreenId}
onSwitchScreen={onSwitchScreen}
onDragStart={onDragStart}
/>
</For>
</div>
<div ref={addBtnRef} className="new-screen" onClick={handleNewScreen} style={{ left: DEFAULT_TAB_WIDTH }}>
<AddIcon className="icon hoverEffect" />
</div>
</div>
);
});
export { ScreenTabs };
+1 -1
View File
@@ -11,7 +11,7 @@ import { If } from "tsx-control-statements/components";
import { GlobalModel } from "@/models";
import { CmdInput } from "./cmdinput/cmdinput";
import { ScreenView } from "./screen/screenview";
import { ScreenTabs } from "./screen/tabs";
import { ScreenTabs } from "./screen/tabs2";
import { ErrorBoundary } from "@/common/error/errorboundary";
import * as textmeasure from "@/util/textmeasure";
import "./workspace.less";