This commit is contained in:
Red Adaya 2024-03-26 06:32:53 +08:00
parent 6dec479133
commit c44d11cf54
4 changed files with 263 additions and 114 deletions

View File

@ -1,24 +1,100 @@
.screen-tab { .screen-tab {
position: absolute; font: var(--base-font);
border: 1px solid #ccc; font-size: var(--screentabs-font-size);
background-color: var(--app-bg-color); line-height: var(--screentabs-line-height);
cursor: pointer; border-top: 2px solid transparent;
user-select: none; background: var(--app-bg-color);
width: 100px; .background {
height: 46px; // 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.
color: var(--app-text-primary-color); z-index: 1;
border-radius: 0; width: var(--screen-tab-width);
box-sizing: border-box; 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;
}
&.active-screen-tab { .tab-name {
// background-color: @activeTabColor; flex-grow: 1;
// border-bottom: 2px solid @buttonBackgroundColor; }
// 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;
}
} }
} }
.screen-tab-inner {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
}

View File

@ -1,27 +1,71 @@
import React, { useRef } from "react"; 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"; import "./tab2.less";
type ScreenTabProps = { type ScreenTabProps = {
name: string; screen: Screen;
onSelect: (tabName: string) => void; activeScreenId: string;
active: boolean;
onDragStart: (name: string, ref: React.RefObject<HTMLDivElement>) => void; onDragStart: (name: string, ref: React.RefObject<HTMLDivElement>) => void;
onSwitchScreen: (screenId: string) => void;
}; };
const ScreenTab: React.FC<ScreenTabProps> = ({ name, onSelect, active, onDragStart }) => { const ScreenTab: React.FC<ScreenTabProps> = mobxReact.observer(
const ref = useRef<HTMLDivElement>(null); ({ screen, activeScreenId, onSwitchScreen, onDragStart }) => {
const ref = useRef<HTMLDivElement>(null);
return ( const openScreenSettings = (e: any, screen: Screen): void => {
<div e.preventDefault();
ref={ref} e.stopPropagation();
className={`screen-tab ${active ? "active-screen-tab" : ""}`} mobx.action(() => {
onMouseDown={() => onDragStart(name, ref)} GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
onClick={() => onSelect(name)} })();
data-screentab-name={name} GlobalModel.modalsModel.pushModal(constants.SCREEN_SETTINGS);
> };
<div className="screen-tab-inner">{name}</div>
</div> 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 }; export { ScreenTab };

View File

@ -1,28 +1,28 @@
.screen-tabs-container { .screen-tabs-container {
position: relative; position: relative;
height: var(--screentabs-height); height: var(--screentabs-height);
}
.screen-tabs-container-inner { .screen-tabs-container-inner {
position: relative; // Needed for absolute positioning of child tabs position: relative; // Needed for absolute positioning of child tabs
white-space: nowrap; white-space: nowrap;
height: 100%; height: 100%;
margin-right: 42px; margin-right: 42px;
overflow: hidden; overflow: hidden;
} }
.new-screen-button { .new-screen {
width: 42px; width: 42px;
height: 40px; height: 40px;
background-color: var(--app-bg-color); background-color: var(--app-bg-color);
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
line-height: 38px; line-height: 38px;
user-select: none; 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. // 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.

View File

@ -1,8 +1,10 @@
import React, { useState, useCallback, useRef, useEffect } from "react"; import React, { useState, useCallback, useRef, useEffect } from "react";
import { computed } from "mobx"; import { reaction } from "mobx";
import { ScreenTab } from "./tab2"; import { ScreenTab } from "./tab2";
import { observer } from "mobx-react"; import { observer, useLocalObservable } from "mobx-react";
import { For } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "@/models"; import { GlobalModel, GlobalCommandRunner, Session, Screen } from "@/models";
import AddIcon from "@/assets/icons/add.svg";
import "./tabs2.less"; import "./tabs2.less";
@ -13,10 +15,9 @@ type ScreenTabsProps = {
}; };
const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => { const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
const [tabs, setTabs] = useState(["Tab1"]); const [screens, setScreens] = useState<Screen[]>([]);
const [activeTab, setActiveTab] = useState("Tab1");
const [tabWidth, setTabWidth] = useState(DEFAULT_TAB_WIDTH); const [tabWidth, setTabWidth] = useState(DEFAULT_TAB_WIDTH);
const [draggedTab, setDraggedTab] = useState<string | null>(null); const [_, setDraggedTab] = useState<string | null>(null);
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]); const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
const tabContainerRef = useRef<HTMLDivElement>(null); const tabContainerRef = useRef<HTMLDivElement>(null);
const addBtnRef = useRef<HTMLDivElement>(null); const addBtnRef = useRef<HTMLDivElement>(null);
@ -27,6 +28,40 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
let draggedRemoved: boolean; let draggedRemoved: boolean;
let shrunk: 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 => { const getActiveScreenId = (): string | null => {
if (session) { if (session) {
return session.activeScreenId.get(); return session.activeScreenId.get();
@ -34,26 +69,6 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
return null; return null;
}; };
const getScreens = computed((): Screen[] => {
let activeScreenId = getActiveScreenId();
if (!activeScreenId) {
return [];
}
let screens = GlobalModel.getSessionScreens(session.sessionId);
let showingScreens = [];
for (const screen of screens) {
if (!screen.archived.get() || activeScreenId === screen.screenId) {
showingScreens.push(screen);
}
}
showingScreens.sort((a, b) => a.screenIdx.get() - b.screenIdx.get());
return showingScreens;
});
const updateTabPositions = useCallback(() => { const updateTabPositions = useCallback(() => {
if (tabContainerRef.current) { if (tabContainerRef.current) {
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab")); const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
@ -67,16 +82,16 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
setDragStartPositions(newStartPositions); setDragStartPositions(newStartPositions);
} }
}, [tabs]); }, [screens]);
useEffect(() => { useEffect(() => {
updateTabPositions(); updateTabPositions();
}, [tabs, updateTabPositions]); }, [screens, updateTabPositions]);
const resizeTabs = useCallback(() => { const resizeTabs = useCallback(() => {
if (tabContainerRef.current) { if (tabContainerRef.current) {
const containerWidth = tabContainerRef.current.getBoundingClientRect().width; const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
const numberOfTabs = tabs.length; const numberOfTabs = screens.length;
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH; const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
if (totalDefaultTabWidth > containerWidth) { if (totalDefaultTabWidth > containerWidth) {
@ -84,9 +99,9 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
shrunk = true; shrunk = true;
const newTabWidth = containerWidth / numberOfTabs; const newTabWidth = containerWidth / numberOfTabs;
setTabWidth(newTabWidth); setTabWidth(newTabWidth);
tabs.forEach((tab, index) => { screens.forEach((screen, index) => {
const tabElement = tabContainerRef.current.querySelector( const tabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${tab}"]` `[data-screentab-name="${screen.name.get()}"]`
) as HTMLElement; ) as HTMLElement;
tabElement.style.width = `${newTabWidth}px`; tabElement.style.width = `${newTabWidth}px`;
tabElement.style.left = `${index * newTabWidth}px`; tabElement.style.left = `${index * newTabWidth}px`;
@ -95,9 +110,9 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
// Case where tabs were previously shrunk or there is enough space for default width tabs // Case where tabs were previously shrunk or there is enough space for default width tabs
shrunk = false; shrunk = false;
setTabWidth(DEFAULT_TAB_WIDTH); setTabWidth(DEFAULT_TAB_WIDTH);
tabs.forEach((tab, index) => { screens.forEach((screen, index) => {
const tabElement = tabContainerRef.current.querySelector( const tabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${tab}"]` `[data-screentab-name="${screen.name.get()}"]`
) as HTMLElement; ) as HTMLElement;
tabElement.style.width = `${DEFAULT_TAB_WIDTH}px`; tabElement.style.width = `${DEFAULT_TAB_WIDTH}px`;
tabElement.style.left = `${index * DEFAULT_TAB_WIDTH}px`; tabElement.style.left = `${index * DEFAULT_TAB_WIDTH}px`;
@ -120,7 +135,7 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
} }
updateTabPositions(); updateTabPositions();
} }
}, [tabs.length, updateTabPositions]); }, [screens.length, updateTabPositions]);
// Resize tabs when the number of tabs or the window size changes // Resize tabs when the number of tabs or the window size changes
useEffect(() => { useEffect(() => {
@ -137,9 +152,9 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
}, [mainSidebarWidth, rightSidebarWidth]); }, [mainSidebarWidth, rightSidebarWidth]);
const onDragStart = useCallback( const onDragStart = useCallback(
(name: string, ref: React.RefObject<HTMLDivElement>) => { (screenId: string, ref: React.RefObject<HTMLDivElement>) => {
setDraggedTab(name); setDraggedTab(screenId);
let tabIndex = tabs.indexOf(name); let tabIndex = screens.findIndex((screen) => screen.screenId === screenId);
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
const containerWidth = tabContainerRef.current.getBoundingClientRect().width; const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
@ -154,7 +169,7 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
// Constrain movement within the container bounds // Constrain movement within the container bounds
if (tabContainerRef.current) { if (tabContainerRef.current) {
const numberOfTabs = tabs.length; const numberOfTabs = screens.length;
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH; const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
const containerRect = tabContainerRef.current.getBoundingClientRect(); const containerRect = tabContainerRef.current.getBoundingClientRect();
let containerRectWidth = containerRect.width; let containerRectWidth = containerRect.width;
@ -188,7 +203,7 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
if (dragDirection === "+") { if (dragDirection === "+") {
// Dragging to the right // Dragging to the right
for (let i = tabIndex + 1; i < tabs.length; i++) { for (let i = tabIndex + 1; i < screens.length; i++) {
const otherTabStart = dragStartPositions[i]; const otherTabStart = dragStartPositions[i];
if (currentX + tabWidth > otherTabStart + tabWidth / 2) { if (currentX + tabWidth > otherTabStart + tabWidth / 2) {
newTabIndex = i; newTabIndex = i;
@ -206,11 +221,11 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
// Rearrange the tabs temporarily // Rearrange the tabs temporarily
if (newTabIndex !== tabIndex) { if (newTabIndex !== tabIndex) {
const tempTabs = Array.from(tabs); const tempTabs = Array.from(screens);
// Remove the dragged tab if not already done // Remove the dragged tab if not already done
if (!draggedRemoved) { if (!draggedRemoved) {
tabs.splice(tabIndex, 1); screens.splice(tabIndex, 1);
draggedRemoved = true; draggedRemoved = true;
} }
@ -253,10 +268,10 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
}); });
// Update the final position of the dragged tab // Update the final position of the dragged tab
const draggedTab = tabs[tabIndex]; const draggedTab = screens[tabIndex];
const finalLeftPosition = tabIndex * tabWidth; const finalLeftPosition = tabIndex * tabWidth;
const draggedTabElement = tabContainerRef.current.querySelector( const draggedTabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${draggedTab}"]` `[data-screentab-name="${draggedTab.name.get()}"]`
) as HTMLElement; ) as HTMLElement;
if (draggedTabElement) { if (draggedTabElement) {
draggedTabElement.style.left = `${finalLeftPosition}px`; draggedTabElement.style.left = `${finalLeftPosition}px`;
@ -270,34 +285,48 @@ const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
document.addEventListener("mouseup", handleMouseUp); document.addEventListener("mouseup", handleMouseUp);
} }
}, },
[tabs, dragStartPositions] [screens, dragStartPositions]
); );
const selectTab = (tabName: string) => { const onSwitchScreen = (screenId: string) => {
setActiveTab(tabName); if (session == null) {
return;
}
if (session.activeScreenId.get() == screenId) {
return;
}
let screen = session.getScreenById(screenId);
if (screen == null) {
return;
}
GlobalCommandRunner.switchScreen(screenId);
}; };
const addTab = () => { const handleNewScreen = () => {
const newTabName = `Tab${tabs.length + 1}`; GlobalCommandRunner.createNewScreen();
setTabs([...tabs, newTabName]);
setActiveTab(newTabName);
}; };
if (session == null) {
return null;
}
const screen: Screen | null = null;
const activeScreenId = getActiveScreenId();
return ( return (
<div className="screen-tabs-container"> <div className="screen-tabs-container">
<div className="screen-tabs-container-inner" ref={tabContainerRef}> <div className="screen-tabs-container-inner" ref={tabContainerRef}>
{tabs.map((tab) => ( <For each="screen" of={tabs}>
<ScreenTab <ScreenTab
key={tab} key={screen.screenId}
name={tab} screen={screen}
onSelect={selectTab} activeScreenId={activeScreenId}
active={activeTab === tab} onSwitchScreen={onSwitchScreen}
onDragStart={onDragStart} onDragStart={onDragStart}
/> />
))} </For>
</div> </div>
<div ref={addBtnRef} className="new-screen-button" onClick={addTab} style={{ left: DEFAULT_TAB_WIDTH }}> <div ref={addBtnRef} className="new-screen" onClick={handleNewScreen} style={{ left: DEFAULT_TAB_WIDTH }}>
+ <AddIcon className="icon hoverEffect" />
</div> </div>
</div> </div>
); );