From 832bacb569ba9121cdb7d0f476d5c4c92b3348e3 Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Sat, 18 Apr 2026 01:34:49 +0200 Subject: [PATCH] feat(ui-scale): re-enable scoped interface scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document-level zoom broke portal positioning and Tauri window measurement, so the slider had been forced to 100 %. Scope the zoom to a wrapper inside .main-content instead — sidebar, queue, player bar and the Linux custom title bar live in sibling grid cells of .app-shell and stay 1:1. - App.tsx: drop the document-level zoom + auto-reset; wrap main-content children in
. - layout.css: add .main-content-zoom (flex column, fills parent). - Settings.tsx: re-enable the slider and snap it to the 6 preset stops (80/90/100/110/125/150) so the thumb lands directly under each preset button. Legacy off-preset values snap to the nearest stop on load. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.tsx | 20 ++++------- src/pages/Settings.tsx | 82 ++++++++++++++++++++++-------------------- src/styles/layout.css | 14 ++++++++ 3 files changed, 65 insertions(+), 51 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 367cd9ee..85423850 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -127,6 +127,7 @@ function AppShell() { const toggleFullscreen = usePlayerStore(s => s.toggleFullscreen); const isQueueVisible = usePlayerStore(s => s.isQueueVisible); const toggleQueue = usePlayerStore(s => s.toggleQueue); + const uiScale = useFontStore(s => s.uiScale); const initializeFromServerQueue = usePlayerStore(s => s.initializeFromServerQueue); const currentTrack = usePlayerStore(s => s.currentTrack); const isPlaying = usePlayerStore(s => s.isPlaying); @@ -341,6 +342,7 @@ function AppShell() { /> )}
+
@@ -389,6 +391,7 @@ function AppShell() { } />
+
{!isMobile && (
s.theme); // keep subscription so re-render on manual change const effectiveTheme = useThemeScheduler(); const font = useFontStore(s => s.font); - const uiScale = useFontStore(s => s.uiScale); - const setUiScale = useFontStore(s => s.setUiScale); const [exportPickerOpen, setExportPickerOpen] = useState(false); useEffect(() => { @@ -931,17 +932,10 @@ export default function App() { document.documentElement.setAttribute('data-font', font); }, [font]); - // TODO(ui-scale): UI scaling is disabled pending a cross-platform rework. - // Reset any stored non-100% value so users aren't stuck at a broken scale. - // When re-enabling: remove this effect AND re-enable the slider in Settings.tsx. - useEffect(() => { - if (uiScale !== 1.0) setUiScale(1.0); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - document.documentElement.style.zoom = String(uiScale); - }, [uiScale]); + // UI scaling is scoped to .main-content via an inner wrapper (see
+ // below). Sidebar, queue, player bar and (Linux) custom title bar stay 1:1 + // because they live in separate grid cells. Document-level zoom is not used + // — it broke portal positioning and Tauri window measurement. useEffect(() => { return initAudioListeners(); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 73c555c2..2ad153b9 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1739,50 +1739,56 @@ export default function Settings() {

{t('settings.uiScaleTitle')}

- {/* TODO: UI scaling is being reworked — disabled until fixed */} -

- Interface scaling is currently being reworked and will be available in a future update. -

-
+
{t('settings.uiScaleLabel')} {Math.round(fontStore.uiScale * 100)}%
- fontStore.setUiScale(parseFloat(e.target.value))} - className="ui-scale-slider" - /> -
- {[80, 90, 100, 110, 125, 150].map(p => { - const pct = ((p / 100) - 0.8) / (1.5 - 0.8) * 100; - const active = Math.round(fontStore.uiScale * 100) === p; - return ( - - ); - })} -
+ {(() => { + const presets = [80, 90, 100, 110, 125, 150]; + const currentPct = Math.round(fontStore.uiScale * 100); + let idx = presets.indexOf(currentPct); + if (idx < 0) { + // Snap legacy off-preset values to the closest preset. + idx = presets.reduce((best, p, i) => + Math.abs(p - currentPct) < Math.abs(presets[best] - currentPct) ? i : best, 0); + } + return ( + <> + fontStore.setUiScale(presets[parseInt(e.target.value, 10)] / 100)} + className="ui-scale-slider" + /> +
+ {presets.map(p => { + const active = currentPct === p; + return ( + + ); + })} +
+ + ); + })()}
diff --git a/src/styles/layout.css b/src/styles/layout.css index 358f63b5..40265d7e 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -940,6 +940,20 @@ z-index: 1; } +/* Scoped UI scaling target. Sidebar / queue / player / titlebar live in + sibling grid cells of .app-shell and stay at 1:1. The wrapper inherits + .main-content's flex column layout so .content-header (fixed height) and + .content-body (flex: 1, scroll) keep working unchanged. + Zoom is applied via inline style only when uiScale !== 1, so the default + case has no extra layer or layout cost. */ +.main-content-zoom { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + min-width: 0; +} + .content-header { display: flex; align-items: center;