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
|
//! 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 "Очень маленький"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user