Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/auto hide tab bar #1527

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions docs/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ wsh editconfig
| window:magnifiedblockblursecondarypx | int | change the blur in CSS pixels that is applied to the visible portions of non-magnified blocks when a block is magnified (see [backdrop-filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter) for more info on how this gets applied) |
| window:maxtabcachesize | int | number of tabs to cache. when tabs are cached, switching between them is very fast. (defaults to 10) |
| window:showmenubar | bool | set to use the OS-native menu bar (Windows and Linux only, requires app restart) |
| window:autohidetabbar | bool | show and hide the tab bar automatically when the mouse moves near the top of the window
| window:nativetitlebar | bool | set to use the OS-native title bar, rather than the overlay (Windows and Linux only, requires app restart) |
| window:disablehardwareacceleration | bool | set to disable Chromium hardware acceleration to resolve graphical bugs (requires app restart) |
| window:savelastwindow | bool | when `true`, the last window that is closed is preserved and is reopened the next time the app is launched (defaults to `true`) |
Expand Down
9 changes: 9 additions & 0 deletions frontend/app/block/block.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0

@use "../mixins.scss";
@use "../tab/tabbar.scss" as tabbar;

.darwin:not(.fullscreen) {
.block.block-frame-default .block-frame-default-header {
.window-drag.left {
width: tabbar.$darwin-not-fullscreen-indent;
}
}
}

.block {
display: flex;
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import clsx from "clsx";
import * as jotai from "jotai";
import * as React from "react";
import { BlockFrameProps } from "./blocktypes";
import { WindowDrag } from "../element/windowdrag";

const NumActiveConnColors = 8;

Expand Down Expand Up @@ -181,6 +182,8 @@ const BlockFrame_Header = ({
const preIconButton = util.useAtomValueSafe(viewModel?.preIconButton);
let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText);
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
const settings = jotai.useAtomValue(atoms.settingsAtom);
const autoHideTabBar = settings?.["window:autohidetabbar"] ?? false;
const prevMagifiedState = React.useRef(magnified);
const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection);
const dragHandleRef = preview ? null : nodeModel.dragHandleRef;
Expand Down Expand Up @@ -252,6 +255,7 @@ const BlockFrame_Header = ({

return (
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
<WindowDrag className={clsx({ 'left': autoHideTabBar })} />
{preIconButtonElem}
<div className="block-frame-default-header-iconview">
{viewIconElem}
Expand Down
42 changes: 39 additions & 3 deletions frontend/app/tab/tabbar.scss
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

@use "./../theme.scss";

// 74px accounts for the macOS window controls and spacing in non-fullscreen mode
$darwin-not-fullscreen-indent: 74px;

.tab-bar-wrapper {
--default-indent: 10px;
--darwin-not-fullscreen-indent: 74px;
}

.darwin:not(.fullscreen) .tab-bar-wrapper {
.window-drag.left {
width: var(--darwin-not-fullscreen-indent);
width: $darwin-not-fullscreen-indent;
}
}

Expand All @@ -22,9 +26,41 @@
margin-bottom: 20px;
}

.tab-bar-wrapper {
.tab-bar-wrapper-always-visible {
padding-top: 6px;
position: relative;
opacity: 1;
}

.tab-bar-wrapper-auto-hide {
left: 0;
right: 0;

padding: 6px;
margin: 2px;

position: fixed;
z-index: 1000;
Comment on lines +42 to +43
Copy link

@coderabbitai coderabbitai bot Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Z-index value needs adjustment to follow established system

The z-index value of 1000 in tabbar.scss does not align with the established z-index system in theme.scss. The highest z-index value defined in the theme is 550 (for flash-error-container), and most UI components use values between 1-100. The tabbar should use a themed z-index variable that fits within this hierarchy.

  • Change z-index: 1000 to use a themed variable like --zindex-tabbar that should be defined in theme.scss with an appropriate value (likely between 100-500 based on other component values)
🔗 Analysis chain

Verify z-index value in the component hierarchy

The z-index value of 1000 seems arbitrary. Please verify that this value:

  1. Doesn't conflict with other fixed-position elements
  2. Is high enough to appear above all necessary elements
  3. Follows any existing z-index system in the codebase
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other z-index values in the codebase for comparison
rg "z-index:" "frontend/**/*.scss" "frontend/**/*.css"

Length of output: 171


Script:

#!/bin/bash
# Search for z-index values in the codebase without path restrictions
rg "z-index:"

Length of output: 119041


Script:

#!/bin/bash
# Find all CSS and SCSS files in the repository
fd -e scss -e css

Length of output: 2818


Script:

#!/bin/bash
# Search for any style files containing 'tabbar' in their name
fd "tabbar" -e scss -e css

Length of output: 55


Script:

#!/bin/bash
# Search for z-index declarations in theme.scss to check for any z-index system
rg "z-index|--zindex" frontend/app/theme.scss

# Search for fixed position elements with z-index
rg "position:\s*fixed.*z-index|z-index.*position:\s*fixed" -A 2 -B 2

Length of output: 1103

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@esimkowitz Thank you for confirming. Let me know if there's anything else you need!


opacity: 0;
background: transparent;
border-radius: 6px;

transition: opacity 0.3s ease, top 0.3s ease;

-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);

@supports not ((backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px))) {
background: rgb(from var(--block-bg-color) r g b / 0.8);
}
}

.tab-bar-wrapper-auto-hide-visible {
opacity: 1;
}

.tab-bar-wrapper {
user-select: none;
display: flex;
flex-direction: row;
Expand Down
37 changes: 36 additions & 1 deletion frontend/app/tab/tabbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { deleteLayoutModelForTab } from "@/layout/index";
import { atoms, createTab, getApi, globalStore, isDev, PLATFORM, setActiveTab } from "@/store/global";
import { fireAndForget } from "@/util/util";
import { useAtomValue } from "jotai";
import clsx from "clsx";
import { OverlayScrollbars } from "overlayscrollbars";
import { createRef, memo, useCallback, useEffect, useRef, useState } from "react";
import { debounce } from "throttle-debounce";
Expand Down Expand Up @@ -174,10 +175,44 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
const isFullScreen = useAtomValue(atoms.isFullScreen);

const settings = useAtomValue(atoms.settingsAtom);
const autoHideTabBar = settings?.["window:autohidetabbar"] ?? false;

let prevDelta: number;
let prevDragDirection: string;

const handleAutoHideTabBar = (event: MouseEvent) => {
const tabBar = tabbarWrapperRef.current;
const tabBarHeight = tabBar?.clientHeight + 1;

if (event.type === 'mouseenter') {
tabBar.style.top = '0px';
tabBar.addEventListener("mouseleave", handleAutoHideTabBar);
tabBar.classList.add('tab-bar-wrapper-auto-hide-visible')
}

if (event.type === 'mouseleave') {
tabBar.style.top = `-${tabBarHeight - 10}px`;
tabBar.removeEventListener("mouseleave", handleAutoHideTabBar);
tabBar.classList.remove('tab-bar-wrapper-auto-hide-visible')
}
};
Comment on lines +183 to +198
Copy link

@coderabbitai coderabbitai bot Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor to use CSS transforms instead of direct style manipulation.

The current implementation uses direct style manipulation which can cause unnecessary reflows. Consider using CSS transforms for better performance.

Apply this diff to use CSS transforms:

const handleAutoHideTabBar = (event: MouseEvent) => {
    const tabBar = tabbarWrapperRef.current;
-   const tabBarHeight = tabBar?.clientHeight + 1;

    if (event.type === 'mouseenter') {
-       tabBar.style.top = '0px';
-       tabBar.addEventListener("mouseleave", handleAutoHideTabBar);
        tabBar.classList.add('tab-bar-wrapper-auto-hide-visible')
    }

    if (event.type === 'mouseleave') {
-       tabBar.style.top = `-${tabBarHeight - 10}px`;
-       tabBar.removeEventListener("mouseleave", handleAutoHideTabBar);
        tabBar.classList.remove('tab-bar-wrapper-auto-hide-visible')
    }
};

Add these CSS classes to your stylesheet:

.tab-bar-wrapper-auto-hide {
    transform: translateY(-100%);
    transition: transform 0.2s ease;
}

.tab-bar-wrapper-auto-hide-visible {
    transform: translateY(0);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a good suggestion @rritik772

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


useEffect(() => {
const tabBar = tabbarWrapperRef.current;
if (!autoHideTabBar) {
tabBar.style.top = '0px';
return;
}

const tabBarHeight = tabBar?.clientHeight + 1;
if (autoHideTabBar) {
tabbarWrapperRef.current.style.top = `-${tabBarHeight - 10}px`
}

tabbarWrapperRef.current.addEventListener("mouseenter", handleAutoHideTabBar);
return () => tabbarWrapperRef.current.removeEventListener("mouseenter", handleAutoHideTabBar);
}, [autoHideTabBar])
Comment on lines +200 to +214
Copy link

@coderabbitai coderabbitai bot Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve event listener management.

The current implementation could lead to memory leaks if the component unmounts while listeners are still attached. Also, the cleanup function might fail if tabbarWrapperRef.current is null.

Apply this diff to improve event listener management:

useEffect(() => {
    const tabBar = tabbarWrapperRef.current;
    if (!autoHideTabBar || !tabBar) {
-       tabBar.style.top = '0px';
        return;
    }

-   const tabBarHeight = tabBar?.clientHeight + 1;
-   if (autoHideTabBar) {
-       tabbarWrapperRef.current.style.top = `-${tabBarHeight - 10}px`
-   }

-   tabbarWrapperRef.current.addEventListener("mouseenter", handleAutoHideTabBar);
+   const handleMouseEnter = (e: MouseEvent) => {
+       tabBar.classList.add('tab-bar-wrapper-auto-hide-visible');
+       tabBar.addEventListener('mouseleave', handleMouseLeave);
+   };

+   const handleMouseLeave = (e: MouseEvent) => {
+       tabBar.classList.remove('tab-bar-wrapper-auto-hide-visible');
+       tabBar.removeEventListener('mouseleave', handleMouseLeave);
+   };

+   tabBar.addEventListener("mouseenter", handleMouseEnter);
    
-   return () => tabbarWrapperRef.current.removeEventListener("mouseenter", handleAutoHideTabBar);
+   return () => {
+       tabBar.removeEventListener("mouseenter", handleMouseEnter);
+       tabBar.removeEventListener("mouseleave", handleMouseLeave);
+   };
}, [autoHideTabBar]);

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also a good suggestion, just in case the mouseleave handler is still attached

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@esimkowitz, glad to hear you find the suggestion helpful! By attaching and detaching the listeners in a more localized manner, we reduce the risk of orphaned event handlers if the component unmounts prematurely. Let me know if you need further refinements to the approach.


// Update refs when tabIds change
useEffect(() => {
tabRefs.current = tabIds.map((_, index) => tabRefs.current[index] || createRef());
Expand Down Expand Up @@ -654,7 +689,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
title: "Add Tab",
};
return (
<div ref={tabbarWrapperRef} className="tab-bar-wrapper">
<div ref={tabbarWrapperRef} className={clsx('tab-bar-wrapper', {'tab-bar-wrapper-auto-hide': autoHideTabBar, 'tab-bar-wrapper-always-visible': !autoHideTabBar})}>
<WindowDrag ref={draggerLeftRef} className="left" />
{appMenuButton}
{devLabel}
Expand Down
1 change: 1 addition & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ declare global {
"window:reducedmotion"?: boolean;
"window:tilegapsize"?: number;
"window:showmenubar"?: boolean;
"window:autohidetabbar"?: boolean;
"window:nativetitlebar"?: boolean;
"window:disablehardwareacceleration"?: boolean;
"window:maxtabcachesize"?: number;
Expand Down
1 change: 1 addition & 0 deletions pkg/wconfig/metaconsts.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const (
ConfigKey_WindowReducedMotion = "window:reducedmotion"
ConfigKey_WindowTileGapSize = "window:tilegapsize"
ConfigKey_WindowShowMenuBar = "window:showmenubar"
ConfigKey_WindowAutoHideTabBar = "window:autohidetabbar"
ConfigKey_WindowNativeTitleBar = "window:nativetitlebar"
ConfigKey_WindowDisableHardwareAcceleration = "window:disablehardwareacceleration"
ConfigKey_WindowMaxTabCacheSize = "window:maxtabcachesize"
Expand Down
1 change: 1 addition & 0 deletions pkg/wconfig/settingsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type SettingsType struct {
WindowReducedMotion bool `json:"window:reducedmotion,omitempty"`
WindowTileGapSize *int64 `json:"window:tilegapsize,omitempty"`
WindowShowMenuBar bool `json:"window:showmenubar,omitempty"`
WindowAutoHideTabBar bool `json:"window:autohidetabbar,omitempty"`
WindowNativeTitleBar bool `json:"window:nativetitlebar,omitempty"`
WindowDisableHardwareAcceleration bool `json:"window:disablehardwareacceleration,omitempty"`
WindowMaxTabCacheSize int `json:"window:maxtabcachesize,omitempty"`
Expand Down