fix(compact): size the window to the strip instead of a tuned constant

compactH was 158px, "tuned so the compact entry strip fits in a single row".
A constant tuned against a layout stops being true the moment the layout
changes, and this one outlived a strip that had since shrunk — leaving about
70px of empty window under the fields.

The frontend now measures what it rendered and asks for that height, watched by
a ResizeObserver so a strip that wraps at a narrow width is followed too. The
topbar is added as its declared h-8 rather than measured, since it is fixed.

Bounded in the backend: a measurement of zero — a layout not yet painted —
must not collapse the window, and the call is ignored unless compact is on so
nothing can shrink the normal window. Rounded to whole pixels so a sub-pixel
reflow cannot start a resize loop.
This commit is contained in:
2026-08-13 11:54:09 +02:00
parent 8fccfa29b1
commit 93879a0ce1
5 changed files with 60 additions and 4 deletions
+26
View File
@@ -17774,3 +17774,29 @@ func (a *App) SetSpotTTLMinutes(min int) error {
a.setSetting(keyClusterSpotTTL, strconv.Itoa(min)) a.setSetting(keyClusterSpotTTL, strconv.Itoa(min))
return nil return nil
} }
// SetCompactHeight resizes the compact window to fit its content.
//
// The height used to be a constant "tuned so the compact entry strip fits in a
// single row". Constants tuned against a layout stop being true the moment the
// layout changes, and this one outlived a strip that had since lost a row —
// leaving a band of empty window under the fields. The frontend measures what
// it actually rendered and says so.
//
// Ignored unless compact is on, so nothing can shrink the normal window.
func (a *App) SetCompactHeight(h int) {
if a.ctx == nil || !a.compact {
return
}
// Bounded: a measurement of zero (a layout not yet painted) must not collapse
// the window to nothing, and no strip is ever 600px tall.
if h < 80 || h > 600 {
return
}
w, _ := wruntime.WindowGetSize(a.ctx)
if w <= 0 {
w = compactW
}
wruntime.WindowSetMinSize(a.ctx, 900, h)
wruntime.WindowSetSize(a.ctx, w, h)
}
+4 -2
View File
@@ -7,14 +7,16 @@
"DX Cluster: a spot lifetime can be set — 5, 10, 15 minutes or your own value — after which spots leave the list and the band maps.", "DX Cluster: a spot lifetime can be set — 5, 10, 15 minutes or your own value — after which spots leave the list and the band maps.",
"Call lookup: QRZ.com now fills Name with the first name only, and optionally with the operator nickname instead.", "Call lookup: QRZ.com now fills Name with the first name only, and optionally with the operator nickname instead.",
"Entry form: a narrower left column, State beside the locator, and a new line with County, CQ, ITU and DXCC.", "Entry form: a narrower left column, State beside the locator, and a new line with County, CQ, ITU and DXCC.",
"Station Control: the short and long path headings are repeated under the compass, at a readable size and clickable." "Station Control: the short and long path headings are repeated under the compass, at a readable size and clickable.",
"Compact mode: the window now fits the entry strip instead of leaving a band of empty space below it."
], ],
"fr": [ "fr": [
"Cluster : « Masquer les contactés » ne masque plus un spot qui est un nouveau préfixe, comté, carré ou parc dans une contrée déjà faite.", "Cluster : « Masquer les contactés » ne masque plus un spot qui est un nouveau préfixe, comté, carré ou parc dans une contrée déjà faite.",
"Cluster DX : on peut fixer une durée de vie des spots — 5, 10, 15 minutes ou une valeur libre — au-delà de laquelle ils quittent la liste et les band maps.", "Cluster DX : on peut fixer une durée de vie des spots — 5, 10, 15 minutes ou une valeur libre — au-delà de laquelle ils quittent la liste et les band maps.",
"Recherche d indicatif : QRZ.com remplit désormais Nom avec le seul prénom, et au choix avec le surnom de l opérateur.", "Recherche d indicatif : QRZ.com remplit désormais Nom avec le seul prénom, et au choix avec le surnom de l opérateur.",
"Saisie : colonne de gauche plus étroite, État à côté du locator, et une nouvelle ligne Comté, CQ, ITU et DXCC.", "Saisie : colonne de gauche plus étroite, État à côté du locator, et une nouvelle ligne Comté, CQ, ITU et DXCC.",
"Contrôle station : les azimuts court et long chemin sont repris sous la boussole, lisibles et cliquables." "Contrôle station : les azimuts court et long chemin sont repris sous la boussole, lisibles et cliquables.",
"Mode compact : la fenêtre épouse la bande de saisie au lieu de laisser une bande vide en dessous."
] ]
}, },
{ {
+24 -2
View File
@@ -16,7 +16,7 @@ import {
GetStartupStatus, CheckForUpdate, DownloadAndApplyUpdate, GetLiveStations, GetWhatsNew, GetChangelog, GetStartupStatus, CheckForUpdate, DownloadAndApplyUpdate, GetLiveStations, GetWhatsNew, GetChangelog,
SMTPConfigured, SendLogToDeveloper, SMTPConfigured, SendLogToDeveloper,
WorkedBefore, WorkedBefore,
SetCompactMode, SetCompactMode, SetCompactHeight,
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, FlexApplyBandAntenna, FlexApplyBandPower, GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, FlexApplyBandAntenna, FlexApplyBandPower,
GetSecretStatus, UnlockSecrets, GetSecretStatus, UnlockSecrets,
RefreshCtyDat, DownloadAllReferenceLists, RefreshCtyDat, DownloadAllReferenceLists,
@@ -529,6 +529,28 @@ export default function App() {
setCompact(next); setCompact(next);
SetCompactMode(next); SetCompactMode(next);
} }
// Fit the compact window to what was actually rendered.
//
// The height was a constant "tuned so the compact entry strip fits in a single
// row". A constant tuned against a layout stops being true the moment the
// layout changes, and this one outlived a strip that had since lost a row —
// leaving a band of empty window under the fields. Measure instead.
const compactRootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!compact) return;
const fit = () => {
const el = compactRootRef.current;
if (!el) return;
// Rounded, so a sub-pixel reflow cannot start a resize loop.
// + the compact topbar, which is a fixed h-8 (32px), + a couple of pixels
// for the border so the strip is never clipped by one row of anti-aliasing.
SetCompactHeight(Math.round(el.getBoundingClientRect().height) + 32 + 2);
};
const id = window.setTimeout(fit, 60); // let the strip paint first
const ro = new ResizeObserver(fit);
if (compactRootRef.current) ro.observe(compactRootRef.current);
return () => { window.clearTimeout(id); ro.disconnect(); };
}, [compact]);
// CAT — receives live rig state via Wails events. // CAT — receives live rig state via Wails events.
const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any); const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any);
@@ -5892,7 +5914,7 @@ export default function App() {
Enter from any <input> inside the strip logs the QSO. Radix Selects Enter from any <input> inside the strip logs the QSO. Radix Selects
render as <button> elements and are ignored by this handler they render as <button> elements and are ignored by this handler they
keep their own keyboard behaviour. */} keep their own keyboard behaviour. */}
<div className={cn(!compact && 'flex gap-2.5 items-stretch px-2.5 pt-2.5 shrink-0')}> <div ref={compactRootRef} className={cn(!compact && 'flex gap-2.5 items-stretch px-2.5 pt-2.5 shrink-0')}>
<section <section
className={cn('bg-card shadow-sm border-border', className={cn('bg-card shadow-sm border-border',
compact compact
+2
View File
@@ -988,6 +988,8 @@ export function SetClublogMostWantedEnabled(arg1:boolean):Promise<void>;
export function SetClusterAutoConnect(arg1:boolean):Promise<void>; export function SetClusterAutoConnect(arg1:boolean):Promise<void>;
export function SetCompactHeight(arg1:number):Promise<void>;
export function SetCompactMode(arg1:boolean):Promise<void>; export function SetCompactMode(arg1:boolean):Promise<void>;
export function SetDVKLabel(arg1:number,arg2:string):Promise<void>; export function SetDVKLabel(arg1:number,arg2:string):Promise<void>;
+4
View File
@@ -1918,6 +1918,10 @@ export function SetClusterAutoConnect(arg1) {
return window['go']['main']['App']['SetClusterAutoConnect'](arg1); return window['go']['main']['App']['SetClusterAutoConnect'](arg1);
} }
export function SetCompactHeight(arg1) {
return window['go']['main']['App']['SetCompactHeight'](arg1);
}
export function SetCompactMode(arg1) { export function SetCompactMode(arg1) {
return window['go']['main']['App']['SetCompactMode'](arg1); return window['go']['main']['App']['SetCompactMode'](arg1);
} }