Skip to content

Commit d89ea3b

Browse files
SWangHashSWangHash
authored andcommitted
!224 merge zz/fix into main
fix(web-ui): anchor native title tooltip at pointer and stop following movement Created-by: dyxdsz Commit-by: dyxdsz Merged-by: SWangHash Description: ## Summary <!-- Briefly describe what changed. --> Fixes # ## Type and Areas Type: <!-- Feature / bug fix / regression fix / refactor / UI/UX / docs / test / CI / dependency / other. --> Areas: <!-- Rust core, desktop/Tauri, web UI, mobile web, server/relay, AI adapters, installer, docs, etc. --> ## Motivation / Impact <!-- What problem does this solve, and what changes for users or developers? Write "No direct user-facing change" if applicable. --> ## Verification <!-- List exact commands, manual checks, and outcomes. For docs-only or template-only changes, use the lightest relevant checks or explain why runtime checks were skipped. --> ## Reviewer Notes <!-- Optional: screenshots, architecture notes, compatibility risks, migration notes, or rollback guidance. --> ## Checklist - [ ] This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts. - [ ] Relevant verification is recorded above, or skipped checks are explained. - [ ] User-facing strings, docs, and locales are updated where applicable. See merge request: OpenHarmonyPCDeveloper/BitFun!224
2 parents fc76643 + 7c2bb5c commit d89ea3b

22 files changed

Lines changed: 520 additions & 320 deletions

File tree

‎scripts/theme-color-governance-baseline.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@
252252
"max": 0
253253
},
254254
"colorDomainScopes.themePreset.occurrences": {
255-
"max": 159
255+
"max": 158
256256
},
257257
"colorDomainScopes.themePreset.uniqueColors": {
258258
"max": 112

‎src/apps/ohos/entry/src/main/ets/entryability/EntryAbility.ets‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ export default class EntryAbility extends RustAbility {
386386
req.visible,
387387
);
388388
if (req.visible) {
389-
mainWin.setWindowDecorHeight(44);
389+
mainWin.setWindowDecorHeight(38);
390390
mainWin.setWindowTitleMoveEnabled(false);
391391
mainWin.setWindowDecorVisible(false);
392392
}
@@ -496,6 +496,19 @@ export default class EntryAbility extends RustAbility {
496496
return '';
497497
});
498498
RustModule.registerArktsFunction('set_min_size_ohos', async (err: Error, arg: string): Promise<string> => {
499+
const req: SetMinSizeArg = JSON.parse(arg);
500+
try {
501+
const mainWin = windowStage.getMainWindowSync();
502+
await mainWin.setWindowLimits({
503+
minWidth: req.width,
504+
minHeight: req.height,
505+
maxWidth: -1,
506+
maxHeight: -1,
507+
});
508+
} catch (e) {
509+
const be = e as BusinessError;
510+
hilog.warn(DOMAIN, TAG, `setWindowLimits failed: ${be.message}`);
511+
}
499512
return '';
500513
});
501514
RustModule.registerArktsFunction('set_focus_ohos', async (err: Error, arg: string): Promise<string> => {
@@ -629,6 +642,27 @@ export default class EntryAbility extends RustAbility {
629642
data.setWindowDecorHeight(38);
630643
data.setWindowTitleMoveEnabled(false);
631644
data.setWindowDecorVisible(false);
645+
// setWindowLimits expects px (physical pixels). Convert the 800x600
646+
// logical minimum, matching the desktop MAIN_WINDOW_MIN_WIDTH/HEIGHT,
647+
// using the display density so non-1x scaling keeps the same logical
648+
// minimum as Windows/Linux/macOS.
649+
let scale: number = 1;
650+
try {
651+
scale = display.getDefaultDisplaySync().densityPixels > 0
652+
? display.getDefaultDisplaySync().densityPixels : 1;
653+
} catch (e) {
654+
hilog.warn(DOMAIN, TAG, 'read densityPixels on startup failed: ' + JSON.stringify(e));
655+
}
656+
const minWidth: number = Math.round(800 * scale);
657+
const minHeight: number = Math.round(600 * scale);
658+
data.setWindowLimits({
659+
minWidth: minWidth,
660+
minHeight: minHeight,
661+
maxWidth: -1,
662+
maxHeight: -1,
663+
}).catch((e: BusinessError) => {
664+
hilog.warn(DOMAIN, TAG, 'setWindowLimits on startup failed: ' + e.message);
665+
});
632666
})
633667
}, 40)
634668
RustModule.registerArktsFunction('reveal_in_explorer', async (err: Error, arg: string): Promise<string> => {

‎src/web-ui/src/app/components/panels/FilesPanel.tsx‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type { FileSystemNode } from '@/tools/file-system/types';
2020
import { globalEventBus } from '@/infrastructure/event-bus';
2121
import { useNotification } from '@/shared/notification-system';
2222
import { InputDialog, CubeLoading } from '@/component-library';
23-
import { openFileInBestTarget } from '@/shared/utils/tabUtils';
23+
import { openFileInBestTarget, isFileAlreadyOpenInBestTarget } from '@/shared/utils/tabUtils';
2424
import { getMotionAwareScrollBehavior } from '@/shared/utils/motionPreference';
2525
import { PanelHeader } from './base';
2626
import { createLogger } from '@/shared/utils/logger';
@@ -363,6 +363,12 @@ const FilesPanel: React.FC<FilesPanelProps> = ({
363363
// ===== File Operation Handlers =====
364364

365365
const shouldOpenLargeFile = useCallback(async (filePath: string, nodeSize?: number): Promise<boolean> => {
366+
// Opening an already-open file only switches to its existing tab without
367+
// reloading content, so the large-file warning is unnecessary.
368+
if (isFileAlreadyOpenInBestTarget(filePath)) {
369+
return true;
370+
}
371+
366372
let fileSize: number | undefined = nodeSize;
367373

368374
if (fileSize === undefined || fileSize === null) {
@@ -1052,7 +1058,12 @@ const FilesPanel: React.FC<FilesPanelProps> = ({
10521058
} else {
10531059
setInternalViewMode(next);
10541060
}
1055-
}, [viewMode, onViewModeChange]);
1061+
// When switching back to the tree, the search input unmounts and focus
1062+
// would land on <body>, leaving the filetree shortcut scope. Restore it.
1063+
if (next === 'tree') {
1064+
focusFileTree();
1065+
}
1066+
}, [viewMode, onViewModeChange, focusFileTree]);
10561067

10571068
const handleExplorerToolbarNewFile = useCallback(() => {
10581069
const parentPath = getNewItemParentPath(workspacePath, selectedFile, fileTree);

‎src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,16 @@ const FlexiblePanel: React.FC<ExtendedFlexiblePanelProps> = memo(({
380380

381381
case 'image-viewer': {
382382
const imageViewerData = content.data || {};
383-
383+
384384
return (
385385
<div className="bitfun-flexible-panel__image-viewer-container" data-bf-component="flexible-panel" data-bf-part="image">
386386
{renderLazyEditor(
387387
<ImageViewer
388388
filePath={imageViewerData.filePath || ''}
389389
fileName={content.title}
390390
workspacePath={workspacePath}
391+
isActiveTab={isActive}
392+
onFileMissingFromDiskChange={onFileMissingFromDiskChange}
391393
className="bitfun-flexible-panel__image-viewer"
392394
/>
393395
)}

‎src/web-ui/src/app/hooks/useApp.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,12 @@ export const useApp = (): UseAppReturn => {
9090
}, []);
9191

9292
const updateRightPanelWidth = useCallback((width: number) => {
93-
// Clamp width: 200px min, 1200px max
94-
const MIN_WIDTH = 200;
93+
// SessionScene already validates the width against the live container size
94+
// (dynamic max, compact floor when there is room). A hard floor here would
95+
// override that validated width on narrow windows and reintroduce the
96+
// horizontal overflow this clamp once guarded against, so only keep the
97+
// sanity bounds.
98+
const MIN_WIDTH = 0;
9599
const MAX_WIDTH = 1200;
96100
const clampedWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, width));
97101

‎src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ const FileViewerNav: React.FC = () => {
5252
<span className="bitfun-file-viewer-nav__actions" data-bf-component="file-viewer-nav" data-bf-part="actions">
5353
{viewMode === 'tree' && explorerToolbar && (
5454
<>
55+
{/* Keep focus on the file tree when clicking toolbar actions:
56+
these buttons live outside the filetree shortcut scope, so
57+
letting mousedown move focus would disable filetree-scoped
58+
shortcuts (F2 / Delete / Ctrl+N / F5 / ...) after the click. */}
5559
<IconButton
5660
size="xs"
5761
variant="ghost"
62+
onMouseDown={(e) => e.preventDefault()}
5863
onClick={explorerToolbar.onNewFile}
5964
tooltip={shortcutTooltip(tTools('fileTree.newFile'), 'filetree.newFile')}
6065
tooltipPlacement="bottom"
@@ -64,6 +69,7 @@ const FileViewerNav: React.FC = () => {
6469
<IconButton
6570
size="xs"
6671
variant="ghost"
72+
onMouseDown={(e) => e.preventDefault()}
6773
onClick={explorerToolbar.onNewFolder}
6874
tooltip={shortcutTooltip(tTools('fileTree.newFolder'), 'filetree.newFolder')}
6975
tooltipPlacement="bottom"
@@ -73,6 +79,7 @@ const FileViewerNav: React.FC = () => {
7379
<IconButton
7480
size="xs"
7581
variant="ghost"
82+
onMouseDown={(e) => e.preventDefault()}
7683
onClick={explorerToolbar.onRefresh}
7784
tooltip={shortcutTooltip(tTools('fileTree.refresh'), 'filetree.refresh')}
7885
tooltipPlacement="bottom"
@@ -83,6 +90,7 @@ const FileViewerNav: React.FC = () => {
8390
)}
8491
<IconButton
8592
size="xs"
93+
onMouseDown={(e) => e.preventDefault()}
8694
onClick={handleToggleViewMode}
8795
tooltip={viewMode === 'tree' ? tFiles('actions.switchToSearch') : tFiles('actions.switchToTree')}
8896
tooltipPlacement="bottom"

‎src/web-ui/src/app/scenes/session/SessionScene.scss‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@
6565

6666
.bitfun-session-scene__chat-pane {
6767
flex: 1 1 auto;
68-
min-width: 400px;
68+
// 400px is the comfortable minimum, but it is a hard floor no more: on a
69+
// narrow window the sum of the chat floor, the resizer and the aux pane
70+
// exceeds the row, and the excess was clipped by the row's overflow: hidden.
71+
// Capping the floor at the row width lets the pane shrink instead.
72+
min-width: min(400px, 100%);
6973
height: 100%;
7074
overflow: hidden;
7175
position: relative;

‎src/web-ui/src/app/scenes/session/SessionScene.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,10 @@ const SessionScene: React.FC<SessionSceneProps> = ({
161161
if (containerWidth <= 0) return newWidth;
162162
// NavPanel (240px) is outside SessionScene — only account for resizer + min chat width
163163
const reserved = PANEL_COMMON_CONFIG.RESIZER_WIDTH + PANEL_COMMON_CONFIG.MIN_CENTER_WIDTH;
164-
const dynamicMax = containerWidth - reserved;
164+
// Below the reserved minimum the dynamic max goes negative; a negative
165+
// inline width is dropped by the browser and the pane falls back to its
166+
// content width, which widens the overflow instead of shrinking it.
167+
const dynamicMax = Math.max(0, containerWidth - reserved);
165168
const maxWidth = Math.min(RIGHT_PANEL_CONFIG.MAX_WIDTH, dynamicMax);
166169
return Math.min(maxWidth, Math.max(RIGHT_PANEL_CONFIG.COMPACT_WIDTH, newWidth));
167170
}, []);

‎src/web-ui/src/app/scenes/settings/SettingsNav.scss‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@
3939
padding: 0 $size-gap-3 $size-gap-2;
4040
}
4141

42-
[data-bf-appearance-switching='true'] &__search-field .search__wrapper,
43-
[data-bf-appearance-switching='true'] &__search-field .search__wrapper * {
44-
transition: none !important;
45-
}
46-
4742
&__search-field.search {
4843
width: 100%;
4944

‎src/web-ui/src/component-library/components/Search/Search.scss‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,21 @@
281281
}
282282

283283

284+
/**
285+
* Appearance switching suppresses transitions via `transition: none !important`
286+
* rules on element trees (`... *`), but `*` cannot match pseudo-elements. The
287+
* placeholder transition would otherwise keep animating color while the rest
288+
* of the UI switches instantly.
289+
*/
290+
[data-bf-appearance-switching='true'] .search__wrapper,
291+
[data-bf-appearance-switching='true'] .search__wrapper *,
292+
[data-bf-appearance-switching='true'] .search__wrapper ::placeholder,
293+
[data-bf-appearance-switching='true'] .search__prefix,
294+
[data-bf-appearance-switching='true'] .search__prefix *,
295+
[data-bf-appearance-switching='true'] .search__icon {
296+
transition: none !important;
297+
}
298+
284299
@keyframes search-spin {
285300
to {
286301
transform: rotate(360deg);

0 commit comments

Comments
 (0)