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:
17 files changed
+385
-43
No files matched your search
@@ -2,6 +2,11 @@
|
||||
//! single entry point. Translations are bundled (pure-Rust, no C dep) from
|
||||
//! `translations/<lang>/LC_MESSAGES/qbz-slint.po`; msgid = English source, no context.
|
||||
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()
|
||||
.with_bundled_translations("translations")
|
||||
.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."
|
||||
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"
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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 "Очень маленький"
|
||||
@@ -17,16 +17,19 @@ import { SplashScreen } from "splash/SplashScreen.slint";
|
||||
import { LoginScreen } from "login/LoginScreen.slint";
|
||||
import { AppShell } from "shell/AppShell.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";
|
||||
|
||||
// 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.
|
||||
export { Theme, ThemeColors } from "foundation/semantic-colors.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.
|
||||
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
|
||||
// restores a saved session, then resolves to `shell` or `login`.
|
||||
@@ -41,8 +44,11 @@ export component AppWindow inherits Window {
|
||||
: "QBZ";
|
||||
preferred-width: 1180px;
|
||||
preferred-height: 760px;
|
||||
min-width: 940px;
|
||||
min-height: 600px;
|
||||
// Divide the interface-size preset back out so the PHYSICAL minimum stays
|
||||
// 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;
|
||||
|
||||
// Resolved by the Rust layer from the desktop environment (e.g. the
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
// Typography scale — font sizes and weights.
|
||||
|
||||
export global Typography {
|
||||
out property <length> legal: 13px;
|
||||
out property <length> link: 14px;
|
||||
out property <length> body: 15px;
|
||||
out property <length> subtitle: 15px;
|
||||
out property <length> button: 17px;
|
||||
// Small-preset font compensation. The interface-size preset scales the
|
||||
// whole window (SLINT_SCALE_FACTOR), so at Small (0.8) body text would
|
||||
// drop to 12px — too small. Rust seeds `boost = 1.1` for that preset
|
||||
// only (net body 15 × 0.8 × 1.1 ≈ 13.2px, readable); every other preset
|
||||
// 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
|
||||
// Releases", ...) and the hero titles on Album / Artist / Playlist
|
||||
// / Mixtape detail pages. One size for all, for consistency.
|
||||
out property <length> section: 18px;
|
||||
out property <length> heading: 19px;
|
||||
out property <length> title: 25px;
|
||||
out property <length> wordmark: 29px;
|
||||
out property <length> section: 18px * boost;
|
||||
out property <length> heading: 19px * boost;
|
||||
out property <length> title: 25px * boost;
|
||||
out property <length> wordmark: 29px * boost;
|
||||
|
||||
out property <int> regular: 400;
|
||||
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
|
||||
// for now). Code kept commented for re-enable; do NOT delete.
|
||||
/*
|
||||
// Interface size — the working replacement for the old percentage "UI
|
||||
// Scale" placeholder that was parked here (commented, unwired). Window
|
||||
// scale-factor preset via SLINT_SCALE_FACTOR (main.rs); restart to apply.
|
||||
SettingRow {
|
||||
unsupported: true;
|
||||
label: @tr("UI Scale");
|
||||
label: @tr("Interface size");
|
||||
description: @tr("Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)");
|
||||
QbzSelect {
|
||||
menu-width: 140px;
|
||||
options: AppearanceState.ui-scales;
|
||||
menu-width: 200px;
|
||||
options: AppearanceState.ui-scale-options;
|
||||
current-index: AppearanceState.ui-scale-index;
|
||||
selected(i) => {
|
||||
AppearanceState.ui-scale-index = i;
|
||||
@@ -283,7 +283,6 @@ export component AppearanceSettings inherits VerticalLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
SettingRow {
|
||||
label: @tr("Immersive search");
|
||||
|
||||
@@ -2904,12 +2904,14 @@ export global AppearanceState {
|
||||
in-out property <string> auto-theme-detected-de: "";
|
||||
|
||||
// --- 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 <int> language-index: 0;
|
||||
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 <[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
|
||||
// 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 <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 ---------------------------------------------------------
|
||||
in-out property <[string]> immersive-views: [
|
||||
@tr("Remember last used"), @tr("Coverflow"), @tr("Static"), @tr("Vinyl"), @tr("Visualizer"),
|
||||
@@ -3619,6 +3628,15 @@ export global LocationViewActions {
|
||||
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.
|
||||
// Globals are imported directly by the shell components, so toggles
|
||||
// need no callback plumbing.
|
||||
|
||||
@@ -23,6 +23,22 @@ const MAX_CONCURRENT: usize = 16;
|
||||
/// modest DPI without holding full ~600px source textures in memory.
|
||||
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).
|
||||
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
|
||||
/// hundreds of MB. Decode row thumbnails small.
|
||||
fn decode_size(&self) -> u32 {
|
||||
match self {
|
||||
scaled_decode(match self {
|
||||
ArtworkTarget::SearchTrack { .. }
|
||||
| ArtworkTarget::FavoriteTrack { .. }
|
||||
| ArtworkTarget::MixTrack { .. }
|
||||
@@ -261,7 +277,7 @@ impl ArtworkTarget {
|
||||
// Detail list-row thumbnails render at 36px.
|
||||
ArtworkTarget::MyQbzDetailRow { .. } => 96,
|
||||
_ => DECODE_SIZE,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+117
-15
@@ -293,6 +293,11 @@ fn seed_tray_appearance(w: &AppWindow, tray: &tray_settings::TraySettings) {
|
||||
appearance.set_renderer_index(crate::ui_prefs::renderer_index(
|
||||
&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).
|
||||
@@ -1097,13 +1102,16 @@ fn install_browser_mouse_nav(window: &AppWindow) {
|
||||
// (mirrors miniplayer.rs). The startup restore clamps to the
|
||||
// monitor; here we just record what the WM settled on. A change
|
||||
// guard avoids redundant writes on the many no-op events the WM
|
||||
// emits. The app minimum is 940x600 (app.slint) — ignore smaller
|
||||
// frames (minimize reports 0x0, mid-transition frames undershoot).
|
||||
// emits. The app minimum is 940x600 PHYSICAL-equivalent — the
|
||||
// .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) => {
|
||||
let scale = slint_window.scale_factor().max(0.01) as f64;
|
||||
let lw = (size.width 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();
|
||||
if (prefs.window_width - lw).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>> {
|
||||
// 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
|
||||
// file, all redacted at the write choke point. Feeds the in-app log viewer and
|
||||
// the diagnostics bundle. Honours RUST_LOG (default "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.
|
||||
// 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
|
||||
// after realization; surviving this long counts as a working renderer).
|
||||
// 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 sentinel_weak = window.as_weak();
|
||||
_renderer_sentinel_timer.start(
|
||||
slint::TimerMode::SingleShot,
|
||||
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
|
||||
// the Slint bundled translations to `lang` and reseed the non-reactive
|
||||
@@ -5840,6 +5904,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
window
|
||||
.global::<ShellState>()
|
||||
.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
|
||||
// presets): femtovg's fringe AA halves at fractional scale factors
|
||||
// (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)
|
||||
// position, each clamped to the current monitor so a smaller / disconnected
|
||||
// display never opens an oversized or stranded window. 0 size = never saved
|
||||
// → keep the `.slint` preferred size. 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).
|
||||
if restored_prefs.window_width >= 940.0 && restored_prefs.window_height >= 600.0 {
|
||||
let mut w = restored_prefs.window_width;
|
||||
let mut h = restored_prefs.window_height;
|
||||
// → keep the `.slint` preferred size, EXCEPT under a >1 interface-size
|
||||
// preset, where even the preferred size may exceed a small monitor once the
|
||||
// preset multiplies it — clamp it exactly like a restored one. Monitor
|
||||
// sizes are divided by the SLINT scale factor (the effective, preset-baked
|
||||
// one); winit's own factor is the raw compositor DPR and under-clamps when
|
||||
// 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| {
|
||||
if let Some(mon) = win.current_monitor() {
|
||||
let scale = win.scale_factor().max(0.01);
|
||||
let avail_w = (mon.size().width as f64 / scale) as f32;
|
||||
let avail_h = (mon.size().height as f64 / scale) as f32;
|
||||
if avail_w >= 940.0 {
|
||||
let avail_w = (mon.size().width as f64 / slint_scale) as f32;
|
||||
let avail_h = (mon.size().height as f64 / slint_scale) as f32;
|
||||
if avail_w >= min_logical_w {
|
||||
w = w.min(avail_w);
|
||||
}
|
||||
if avail_h >= 600.0 {
|
||||
if avail_h >= min_logical_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"),
|
||||
);
|
||||
}
|
||||
"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" => {
|
||||
// 0 = Auto, 1 = English, 2 = Español, 3 = Français, 4 = Deutsch,
|
||||
// 5 = Português, 6 = Русский, 7 = 日本語. Persist the RAW user
|
||||
|
||||
@@ -141,7 +141,10 @@ pub fn load(weak: slint::Weak<AppWindow>, handle: tokio::runtime::Handle) {
|
||||
id: t.track_id.to_string(),
|
||||
title: t.title.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 {
|
||||
@@ -158,7 +161,7 @@ pub fn load(weak: slint::Weak<AppWindow>, handle: tokio::runtime::Handle) {
|
||||
artist: lt.artist.clone(),
|
||||
cover: crate::artwork::decode_local_pixels(
|
||||
lt.artwork_path.as_deref().unwrap_or_default(),
|
||||
COVER_DECODE_SIZE,
|
||||
crate::artwork::scaled_decode(COVER_DECODE_SIZE),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -230,7 +230,10 @@ pub async fn rebuild(weak: slint::Weak<AppWindow>) {
|
||||
),
|
||||
status: album_status,
|
||||
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(),
|
||||
});
|
||||
for (i, t) in group.iter().enumerate() {
|
||||
|
||||
@@ -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's own reader in `miniplayer.rs`). 0 = "remember".
|
||||
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".
|
||||
#[serde(default = "default_renderer")]
|
||||
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).
|
||||
@@ -438,6 +485,14 @@ fn default_renderer() -> String {
|
||||
"auto".to_string()
|
||||
}
|
||||
|
||||
fn default_ui_scale() -> String {
|
||||
"default".to_string()
|
||||
}
|
||||
|
||||
fn default_last_dpr() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
fn default_mini_surface() -> i32 {
|
||||
2
|
||||
}
|
||||
@@ -566,6 +621,8 @@ impl Default for UiPrefs {
|
||||
window_x: default_window_pos(),
|
||||
window_y: default_window_pos(),
|
||||
renderer: default_renderer(),
|
||||
ui_scale: default_ui_scale(),
|
||||
last_dpr: default_last_dpr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user