From 87aea21184122b08cdb406b0956a2bee4d51bd76 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 4 Dec 2024 20:20:06 -0500 Subject: [PATCH 1/3] Better flicker prevention when switching tabs (#1389) There was still some flicker when nohover took effect before the tab view actually switched. Now, we override the active tab in the tabbar when the app is switching tabs. We also override active tab behavior so that the close button is always visible while nohover is in effect. This effectively removes the flickering --- frontend/app/store/global.ts | 13 +++++++++---- frontend/app/tab/tab.scss | 6 ++++++ frontend/wave.ts | 13 +++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 3f931266f..74ee256e4 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -43,6 +43,9 @@ function setPlatform(platform: NodeJS.Platform) { PLATFORM = platform; } +// Used to override the tab id when switching tabs to prevent flicker in the tab bar. +const overrideStaticTabAtom = atom(null) as PrimitiveAtom; + function initGlobalAtoms(initOpts: GlobalInitOptions) { const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom; const clientIdAtom = atom(initOpts.clientId) as PrimitiveAtom; @@ -100,9 +103,8 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { const tabAtom: Atom = atom((get) => { return WOS.getObjectValue(WOS.makeORef("tab", initOpts.tabId), get); }); - const staticTabIdAtom: Atom = atom((get) => { - return initOpts.tabId; - }); + // This atom is used to determine the tab id to use for the static tab. It is set to the overrideStaticTabAtom value if it is not null, otherwise it is set to the initOpts.tabId value. + const staticTabIdAtom: Atom = atom((get) => get(overrideStaticTabAtom) ?? initOpts.tabId); const controlShiftDelayAtom = atom(false); const updaterStatusAtom = atom("up-to-date") as PrimitiveAtom; try { @@ -625,7 +627,9 @@ function createTab() { } function setActiveTab(tabId: string) { - // We use this hack to prevent a flicker in the tab bar when switching to a new tab. This class is unset in reinitWave in wave.ts. See tab.scss for where this class is used. + // We use this hack to prevent a flicker of the previously-hovered tab when this view was last active. This class is set in setActiveTab in global.ts. See tab.scss for where this class is used. + // Also overrides the staticTabAtom to the new tab id so that the active tab is set correctly. + globalStore.set(overrideStaticTabAtom, tabId); document.body.classList.add("nohover"); getApi().setActiveTab(tabId); } @@ -653,6 +657,7 @@ export { isDev, loadConnStatus, openLink, + overrideStaticTabAtom, PLATFORM, pushFlashError, pushNotification, diff --git a/frontend/app/tab/tab.scss b/frontend/app/tab/tab.scss index 80358e264..771df50a4 100644 --- a/frontend/app/tab/tab.scss +++ b/frontend/app/tab/tab.scss @@ -102,6 +102,7 @@ } } +// Only apply hover effects when not in nohover mode. This prevents the previously-hovered tab from remaining hovered while a tab view is not mounted. body:not(.nohover) .tab:hover { & + .tab::after, &::after { @@ -120,6 +121,11 @@ body:not(.nohover) .tab:hover { } } +// When in nohover mode, always show the close button on the active tab. This prevents the close button of the active tab from flickering when nohover is toggled. +body.nohover .tab.active .close { + visibility: visible; +} + @keyframes expandWidthAndFadeIn { from { width: var(--initial-tab-width); diff --git a/frontend/wave.ts b/frontend/wave.ts index de2d249cc..c296de862 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -22,6 +22,7 @@ import { initGlobal, initGlobalWaveEventSubs, loadConnStatus, + overrideStaticTabAtom, pushFlashError, pushNotification, removeNotificationById, @@ -88,12 +89,12 @@ async function reinitWave() { console.log("Reinit Wave"); getApi().sendLog("Reinit Wave"); - // We use this hack to prevent a flicker in the tab bar when switching to a new tab. This class is set in setActiveTab in global.ts. See tab.scss for where this class is used. - requestAnimationFrame(() => { - setTimeout(() => { - document.body.classList.remove("nohover"); - }, 100); - }); + // We use this hack to prevent a flicker of the previously-hovered tab when this view was last active. This class is set in setActiveTab in global.ts. See tab.scss for where this class is used. + // Also overrides the staticTabAtom to the new tab id so that the active tab is set correctly. + globalStore.set(overrideStaticTabAtom, savedInitOpts.tabId); + setTimeout(() => { + document.body.classList.remove("nohover"); + }, 100); const client = await WOS.reloadWaveObject(WOS.makeORef("client", savedInitOpts.clientId)); const waveWindow = await WOS.reloadWaveObject(WOS.makeORef("window", savedInitOpts.windowId)); From df2889f2802ff6e390430f0daf8095136d6bfc7c Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 4 Dec 2024 20:30:28 -0500 Subject: [PATCH 2/3] Fix zombie tab context menus (#1390) The memoizing of the tabs was causing the callbacks for handleContextMenu to become dead ends. This makes more of the callbacks into memoized callbacks and makes the handleContextMenu function itself a memoized callback to ensure it's properly updated when its upstream callbacks change. --- frontend/app/tab/tab.tsx | 83 +++++++++++++++++++------------------ frontend/app/tab/tabbar.tsx | 15 ++++--- 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index ced541e96..302d66154 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -7,7 +7,7 @@ import { TabRpcClient } from "@/app/store/wshrpcutil"; import { Button } from "@/element/button"; import { ContextMenuModel } from "@/store/contextmenu"; import { clsx } from "clsx"; -import { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from "react"; +import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { ObjectService } from "../store/services"; import { makeORef, useWaveObjectValue } from "../store/wos"; import "./tab.scss"; @@ -144,47 +144,50 @@ const Tab = memo( event.stopPropagation(); }; - function handleContextMenu(e: React.MouseEvent) { - e.preventDefault(); - let menu: ContextMenuItem[] = [ - { label: isPinned ? "Unpin Tab" : "Pin Tab", click: onPinChange }, - { label: "Rename Tab", click: () => handleRenameTab(null) }, - { label: "Copy TabId", click: () => navigator.clipboard.writeText(id) }, - { type: "separator" }, - ]; - const fullConfig = globalStore.get(atoms.fullConfigAtom); - const bgPresets: string[] = []; - for (const key in fullConfig?.presets ?? {}) { - if (key.startsWith("bg@")) { - bgPresets.push(key); - } - } - bgPresets.sort((a, b) => { - const aOrder = fullConfig.presets[a]["display:order"] ?? 0; - const bOrder = fullConfig.presets[b]["display:order"] ?? 0; - return aOrder - bOrder; - }); - if (bgPresets.length > 0) { - const submenu: ContextMenuItem[] = []; - const oref = makeORef("tab", id); - for (const presetName of bgPresets) { - const preset = fullConfig.presets[presetName]; - if (preset == null) { - continue; + const handleContextMenu = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + let menu: ContextMenuItem[] = [ + { label: isPinned ? "Unpin Tab" : "Pin Tab", click: () => onPinChange() }, + { label: "Rename Tab", click: () => handleRenameTab(null) }, + { label: "Copy TabId", click: () => navigator.clipboard.writeText(id) }, + { type: "separator" }, + ]; + const fullConfig = globalStore.get(atoms.fullConfigAtom); + const bgPresets: string[] = []; + for (const key in fullConfig?.presets ?? {}) { + if (key.startsWith("bg@")) { + bgPresets.push(key); } - submenu.push({ - label: preset["display:name"] ?? presetName, - click: () => { - ObjectService.UpdateObjectMeta(oref, preset); - RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 }); - }, - }); } - menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" }); - } - menu.push({ label: "Close Tab", click: () => onClose(null) }); - ContextMenuModel.showContextMenu(menu, e); - } + bgPresets.sort((a, b) => { + const aOrder = fullConfig.presets[a]["display:order"] ?? 0; + const bOrder = fullConfig.presets[b]["display:order"] ?? 0; + return aOrder - bOrder; + }); + if (bgPresets.length > 0) { + const submenu: ContextMenuItem[] = []; + const oref = makeORef("tab", id); + for (const presetName of bgPresets) { + const preset = fullConfig.presets[presetName]; + if (preset == null) { + continue; + } + submenu.push({ + label: preset["display:name"] ?? presetName, + click: () => { + ObjectService.UpdateObjectMeta(oref, preset); + RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 }); + }, + }); + } + menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" }); + } + menu.push({ label: "Close Tab", click: () => onClose(null) }); + ContextMenuModel.showContextMenu(menu, e); + }, + [onPinChange, handleRenameTab, id, onClose, isPinned] + ); return (
{ deleteLayoutModelForTab(tabId); }; - const handlePinChange = (tabId: string, pinned: boolean) => { - console.log("handlePinChange", tabId, pinned); - fireAndForget(async () => { - await WorkspaceService.ChangeTabPinning(workspace.oid, tabId, pinned); - }); - }; + const handlePinChange = useCallback( + (tabId: string, pinned: boolean) => { + console.log("handlePinChange", tabId, pinned); + fireAndForget(async () => { + await WorkspaceService.ChangeTabPinning(workspace.oid, tabId, pinned); + }); + }, + [workspace] + ); const handleTabLoaded = useCallback((tabId: string) => { setTabsLoaded((prev) => { From 5c315779ba5f02711d3595ef90c83785356cbd25 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 4 Dec 2024 20:35:05 -0500 Subject: [PATCH 3/3] Remove another instance of redundant Vite Sass config (#1391) --- electron.vite.config.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 03cc883a5..f2d8572d9 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -76,12 +76,5 @@ export default defineConfig({ targets: [{ src: "node_modules/monaco-editor/min/vs/*", dest: "monaco" }], }), ], - css: { - preprocessorOptions: { - scss: { - api: "modern-compiler", // or "modern" - }, - }, - }, }, });