feat: interface-size (UI scale) presets

Webview-zoom-style presets: Extra small 0.8 / Small 0.9 / Default 1.0 /
Large 1.2 / Extra large 1.5, applied through SLINT_SCALE_FACTOR set at
the top of main() before any thread or the backend exists. The env var
overrides the compositor DPR, so the value is last observed DPR x
preset; the Default preset leaves the var unset (stock DPR handling,
including live monitor changes, stays intact). last_dpr is persisted
from the winit window (real compositor DPR) on the 5s renderer-sentinel
timer.

- const-bake guard: strip SLINT_SCALE_FACTOR in qbz-ui build.rs so the
  slint compiler never bakes the factor into the generated code
- new UiScale global + scale-divided window minimums keep the physical
  minimum constant across presets (XL fits a 1366x768 screen); the
  three window-geometry gates in main.rs use the process-active factor
- artwork decode targets scale with the preset: sharp covers at >=1.2,
  smaller decoded-cache footprint below 1.0
- Settings > Appearance "Interface size" row below Language (replaces
  the commented-out percentage placeholder and its orphan properties),
  restart-to-apply toast
- Typography.boost = 1.1 on Extra small only, so body text stays
  readable at the 0.8 factor; Small (0.9) needs no compensation
- i18n: new strings added to all 7 locale catalogs

Known accepted limitation: slight text softness at fractional scale
factors (owner-accepted; diagnostic notes live in
qbz-nix-docs/ui-scale-presets/).

cc
This commit is contained in:
vicrodh committed 2026-07-02 08:36:40 -06:00
1 parent d9b82e7b12
commit b7549790a3
17 files changed
+385 -43

No files matched your search

+5
View File
@@ -2,6 +2,11 @@
//! single entry point. Translations are bundled (pure-Rust, no C dep) from //! single entry point. Translations are bundled (pure-Rust, no C dep) from
//! `translations/<lang>/LC_MESSAGES/qbz-slint.po`; msgid = English source, no context. //! `translations/<lang>/LC_MESSAGES/qbz-slint.po`; msgid = English source, no context.
fn main() { fn main() {
// SLINT_SCALE_FACTOR is a RUNTIME preference in this app (main.rs sets it
// from the persisted interface-size preset). If it leaks into the BUILD
// environment, the slint compiler const-propagates the factor and the
// runtime override becomes a permanent no-op — strip it unconditionally.
std::env::remove_var("SLINT_SCALE_FACTOR");
let config = slint_build::CompilerConfiguration::new() let config = slint_build::CompilerConfiguration::new()
.with_bundled_translations("translations") .with_bundled_translations("translations")
.with_default_translation_context(slint_build::DefaultTranslationContext::None); .with_default_translation_context(slint_build::DefaultTranslationContext::None);
@@ -4174,3 +4174,21 @@ msgstr "JACK ist nicht bit-perfekt"
msgid "The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough." msgid "The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough."
msgstr "Das JACK-Backend leitet die Audioausgabe durch den JACK-Graphen, der auf die Graph-Abtastrate resampled. Für bit-perfekte Wiedergabe verwende ALSA (direkt) oder PipeWire mit Passthrough." msgstr "Das JACK-Backend leitet die Audioausgabe durch den JACK-Graphen, der auf die Graph-Abtastrate resampled. Für bit-perfekte Wiedergabe verwende ALSA (direkt) oder PipeWire mit Passthrough."
msgid "Extra large"
msgstr "Extra groß"
msgid "INTERFACE"
msgstr "OBERFLÄCHE"
msgid "Interface size"
msgstr "Oberflächengröße"
msgid "Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)"
msgstr "Skaliert die gesamte Oberfläche, wie der Browser-Zoom. Klein zeigt mehr Inhalt auf dem Bildschirm, Groß und Extra groß verbessern die Lesbarkeit (Neustart erforderlich)"
msgid "Interface size changed — restart QBZ to apply"
msgstr "Oberflächengröße geändert — QBZ neu starten, um sie zu übernehmen"
msgid "Extra small"
msgstr "Extra klein"
@@ -60,3 +60,21 @@ msgstr[1] "Added {} tracks to queue"
msgid "Unfollow on Qobuz" msgid "Unfollow on Qobuz"
msgstr "Unfollow on Qobuz" msgstr "Unfollow on Qobuz"
msgid "Extra large"
msgstr "Extra large"
msgid "INTERFACE"
msgstr "INTERFACE"
msgid "Interface size"
msgstr "Interface size"
msgid "Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)"
msgstr "Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)"
msgid "Interface size changed — restart QBZ to apply"
msgstr "Interface size changed — restart QBZ to apply"
msgid "Extra small"
msgstr "Extra small"
@@ -4231,3 +4231,21 @@ msgstr "JACK no es bit-perfect"
msgid "The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough." msgid "The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough."
msgstr "El backend de JACK enruta el audio por el grafo de JACK, que remuestrea a la frecuencia del grafo. Para reproducción bit-perfect usa ALSA (directo) o PipeWire con passthrough." msgstr "El backend de JACK enruta el audio por el grafo de JACK, que remuestrea a la frecuencia del grafo. Para reproducción bit-perfect usa ALSA (directo) o PipeWire con passthrough."
msgid "Extra large"
msgstr "Extra grande"
msgid "INTERFACE"
msgstr "INTERFAZ"
msgid "Interface size"
msgstr "Tamaño de la interfaz"
msgid "Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)"
msgstr "Escala toda la interfaz, como el zoom del navegador. Pequeño muestra más contenido en pantalla, Grande y Extra grande mejoran la legibilidad (requiere reiniciar)"
msgid "Interface size changed — restart QBZ to apply"
msgstr "Tamaño de la interfaz cambiado — reinicia QBZ para aplicar"
msgid "Extra small"
msgstr "Extra pequeño"
@@ -4174,3 +4174,21 @@ msgstr "JACK n'est pas bit-perfect"
msgid "The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough." msgid "The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough."
msgstr "Le backend JACK achemine l'audio via le graphe JACK, qui rééchantillonne à la fréquence du graphe. Pour une lecture bit-perfect, utilisez ALSA (direct) ou PipeWire avec passthrough." msgstr "Le backend JACK achemine l'audio via le graphe JACK, qui rééchantillonne à la fréquence du graphe. Pour une lecture bit-perfect, utilisez ALSA (direct) ou PipeWire avec passthrough."
msgid "Extra large"
msgstr "Très grand"
msgid "INTERFACE"
msgstr "INTERFACE"
msgid "Interface size"
msgstr "Taille de l'interface"
msgid "Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)"
msgstr "Met à l'échelle toute l'interface, comme le zoom du navigateur. Petit affiche plus de contenu à l'écran, Grand et Très grand améliorent la lisibilité (redémarrage requis)"
msgid "Interface size changed — restart QBZ to apply"
msgstr "Taille de l'interface modifiée — redémarrez QBZ pour appliquer"
msgid "Extra small"
msgstr "Très petit"
@@ -4121,3 +4121,21 @@ msgstr "ビットパーフェクトなHi-Fi再生を求めるオーディオフ
msgid "Built with Slint, and powered by Rodio and Symphonia. With thanks to Qobuz, MusicBrainz, Song.link/Odesli, LRCLIB, lyrics.ovh, Lucide, and Kawarp." msgid "Built with Slint, and powered by Rodio and Symphonia. With thanks to Qobuz, MusicBrainz, Song.link/Odesli, LRCLIB, lyrics.ovh, Lucide, and Kawarp."
msgstr "Slintで構築し、RodioとSymphoniaを採用しています。Qobuz、MusicBrainz、Song.link/Odesli、LRCLIB、lyrics.ovh、Lucide、Kawarpに感謝します。" msgstr "Slintで構築し、RodioとSymphoniaを採用しています。Qobuz、MusicBrainz、Song.link/Odesli、LRCLIB、lyrics.ovh、Lucide、Kawarpに感謝します。"
msgid "Extra large"
msgstr "特大"
msgid "INTERFACE"
msgstr "インターフェース"
msgid "Interface size"
msgstr "インターフェースのサイズ"
msgid "Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)"
msgstr "ブラウザのズームのようにインターフェース全体を拡大縮小します。「小」は画面により多くのコンテンツを表示し、「大」と「特大」は読みやすさを向上させます(再起動が必要)"
msgid "Interface size changed — restart QBZ to apply"
msgstr "インターフェースのサイズを変更しました — 適用するには QBZ を再起動してください"
msgid "Extra small"
msgstr "極小"
@@ -4174,3 +4174,21 @@ msgstr "JACK não é bit-perfect"
msgid "The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough." msgid "The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough."
msgstr "O backend JACK roteia o áudio pelo grafo do JACK, que faz resample para a taxa do grafo. Para reprodução bit-perfect, use ALSA (direto) ou PipeWire com passthrough." msgstr "O backend JACK roteia o áudio pelo grafo do JACK, que faz resample para a taxa do grafo. Para reprodução bit-perfect, use ALSA (direto) ou PipeWire com passthrough."
msgid "Extra large"
msgstr "Extra grande"
msgid "INTERFACE"
msgstr "INTERFACE"
msgid "Interface size"
msgstr "Tamanho da interface"
msgid "Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)"
msgstr "Dimensiona toda a interface, como o zoom do navegador. Pequeno mostra mais conteúdo na tela, Grande e Extra grande melhoram a legibilidade (requer reinicialização)"
msgid "Interface size changed — restart QBZ to apply"
msgstr "Tamanho da interface alterado — reinicie o QBZ para aplicar"
msgid "Extra small"
msgstr "Extra pequeno"
@@ -4145,3 +4145,21 @@ msgstr "Нативный клиент музыкального стриминг
msgid "Built with Slint, and powered by Rodio and Symphonia. With thanks to Qobuz, MusicBrainz, Song.link/Odesli, LRCLIB, lyrics.ovh, Lucide, and Kawarp." msgid "Built with Slint, and powered by Rodio and Symphonia. With thanks to Qobuz, MusicBrainz, Song.link/Odesli, LRCLIB, lyrics.ovh, Lucide, and Kawarp."
msgstr "Создано на Slint, работает на Rodio и Symphonia. Благодарим Qobuz, MusicBrainz, Song.link/Odesli, LRCLIB, lyrics.ovh, Lucide и Kawarp." msgstr "Создано на Slint, работает на Rodio и Symphonia. Благодарим Qobuz, MusicBrainz, Song.link/Odesli, LRCLIB, lyrics.ovh, Lucide и Kawarp."
msgid "Extra large"
msgstr "Очень большой"
msgid "INTERFACE"
msgstr "ИНТЕРФЕЙС"
msgid "Interface size"
msgstr "Размер интерфейса"
msgid "Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)"
msgstr "Масштабирует весь интерфейс, как масштаб страницы в браузере. Маленький вмещает больше контента на экране, Большой и Очень большой улучшают читаемость (требуется перезапуск)"
msgid "Interface size changed — restart QBZ to apply"
msgstr "Размер интерфейса изменён — перезапустите QBZ, чтобы применить"
msgid "Extra small"
msgstr "Очень маленький"
+10 -4
View File
@@ -17,16 +17,19 @@ import { SplashScreen } from "splash/SplashScreen.slint";
import { LoginScreen } from "login/LoginScreen.slint"; import { LoginScreen } from "login/LoginScreen.slint";
import { AppShell } from "shell/AppShell.slint"; import { AppShell } from "shell/AppShell.slint";
import { Toast } from "primitives/Toast.slint"; import { Toast } from "primitives/Toast.slint";
import { ShellState, MiniPlayerState, NowPlayingState, AppearanceState } from "state.slint"; import { ShellState, MiniPlayerState, NowPlayingState, AppearanceState, UiScale } from "state.slint";
import { MiniShell } from "miniplayer/MiniShell.slint"; import { MiniShell } from "miniplayer/MiniShell.slint";
// Re-export the theming globals so the Rust layer can push palettes (Theme.c / // Re-export the theming globals so the Rust layer can push palettes (Theme.c /
// Theme.is-system) and keep the dropdown index (ThemeState.mode) in sync. // Theme.is-system) and keep the dropdown index (ThemeState.mode) in sync.
export { Theme, ThemeColors } from "foundation/semantic-colors.slint"; export { Theme, ThemeColors } from "foundation/semantic-colors.slint";
export { ThemeState } from "foundation/theme-state.slint"; export { ThemeState } from "foundation/theme-state.slint";
// Typography is exported for the Small-preset font compensation only: Rust
// seeds `Typography.boost = 1.1` when the interface-size preset is Small.
export { Typography } from "foundation/typography.slint";
// Re-export the state globals so the Rust layer can populate them. // Re-export the state globals so the Rust layer can populate them.
export { HomeState, HomeActions, DiscoverState, DiscoverActions, SectionDescriptor, ConfigRow, DiscoverBrowseState, DiscoverBrowseActions, ForYouState, ExternalRecoState, ExternalRecoActions, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, LyricsState, LyricsLineItem, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, AwardState, AwardActions, AwardEntry, ArtistReleasesState, ArtistReleasesActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, PlaylistPickerState, PlaylistPickerActions, DuplicateConfirmState, DuplicateConfirmActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, MyQbzCreateState, MyQbzCreateActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, BlacklistState, BlacklistActions, BlacklistedArtistItem, MyQbzState, MyQbzActions, MixtapeCardItem, MyQbzAddState, MyQbzAddActions, MyQbzAddRow, MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, MyQbzEditState, MyQbzEditActions, MyQbzMixState, MyQbzMixActions, DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate, LocalLibraryState, LocalLibraryActions, LibraryFoldersState, LibFolderEditState, LibraryManageActions, LibraryScanState, LibAlbumFilterState, LocalAlbumState, LocalAlbumActions, TagEditorState, TagEditorActions, FolderEditState, FolderEditActions, ToastState, TextUtil, QconnectDevState, QconnectDevice, CastState, CastDevice, CastActions, AppearanceState, MyQbzBrandingState, EphemeralPlayChoiceState, EphemeralPlayChoiceActions, PlexSettingsState, PlexAuthActions, PlexSectionItem, ScrobbleState, ScrobbleActions, DiscordState, OfflineState, OfflineModeActions, OfflineFavoritesState, OfflineFavoritesActions, ImportLogEntry, PlaylistImportState, PlaylistImportActions, DacWizardState, DacWizardActions, DacCandidateRow, RemediationRow, DacConfigRow, InfoCreditRow, InfoCreditPair, AlbumCreditPerformer, AlbumCreditTrack, TrackInfoState, TrackInfoActions, AlbumInfoState, AlbumInfoActions, BookletState, BookletActions, SuggestionsState, SuggestionsActions, SuggestionCard, PlaylistSuggestionsState, PlaylistSuggestionsActions, PlaylistSuggestionRow, VisualizerState, ImmersiveState, ImmersiveSearchActions, ImmersiveActions, MiniPlayerState, WindowControlActions, PurchasesState, PurchasesActions, PurchaseAlbumItem, PurchaseTrackItem, PurchaseAlbumGroup, PurchaseTrackGroup, PurchaseFormatItem, PurchaseDetailState, PurchaseDetailActions, PurchaseDetailTrack, KeybindingRow, KeybindingCategoryGroup, KeybindingsState, KeybindingsActions, KeyboardShortcutsState, LinkResolverState, LinkResolverActions, UiFocusState, SleepTimerState, SleepTimerActions, LogRow, LogViewerState, DiagRow, DiagnosticsState, ReportIssueState, ReportIssueActions, AboutState, AboutActions, AboutContributorRow, AboutContributorGroup, WhatsNewState, WhatsNewActions, WhatsNewBlock, WhatsNewTocEntry } from "state.slint"; export { HomeState, HomeActions, DiscoverState, DiscoverActions, SectionDescriptor, ConfigRow, DiscoverBrowseState, DiscoverBrowseActions, ForYouState, ExternalRecoState, ExternalRecoActions, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, LyricsState, LyricsLineItem, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, AwardState, AwardActions, AwardEntry, ArtistReleasesState, ArtistReleasesActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, PlaylistPickerState, PlaylistPickerActions, DuplicateConfirmState, DuplicateConfirmActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, MyQbzCreateState, MyQbzCreateActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, BlacklistState, BlacklistActions, BlacklistedArtistItem, MyQbzState, MyQbzActions, MixtapeCardItem, MyQbzAddState, MyQbzAddActions, MyQbzAddRow, MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, MyQbzEditState, MyQbzEditActions, MyQbzMixState, MyQbzMixActions, DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate, LocalLibraryState, LocalLibraryActions, LibraryFoldersState, LibFolderEditState, LibraryManageActions, LibraryScanState, LibAlbumFilterState, LocalAlbumState, LocalAlbumActions, TagEditorState, TagEditorActions, FolderEditState, FolderEditActions, ToastState, TextUtil, QconnectDevState, QconnectDevice, CastState, CastDevice, CastActions, AppearanceState, MyQbzBrandingState, EphemeralPlayChoiceState, EphemeralPlayChoiceActions, PlexSettingsState, PlexAuthActions, PlexSectionItem, ScrobbleState, ScrobbleActions, DiscordState, OfflineState, OfflineModeActions, OfflineFavoritesState, OfflineFavoritesActions, ImportLogEntry, PlaylistImportState, PlaylistImportActions, DacWizardState, DacWizardActions, DacCandidateRow, RemediationRow, DacConfigRow, InfoCreditRow, InfoCreditPair, AlbumCreditPerformer, AlbumCreditTrack, TrackInfoState, TrackInfoActions, AlbumInfoState, AlbumInfoActions, BookletState, BookletActions, SuggestionsState, SuggestionsActions, SuggestionCard, PlaylistSuggestionsState, PlaylistSuggestionsActions, PlaylistSuggestionRow, VisualizerState, ImmersiveState, ImmersiveSearchActions, ImmersiveActions, MiniPlayerState, WindowControlActions, PurchasesState, PurchasesActions, PurchaseAlbumItem, PurchaseTrackItem, PurchaseAlbumGroup, PurchaseTrackGroup, PurchaseFormatItem, PurchaseDetailState, PurchaseDetailActions, PurchaseDetailTrack, KeybindingRow, KeybindingCategoryGroup, KeybindingsState, KeybindingsActions, KeyboardShortcutsState, LinkResolverState, LinkResolverActions, UiFocusState, UiScale, SleepTimerState, SleepTimerActions, LogRow, LogViewerState, DiagRow, DiagnosticsState, ReportIssueState, ReportIssueActions, AboutState, AboutActions, AboutContributorRow, AboutContributorGroup, WhatsNewState, WhatsNewActions, WhatsNewBlock, WhatsNewTocEntry } from "state.slint";
// Which top-level screen is shown. The app starts on `splash` while it // Which top-level screen is shown. The app starts on `splash` while it
// restores a saved session, then resolves to `shell` or `login`. // restores a saved session, then resolves to `shell` or `login`.
@@ -41,8 +44,11 @@ export component AppWindow inherits Window {
: "QBZ"; : "QBZ";
preferred-width: 1180px; preferred-width: 1180px;
preferred-height: 760px; preferred-height: 760px;
min-width: 940px; // Divide the interface-size preset back out so the PHYSICAL minimum stays
min-height: 600px; // constant across presets (a plain 940px logical minimum at the XL preset
// would be 1410 physical px — taller than a 1366x768 screen).
min-width: 940px / UiScale.factor;
min-height: 600px / UiScale.factor;
background: Theme.surface-main; background: Theme.surface-main;
// Resolved by the Rust layer from the desktop environment (e.g. the // Resolved by the Rust layer from the desktop environment (e.g. the
+16 -9
View File
@@ -1,18 +1,25 @@
// Typography scale — font sizes and weights. // Typography scale — font sizes and weights.
export global Typography { export global Typography {
out property <length> legal: 13px; // Small-preset font compensation. The interface-size preset scales the
out property <length> link: 14px; // whole window (SLINT_SCALE_FACTOR), so at Small (0.8) body text would
out property <length> body: 15px; // drop to 12px — too small. Rust seeds `boost = 1.1` for that preset
out property <length> subtitle: 15px; // only (net body 15 × 0.8 × 1.1 ≈ 13.2px, readable); every other preset
out property <length> button: 17px; // keeps 1.0. Layout metrics take the full 0.8 on purpose — the extra
// density is the point of Small.
in-out property <float> boost: 1.0;
out property <length> legal: 13px * boost;
out property <length> link: 14px * boost;
out property <length> body: 15px * boost;
out property <length> subtitle: 15px * boost;
out property <length> button: 17px * boost;
// Canonical page / section title — Home section headings ("New // Canonical page / section title — Home section headings ("New
// Releases", ...) and the hero titles on Album / Artist / Playlist // Releases", ...) and the hero titles on Album / Artist / Playlist
// / Mixtape detail pages. One size for all, for consistency. // / Mixtape detail pages. One size for all, for consistency.
out property <length> section: 18px; out property <length> section: 18px * boost;
out property <length> heading: 19px; out property <length> heading: 19px * boost;
out property <length> title: 25px; out property <length> title: 25px * boost;
out property <length> wordmark: 29px; out property <length> wordmark: 29px * boost;
out property <int> regular: 400; out property <int> regular: 400;
out property <int> medium: 500; out property <int> medium: 500;
@@ -267,15 +267,15 @@ export component AppearanceSettings inherits VerticalLayout {
} }
} }
} }
// UI Scale — hidden per owner request (only the global scale factor works // Interface size — the working replacement for the old percentage "UI
// for now). Code kept commented for re-enable; do NOT delete. // Scale" placeholder that was parked here (commented, unwired). Window
/* // scale-factor preset via SLINT_SCALE_FACTOR (main.rs); restart to apply.
SettingRow { SettingRow {
unsupported: true; label: @tr("Interface size");
label: @tr("UI Scale"); description: @tr("Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)");
QbzSelect { QbzSelect {
menu-width: 140px; menu-width: 200px;
options: AppearanceState.ui-scales; options: AppearanceState.ui-scale-options;
current-index: AppearanceState.ui-scale-index; current-index: AppearanceState.ui-scale-index;
selected(i) => { selected(i) => {
AppearanceState.ui-scale-index = i; AppearanceState.ui-scale-index = i;
@@ -283,7 +283,6 @@ export component AppearanceSettings inherits VerticalLayout {
} }
} }
} }
*/
SettingRow { SettingRow {
label: @tr("Immersive search"); label: @tr("Immersive search");
+20 -2
View File
@@ -2904,12 +2904,14 @@ export global AppearanceState {
in-out property <string> auto-theme-detected-de: ""; in-out property <string> auto-theme-detected-de: "";
// --- TYPOGRAPHY / SCALE / LANGUAGE ------------------------------------- // --- TYPOGRAPHY / SCALE / LANGUAGE -------------------------------------
// NOTE: the old Tauri-style percentage `ui-scales` placeholder properties
// that lived here were superseded by the working interface-size presets
// (`ui-scale-options` / `ui-scale-index` in the INTERFACE SIZE block
// further down, wired to SLINT_SCALE_FACTOR in main.rs).
in-out property <[string]> languages: [@tr("Auto"), "English", "Español", "Français", "Deutsch", "Português", "Русский", "日本語"]; in-out property <[string]> languages: [@tr("Auto"), "English", "Español", "Français", "Deutsch", "Português", "Русский", "日本語"];
in-out property <int> language-index: 0; in-out property <int> language-index: 0;
in-out property <[string]> fonts: ["LINE Seed JP", "Montserrat", "Noto Sans", "Source Sans 3", "System"]; in-out property <[string]> fonts: ["LINE Seed JP", "Montserrat", "Noto Sans", "Source Sans 3", "System"];
in-out property <int> font-index: 0; in-out property <int> font-index: 0;
in-out property <[string]> ui-scales: ["80%", "85%", "90%", "95%", "100%", "110%", "125%", "150%", "175%"];
in-out property <int> ui-scale-index: 4;
// Immersive search action — what selecting a result in the Immersive search // Immersive search action — what selecting a result in the Immersive search
// cortinilla does (index: 0=Disabled, 1=Replace queue, 2=Play next, // cortinilla does (index: 0=Disabled, 1=Replace queue, 2=Play next,
@@ -3005,6 +3007,13 @@ export global AppearanceState {
in-out property <[string]> renderer-options: [@tr("Auto (recommended)"), @tr("GPU"), @tr("GPU (compatibility)"), @tr("Software")]; in-out property <[string]> renderer-options: [@tr("Auto (recommended)"), @tr("GPU"), @tr("GPU (compatibility)"), @tr("Software")];
in-out property <int> renderer-index: 0; in-out property <int> renderer-index: 0;
// --- INTERFACE SIZE ----------------------------------------------------
// Window scale-factor preset (XS 0.8 / Small 0.9 / Default 1.0 /
// Large 1.2 / XL 1.5) — startup-time choice, restart to apply.
// Index 2 = Default.
in-out property <[string]> ui-scale-options: [@tr("Extra small"), @tr("Small"), @tr("Default"), @tr("Large"), @tr("Extra large")];
in-out property <int> ui-scale-index: 2;
// --- IMMERSIVE --------------------------------------------------------- // --- IMMERSIVE ---------------------------------------------------------
in-out property <[string]> immersive-views: [ in-out property <[string]> immersive-views: [
@tr("Remember last used"), @tr("Coverflow"), @tr("Static"), @tr("Vinyl"), @tr("Visualizer"), @tr("Remember last used"), @tr("Coverflow"), @tr("Static"), @tr("Vinyl"), @tr("Visualizer"),
@@ -3619,6 +3628,15 @@ export global LocationViewActions {
callback load-more(); callback load-more();
} }
// Interface-size preset (UI scale). Seeded once from Rust before the shell
// renders. The actual scaling happens through the window scale factor
// (SLINT_SCALE_FACTOR set in main.rs) — this global only lets bindings that
// must stay PHYSICALLY constant divide the preset back out (e.g. the window
// minimums, so an XL window still fits a 1366x768 screen).
export global UiScale {
in-out property <float> factor: 1.0;
}
// Shell chrome state — collapsible left sidebar and right-side panels. // Shell chrome state — collapsible left sidebar and right-side panels.
// Globals are imported directly by the shell components, so toggles // Globals are imported directly by the shell components, so toggles
// need no callback plumbing. // need no callback plumbing.
+18 -2
View File
@@ -23,6 +23,22 @@ const MAX_CONCURRENT: usize = 16;
/// modest DPI without holding full ~600px source textures in memory. /// modest DPI without holding full ~600px source textures in memory.
const DECODE_SIZE: u32 = 264; const DECODE_SIZE: u32 = 264;
/// Interface-size preset multiplier for decode targets, set ONCE at startup
/// (main.rs, before any artwork job runs). Under a scaled UI every card gets
/// `preset ×` more physical pixels, so decode sizes must grow with it or
/// covers go soft at Large/XL; Small shrinks them, saving decoded-cache RAM.
static UI_SCALE_FACTOR: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
pub fn set_ui_scale_factor(factor: f32) {
let _ = UI_SCALE_FACTOR.set(factor);
}
/// Scale a base decode size by the interface-size preset, rounded up.
pub fn scaled_decode(base: u32) -> u32 {
let factor = UI_SCALE_FACTOR.get().copied().unwrap_or(1.0);
(base as f32 * factor).ceil() as u32
}
/// Default image-cache size budget (matches the Tauri default). /// Default image-cache size budget (matches the Tauri default).
pub const MAX_CACHE_BYTES: u64 = 200 * 1024 * 1024; pub const MAX_CACHE_BYTES: u64 = 200 * 1024 * 1024;
@@ -241,7 +257,7 @@ impl ArtworkTarget {
/// huge buffers in the model — a 2000-row playlist would hold /// huge buffers in the model — a 2000-row playlist would hold
/// hundreds of MB. Decode row thumbnails small. /// hundreds of MB. Decode row thumbnails small.
fn decode_size(&self) -> u32 { fn decode_size(&self) -> u32 {
match self { scaled_decode(match self {
ArtworkTarget::SearchTrack { .. } ArtworkTarget::SearchTrack { .. }
| ArtworkTarget::FavoriteTrack { .. } | ArtworkTarget::FavoriteTrack { .. }
| ArtworkTarget::MixTrack { .. } | ArtworkTarget::MixTrack { .. }
@@ -261,7 +277,7 @@ impl ArtworkTarget {
// Detail list-row thumbnails render at 36px. // Detail list-row thumbnails render at 36px.
ArtworkTarget::MyQbzDetailRow { .. } => 96, ArtworkTarget::MyQbzDetailRow { .. } => 96,
_ => DECODE_SIZE, _ => DECODE_SIZE,
} })
} }
} }
+117 -15
View File
@@ -293,6 +293,11 @@ fn seed_tray_appearance(w: &AppWindow, tray: &tray_settings::TraySettings) {
appearance.set_renderer_index(crate::ui_prefs::renderer_index( appearance.set_renderer_index(crate::ui_prefs::renderer_index(
&crate::ui_prefs::load().renderer, &crate::ui_prefs::load().renderer,
)); ));
// Interface-size row (all platforms). Same choke point as the renderer
// row so the dropdown always reflects the persisted value.
appearance.set_ui_scale_index(crate::ui_prefs::ui_scale_index(
&crate::ui_prefs::load().ui_scale,
));
} }
/// Refresh the blacklist count + enabled flag on `BlacklistState` (T10). /// Refresh the blacklist count + enabled flag on `BlacklistState` (T10).
@@ -1097,13 +1102,16 @@ fn install_browser_mouse_nav(window: &AppWindow) {
// (mirrors miniplayer.rs). The startup restore clamps to the // (mirrors miniplayer.rs). The startup restore clamps to the
// monitor; here we just record what the WM settled on. A change // monitor; here we just record what the WM settled on. A change
// guard avoids redundant writes on the many no-op events the WM // guard avoids redundant writes on the many no-op events the WM
// emits. The app minimum is 940x600 (app.slint) — ignore smaller // emits. The app minimum is 940x600 PHYSICAL-equivalent — the
// frames (minimize reports 0x0, mid-transition frames undershoot). // .slint mins divide the interface-size preset out, so the
// LOGICAL minimum scales with it (XL windows sit well below 940
// logical). Ignore smaller frames (minimize reports 0x0,
// mid-transition frames undershoot).
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
let scale = slint_window.scale_factor().max(0.01) as f64; let scale = slint_window.scale_factor().max(0.01) as f64;
let lw = (size.width as f64 / scale) as f32; let lw = (size.width as f64 / scale) as f32;
let lh = (size.height as f64 / scale) as f32; let lh = (size.height as f64 / scale) as f32;
if lw >= 940.0 && lh >= 600.0 { if lw >= 940.0 / active_ui_scale() && lh >= 600.0 / active_ui_scale() {
let mut prefs = crate::ui_prefs::load(); let mut prefs = crate::ui_prefs::load();
if (prefs.window_width - lw).abs() > 0.5 if (prefs.window_width - lw).abs() > 0.5
|| (prefs.window_height - lh).abs() > 0.5 || (prefs.window_height - lh).abs() > 0.5
@@ -5744,11 +5752,48 @@ fn poll_ready<F: std::future::Future>(fut: F) -> Option<F::Output> {
} }
} }
/// Interface-size preset factor active for THIS process run. The persisted
/// pref can change mid-session (Settings dropdown), but the window scale only
/// applies on restart — geometry math must use the factor the window was
/// actually created with, so read this, not the pref.
static ACTIVE_UI_SCALE: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
fn active_ui_scale() -> f32 {
ACTIVE_UI_SCALE.get().copied().unwrap_or(1.0)
}
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
// UI SCALE PRESET — must run FIRST: SLINT_SCALE_FACTOR has to be in the
// environment before the backend/window exist (winit reads it at window
// creation), and `set_var` must run before any thread spawns (the tokio
// runtime below). The env var OVERRIDES the compositor DPR rather than
// multiplying it, so the last observed real DPR is baked into the value
// (see `last_dpr` in ui_prefs). Default preset => the var is NOT set at
// all: stock DPR handling (incl. live monitor changes) stays intact.
let ui_scale_factor = {
let prefs = crate::ui_prefs::load();
let factor = crate::ui_prefs::ui_scale_factor(&prefs.ui_scale);
if factor != 1.0 {
let effective = prefs.last_dpr.max(0.5) * factor;
std::env::set_var("SLINT_SCALE_FACTOR", effective.to_string());
}
let _ = ACTIVE_UI_SCALE.set(factor);
factor
};
// Composite logger: stderr (unchanged) + a bounded in-memory ring + an on-disk // Composite logger: stderr (unchanged) + a bounded in-memory ring + an on-disk
// file, all redacted at the write choke point. Feeds the in-app log viewer and // file, all redacted at the write choke point. Feeds the in-app log viewer and
// the diagnostics bundle. Honours RUST_LOG (default "info"). // the diagnostics bundle. Honours RUST_LOG (default "info").
qbz_log::install("info"); qbz_log::install("info");
if ui_scale_factor != 1.0 {
log::info!(
"[ui-scale] preset factor {ui_scale_factor} -> SLINT_SCALE_FACTOR={}",
std::env::var("SLINT_SCALE_FACTOR").unwrap_or_default()
);
}
// Artwork decode targets grow with the preset so covers stay sharp at
// Large/XL (and shrink RAM at Small). Set before any artwork job runs.
crate::artwork::set_ui_scale_factor(ui_scale_factor);
// Install the rustls process-level CryptoProvider ONCE, before ANY TLS use. // Install the rustls process-level CryptoProvider ONCE, before ANY TLS use.
// The full binary compiles BOTH rustls providers (aws-lc-rs via qbz-cast's // The full binary compiles BOTH rustls providers (aws-lc-rs via qbz-cast's
@@ -5816,11 +5861,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// for a few seconds (renderer/driver init crashes happen at or shortly // for a few seconds (renderer/driver init crashes happen at or shortly
// after realization; surviving this long counts as a working renderer). // after realization; surviving this long counts as a working renderer).
// Single-shot timer — the binding must outlive this scope's run loop. // Single-shot timer — the binding must outlive this scope's run loop.
// Piggybacked: persist the REAL compositor DPR as `last_dpr` (the surface
// is mapped by now, so winit reports the true value — unlike right after
// creation on Wayland). Read from the WINIT window, not the Slint one:
// SLINT_SCALE_FACTOR overrides Slint's factor but winit still sees the
// compositor's. The next scaled launch bakes this into the env value.
let _renderer_sentinel_timer = slint::Timer::default(); let _renderer_sentinel_timer = slint::Timer::default();
let sentinel_weak = window.as_weak();
_renderer_sentinel_timer.start( _renderer_sentinel_timer.start(
slint::TimerMode::SingleShot, slint::TimerMode::SingleShot,
std::time::Duration::from_secs(5), std::time::Duration::from_secs(5),
clear_renderer_sentinel, move || {
clear_renderer_sentinel();
if let Some(w) = sentinel_weak.upgrade() {
w.window().with_winit_window(|win| {
let real_dpr = win.scale_factor() as f32;
let mut prefs = crate::ui_prefs::load();
if real_dpr > 0.1 && (prefs.last_dpr - real_dpr).abs() > 0.01 {
log::info!("[ui-scale] observed compositor DPR {real_dpr} -> persisted");
prefs.last_dpr = real_dpr;
crate::ui_prefs::save(&prefs);
}
});
}
},
); );
// Now that the AppWindow (and its translation global context) exists, switch // Now that the AppWindow (and its translation global context) exists, switch
// the Slint bundled translations to `lang` and reseed the non-reactive // the Slint bundled translations to `lang` and reseed the non-reactive
@@ -5840,6 +5904,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
window window
.global::<ShellState>() .global::<ShellState>()
.set_reduce_motion(!use_gpu_renderer); .set_reduce_motion(!use_gpu_renderer);
// Interface-size preset: publish the factor so `.slint` bindings that must
// stay physically constant (the window minimums) can divide it back out.
// Extra small also gets the font compensation: a plain 0.8 drops body text
// to 12px; boosting Typography tokens ~10% lands at ~13px (readable) while
// layout metrics keep the full 0.8 — that density is the point of XS.
// Small (0.9) needs none: body lands at 13.5px on its own.
window.global::<UiScale>().set_factor(ui_scale_factor);
if ui_scale_factor < 0.85 {
window.global::<Typography>().set_boost(1.1);
}
// Diagnostic for the circle-AA investigation (and the future UI-scale // Diagnostic for the circle-AA investigation (and the future UI-scale
// presets): femtovg's fringe AA halves at fractional scale factors // presets): femtovg's fringe AA halves at fractional scale factors
// (internal dpi = ceil(scale)), so knowing the real factor matters. // (internal dpi = ceil(scale)), so knowing the real factor matters.
@@ -5894,21 +5968,36 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Main window geometry: restore the persisted LOGICAL size + (best-effort) // Main window geometry: restore the persisted LOGICAL size + (best-effort)
// position, each clamped to the current monitor so a smaller / disconnected // position, each clamped to the current monitor so a smaller / disconnected
// display never opens an oversized or stranded window. 0 size = never saved // display never opens an oversized or stranded window. 0 size = never saved
// → keep the `.slint` preferred size. The monitor query is best-effort // → keep the `.slint` preferred size, EXCEPT under a >1 interface-size
// (`with_winit_window` returns None before the surface exists → the WM's own // preset, where even the preferred size may exceed a small monitor once the
// clamping is the fallback, and the Resized handler re-saves the result). // preset multiplies it — clamp it exactly like a restored one. Monitor
if restored_prefs.window_width >= 940.0 && restored_prefs.window_height >= 600.0 { // sizes are divided by the SLINT scale factor (the effective, preset-baked
let mut w = restored_prefs.window_width; // one); winit's own factor is the raw compositor DPR and under-clamps when
let mut h = restored_prefs.window_height; // a preset is active. The minimum is physically constant across presets
// (mirrors the `/ UiScale.factor` bindings in app.slint). The monitor query
// is best-effort (`with_winit_window` returns None before the surface
// exists → the WM's own clamping is the fallback, and the Resized handler
// re-saves the result).
let min_logical_w = 940.0 / ui_scale_factor;
let min_logical_h = 600.0 / ui_scale_factor;
// Plausibility gate for the persisted size: at least the scaled minimum,
// but never stricter than the historical 940x600 (so sizes saved under a
// previous, less-scaled preset still restore — the .slint mins clamp them
// up if the current preset needs more).
let has_saved_size = restored_prefs.window_width >= min_logical_w.min(940.0)
&& restored_prefs.window_height >= min_logical_h.min(600.0);
if has_saved_size || ui_scale_factor > 1.0 {
let mut w = if has_saved_size { restored_prefs.window_width } else { 1180.0 };
let mut h = if has_saved_size { restored_prefs.window_height } else { 760.0 };
let slint_scale = (window.window().scale_factor() as f64).max(0.01);
window.window().with_winit_window(|win| { window.window().with_winit_window(|win| {
if let Some(mon) = win.current_monitor() { if let Some(mon) = win.current_monitor() {
let scale = win.scale_factor().max(0.01); let avail_w = (mon.size().width as f64 / slint_scale) as f32;
let avail_w = (mon.size().width as f64 / scale) as f32; let avail_h = (mon.size().height as f64 / slint_scale) as f32;
let avail_h = (mon.size().height as f64 / scale) as f32; if avail_w >= min_logical_w {
if avail_w >= 940.0 {
w = w.min(avail_w); w = w.min(avail_w);
} }
if avail_h >= 600.0 { if avail_h >= min_logical_h {
h = h.min(avail_h); h = h.min(avail_h);
} }
} }
@@ -8255,6 +8344,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
qbz_i18n::t("Renderer changed — restart QBZ to apply"), qbz_i18n::t("Renderer changed — restart QBZ to apply"),
); );
} }
"ui-scale" => {
// 0 = Extra small, 1 = Small, 2 = Default, 3 = Large,
// 4 = Extra large. Startup-time choice — SLINT_SCALE_FACTOR is
// set at the very top of main() before the backend exists, so
// it applies on the next launch.
let mut prefs = crate::ui_prefs::load();
prefs.ui_scale = crate::ui_prefs::ui_scale_for_index(index).to_string();
crate::ui_prefs::save(&prefs);
crate::toast::info_weak(
&theme_weak,
qbz_i18n::t("Interface size changed — restart QBZ to apply"),
);
}
"language" => { "language" => {
// 0 = Auto, 1 = English, 2 = Español, 3 = Français, 4 = Deutsch, // 0 = Auto, 1 = English, 2 = Español, 3 = Français, 4 = Deutsch,
// 5 = Português, 6 = Русский, 7 = 日本語. Persist the RAW user // 5 = Português, 6 = Русский, 7 = 日本語. Persist the RAW user
+5 -2
View File
@@ -141,7 +141,10 @@ pub fn load(weak: slint::Weak<AppWindow>, handle: tokio::runtime::Handle) {
id: t.track_id.to_string(), id: t.track_id.to_string(),
title: t.title.clone(), title: t.title.clone(),
artist: t.artist.clone(), artist: t.artist.clone(),
cover: crate::artwork::decode_local_pixels(&cover, COVER_DECODE_SIZE), cover: crate::artwork::decode_local_pixels(
&cover,
crate::artwork::scaled_decode(COVER_DECODE_SIZE),
),
}); });
} }
for lt in &lib_rows { for lt in &lib_rows {
@@ -158,7 +161,7 @@ pub fn load(weak: slint::Weak<AppWindow>, handle: tokio::runtime::Handle) {
artist: lt.artist.clone(), artist: lt.artist.clone(),
cover: crate::artwork::decode_local_pixels( cover: crate::artwork::decode_local_pixels(
lt.artwork_path.as_deref().unwrap_or_default(), lt.artwork_path.as_deref().unwrap_or_default(),
COVER_DECODE_SIZE, crate::artwork::scaled_decode(COVER_DECODE_SIZE),
), ),
}); });
} }
+4 -1
View File
@@ -230,7 +230,10 @@ pub async fn rebuild(weak: slint::Weak<AppWindow>) {
), ),
status: album_status, status: album_status,
progress: 0.0, progress: 0.0,
cover: crate::artwork::decode_local_pixels(&cover_path, COVER_DECODE_SIZE), cover: crate::artwork::decode_local_pixels(
&cover_path,
crate::artwork::scaled_decode(COVER_DECODE_SIZE),
),
number: String::new(), number: String::new(),
}); });
for (i, t) in group.iter().enumerate() { for (i, t) in group.iter().enumerate() {
+57
View File
@@ -183,6 +183,41 @@ pub fn renderer_index(key: &str) -> i32 {
} }
} }
/// Interface-size select index (0 = Extra small, 1 = Small, 2 = Default,
/// 3 = Large, 4 = Extra large) -> persisted key. Unknown indices fall back
/// to "default".
pub fn ui_scale_for_index(index: i32) -> &'static str {
match index {
0 => "xs",
1 => "small",
3 => "large",
4 => "xl",
_ => "default",
}
}
/// Inverse: select index for a persisted interface-size key.
pub fn ui_scale_index(key: &str) -> i32 {
match key {
"xs" => 0,
"small" => 1,
"large" => 3,
"xl" => 4,
_ => 2,
}
}
/// Numeric window-scale multiplier for a persisted interface-size key.
pub fn ui_scale_factor(key: &str) -> f32 {
match key {
"xs" => 0.8,
"small" => 0.9,
"large" => 1.2,
"xl" => 1.5,
_ => 1.0,
}
}
/// Miniplayer default-view select index -> persisted key (mirrors the /// Miniplayer default-view select index -> persisted key (mirrors the
/// miniplayer's own reader in `miniplayer.rs`). 0 = "remember". /// miniplayer's own reader in `miniplayer.rs`). 0 = "remember".
pub fn mini_default_view_for_index(index: i32) -> &'static str { pub fn mini_default_view_for_index(index: i32) -> &'static str {
@@ -427,6 +462,18 @@ pub struct UiPrefs {
/// window comes up, the next start reverts this to "auto". /// window comes up, the next start reverts this to "auto".
#[serde(default = "default_renderer")] #[serde(default = "default_renderer")]
pub renderer: String, pub renderer: String,
/// Interface-size preset: `"default"` | `"small"` | `"large"` | `"xl"`.
/// Read at the very top of main() (before ANY thread exists) to set
/// SLINT_SCALE_FACTOR, so changes apply on restart.
#[serde(default = "default_ui_scale")]
pub ui_scale: String,
/// Last observed compositor device-pixel-ratio. SLINT_SCALE_FACTOR
/// OVERRIDES the compositor DPR (it does not multiply), so a scaled
/// launch must bake the real DPR into the env value itself:
/// `env = last_dpr × preset`. Refreshed a few seconds after the window
/// maps (winit reports the real compositor value there).
#[serde(default = "default_last_dpr")]
pub last_dpr: f32,
} }
/// Sentinel for "no saved window position" (let the WM place the window). /// Sentinel for "no saved window position" (let the WM place the window).
@@ -438,6 +485,14 @@ fn default_renderer() -> String {
"auto".to_string() "auto".to_string()
} }
fn default_ui_scale() -> String {
"default".to_string()
}
fn default_last_dpr() -> f32 {
1.0
}
fn default_mini_surface() -> i32 { fn default_mini_surface() -> i32 {
2 2
} }
@@ -566,6 +621,8 @@ impl Default for UiPrefs {
window_x: default_window_pos(), window_x: default_window_pos(),
window_y: default_window_pos(), window_y: default_window_pos(),
renderer: default_renderer(), renderer: default_renderer(),
ui_scale: default_ui_scale(),
last_dpr: default_last_dpr(),
} }
} }
} }