From 169b177271eec004803fa9054b301ffeb631285f Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Thu, 28 May 2026 11:56:09 -0700 Subject: [PATCH] Cap derived-quality ghost font size separately from exact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resolvedGhostFontSize` treated `.exact` and `.derived` carets identically, capping both at 24pt. In hosts that emit `.derived` geometry (Slack, several web editors), the line-box height commonly inflates against the real font size, so ghost text rendered with a ~20pt cap looked oversized next to ~13pt host text — the "insanely big derived" failure mode users reported. Split the cap into three tiers keyed off `CaretGeometryQuality`: 24pt for `.exact` (measured text bounds, still trusted), 17pt for `.derived` (just above the estimated cap, since derived geometry is more reliable than estimated but still not measured), 16pt for `.estimated` (unchanged). The observed failure mode is too-large, not too-small, so biasing the derived ceiling toward the smaller end is the right tradeoff. `GhostFontSizeStabilizer` still handles per-session downward smoothing on top of this. --- Cotabby/Services/UI/OverlayController.swift | 29 +++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Cotabby/Services/UI/OverlayController.swift b/Cotabby/Services/UI/OverlayController.swift index 75e74c63..e136866b 100644 --- a/Cotabby/Services/UI/OverlayController.swift +++ b/Cotabby/Services/UI/OverlayController.swift @@ -13,6 +13,11 @@ final class OverlayController: SuggestionOverlayControlling { private enum Layout { static let minimumGhostFontSize: CGFloat = 14 static let maximumGhostFontSize: CGFloat = 24 + /// Derived caret rects come from line-box geometry rather than measured text bounds, so the + /// reported height often inflates against the host's real font size (Slack, several web + /// editors). Keep the ceiling well below the exact cap so a wrong derived reading cannot + /// render oversized ghost text — the observed failure mode is too-large, not too-small. + static let maximumDerivedGhostFontSize: CGFloat = 17 static let maximumEstimatedGhostFontSize: CGFloat = 16 static let fontToLineHeightRatio: CGFloat = 0.78 } @@ -240,11 +245,13 @@ final class OverlayController: SuggestionOverlayControlling { panel.orderFrontRegardless() } - /// Exact and derived caret rects usually reflect the real text line height, so they may scale - /// up in larger editors. Estimated rects are much less trustworthy because some apps only - /// expose the full field frame; the extra ceiling prevents one bad estimate from rendering - /// comically oversized ghost text. `caretHeight` is already floored to the per-session minimum - /// by `ghostFontStabilizer`, so this only applies the static floor and quality ceilings. + /// Caps the proposed font size by how much we trust the caret geometry: + /// - `.exact`: measured text bounds, trusted up to the full ceiling so larger editors scale. + /// - `.derived`: line-box geometry that commonly inflates against the real font, tightly + /// capped to avoid the "insanely big derived" failure mode observed in Slack-like hosts. + /// - `.estimated`: full-field-frame fallback, even less trustworthy, tightest ceiling. + /// `caretHeight` is already floored to the per-session minimum by `ghostFontStabilizer`, so this + /// only applies the static floor and the per-quality ceiling. private func resolvedGhostFontSize( forCaretHeight caretHeight: CGFloat, caretQuality: CaretGeometryQuality @@ -253,9 +260,15 @@ final class OverlayController: SuggestionOverlayControlling { Layout.minimumGhostFontSize, caretHeight * Layout.fontToLineHeightRatio ) - let qualityCap = caretQuality == .estimated - ? Layout.maximumEstimatedGhostFontSize - : Layout.maximumGhostFontSize + let qualityCap: CGFloat + switch caretQuality { + case .exact: + qualityCap = Layout.maximumGhostFontSize + case .derived: + qualityCap = Layout.maximumDerivedGhostFontSize + case .estimated: + qualityCap = Layout.maximumEstimatedGhostFontSize + } return min(proposedSize, qualityCap) }