From d326e1c73dd80baef4f4a14463f931bb83234666 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 11 May 2026 10:12:37 +0800 Subject: [PATCH] fix: hide popup triangle when inside popup + EPUB image-only paragraph rendering (#4121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Popup: hide the inner triangle when its anchor point lands inside the popup body. Extracted as a generic `isPointInRect` helper in `sel.ts` (with a default 1px padding so edge cases stay visible). - style.ts: handle `

` (common in some MOBI conversions) — clear hardcoded width/height and apply multiply blend for dark themes so the image doesn't sit on a colored box. - Annotator: shrink dict popup height from 480 to 360 to fit smaller screens. - foliate-js: submodule bump. Co-authored-by: Claude Opus 4.7 (1M context) --- .../src/__tests__/utils/sel.test.ts | 32 ++++++++++++++++++- .../reader/components/annotator/Annotator.tsx | 2 +- apps/readest-app/src/components/Popup.tsx | 20 ++++++++++-- apps/readest-app/src/utils/sel.ts | 9 ++++++ apps/readest-app/src/utils/style.ts | 8 +++++ packages/foliate-js | 2 +- 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/apps/readest-app/src/__tests__/utils/sel.test.ts b/apps/readest-app/src/__tests__/utils/sel.test.ts index 511f4a06..14f56e4c 100644 --- a/apps/readest-app/src/__tests__/utils/sel.test.ts +++ b/apps/readest-app/src/__tests__/utils/sel.test.ts @@ -6,9 +6,39 @@ import type { Rect, Position } from '@/utils/sel'; // constrainPointWithinRect is also non-exported but exercised through getPosition. // For getPopupPosition we can test directly. -import { getPopupPosition } from '@/utils/sel'; +import { getPopupPosition, isPointInRect } from '@/utils/sel'; describe('sel utilities', () => { + describe('isPointInRect', () => { + const rect: Rect = { left: 100, top: 50, right: 300, bottom: 200 }; + + it('returns true for a point strictly inside the rect', () => { + expect(isPointInRect({ x: 200, y: 100 }, rect)).toBe(true); + }); + + it('treats edges as outside with the default 1px padding', () => { + expect(isPointInRect({ x: 100, y: 100 }, rect)).toBe(false); + expect(isPointInRect({ x: 300, y: 100 }, rect)).toBe(false); + expect(isPointInRect({ x: 200, y: 50 }, rect)).toBe(false); + expect(isPointInRect({ x: 200, y: 200 }, rect)).toBe(false); + }); + + it('returns true on edges when padding is 0', () => { + expect(isPointInRect({ x: 100, y: 50 }, rect, 0)).toBe(true); + expect(isPointInRect({ x: 300, y: 200 }, rect, 0)).toBe(true); + }); + + it('returns false when x is outside', () => { + expect(isPointInRect({ x: 99, y: 100 }, rect)).toBe(false); + expect(isPointInRect({ x: 301, y: 100 }, rect)).toBe(false); + }); + + it('returns false when y is outside', () => { + expect(isPointInRect({ x: 200, y: 49 }, rect)).toBe(false); + expect(isPointInRect({ x: 200, y: 201 }, rect)).toBe(false); + }); + }); + describe('getPopupPosition', () => { const boundingRect: Rect = { top: 0, right: 800, bottom: 600, left: 0 }; diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index a37d62ae..2c3b4604 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -127,7 +127,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { // Tall enough to fit a header + 2-3 expanded cards comfortably. The popup // shows all enabled providers stacked (no tabs) so it needs more vertical // room than the legacy single-tab layout. - const dictPopupHeight = Math.min(480, maxHeight); + const dictPopupHeight = Math.min(360, maxHeight); const transPopupWidth = Math.min(480, maxWidth); const transPopupHeight = Math.min(265, maxHeight); const proofreadPopupWidth = Math.min(440, maxWidth); diff --git a/apps/readest-app/src/components/Popup.tsx b/apps/readest-app/src/components/Popup.tsx index 5a762f64..747c2876 100644 --- a/apps/readest-app/src/components/Popup.tsx +++ b/apps/readest-app/src/components/Popup.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { Position } from '@/utils/sel'; +import { Position, isPointInRect } from '@/utils/sel'; import { useEffect, useRef, useState } from 'react'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { useKeyDownActions } from '@/hooks/useKeyDownActions'; @@ -128,6 +128,18 @@ const Popup = ({ const outerTriangleStyles = getTriangleStyles(trianglePosition, 7, 0); const innerTriangleStyles = getTriangleStyles(trianglePosition, 7, -1); + const popupHeight = height ?? childrenHeight; + const triangleHidden = !!( + adjustedPosition && + trianglePosition && + isPointInRect(trianglePosition.point, { + left: adjustedPosition.point.x, + top: adjustedPosition.point.y, + right: adjustedPosition.point.x + width, + bottom: adjustedPosition.point.y + popupHeight, + }) + ); + return (
diff --git a/apps/readest-app/src/utils/sel.ts b/apps/readest-app/src/utils/sel.ts index 76fdf4a8..b0a9d18e 100644 --- a/apps/readest-app/src/utils/sel.ts +++ b/apps/readest-app/src/utils/sel.ts @@ -75,6 +75,15 @@ const constrainPointWithinRect = (point: Point, rect: Rect, padding: number) => }; }; +export const isPointInRect = (point: Point, rect: Rect, padding: number = 1): boolean => { + return ( + point.x >= rect.left + padding && + point.x <= rect.right - padding && + point.y >= rect.top + padding && + point.y <= rect.bottom - padding + ); +}; + export const isPointerInsideSelection = (selection: Selection, ev: PointerEvent) => { if (selection.rangeCount === 0) return false; const range = selection.getRangeAt(0); diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index 2e1cdf18..d300d541 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -182,6 +182,9 @@ const getColorStyles = ( hr.background-img { mix-blend-mode: multiply; } + p[width][height] > img:only-child { + mix-blend-mode: multiply; + } /* inline images */ *:has(> img.has-text-siblings):not(body) { ${overrideColor ? `background-color: ${bg};` : ''} @@ -349,6 +352,11 @@ const getPageLayoutStyles = ( width: auto; height: auto; } + /* some mobi */ + p[width][height] > img:only-child { + width: unset !important; + height: unset !important; + } /* page break */ body.paginated-mode div[style*="page-break-after: always"], diff --git a/packages/foliate-js b/packages/foliate-js index befe16d4..a7ecb05a 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit befe16d47c1b21f80a7eb67c16134eb44305e1c8 +Subproject commit a7ecb05a8319b15da893ba98cd5c0f5ecafa4a7a