feat(app): a text size setting, kept on this computer

Settings > Appearance offers 90% to 150%, applied as egui's zoom factor
so controls grow with the text. Ctrl+plus and Ctrl+minus already zoomed
through egui; what they settle on is now kept too, so Settings shows it
and the next launch opens at it. It lives in the desktop-only settings:
what reads well depends on the screen, not on the viewer's other
devices.
This commit is contained in:
nvrl committed 2026-10-13 16:29:42 +02:00
1 parent 31bdd75559
commit 47c482498b
3 files changed
+64 -3

No files matched your search

+13 -1
View File
@@ -715,6 +715,7 @@ impl App {
let (engine, events) = Engine::new(runtime, dirs, cc.egui_ctx.clone())?;
theme::install_fonts(&cc.egui_ctx);
theme::apply(&cc.egui_ctx, engine.appearance());
cc.egui_ctx.set_zoom_factor(engine.desktop_prefs().zoom());
// Paint from the catalog immediately; the network catches up. A library
// that was crawled yesterday is on screen before the shares open.
@@ -1829,7 +1830,10 @@ impl App {
self.note(ctx, text, false);
}
}
Action::SetDesktopPrefs(prefs) => self.engine.set_desktop_prefs(prefs),
Action::SetDesktopPrefs(prefs) => {
ctx.set_zoom_factor(prefs.zoom());
self.engine.set_desktop_prefs(prefs);
}
Action::SetTitleWatched { key, watched } => {
let Some(title) = self.library.get(&key) else {
return;
@@ -2101,6 +2105,14 @@ impl eframe::App for App {
let ctx = ui.ctx().clone();
let ctx = &ctx;
self.pump(ctx, frame);
// Ctrl+plus and Ctrl+minus zoom too, through egui: keep what they
// settled on, so Settings shows it and the next launch opens at it.
let mut desktop = self.engine.desktop_prefs();
if (ctx.zoom_factor() - desktop.zoom()).abs() > 0.005 {
desktop.text_size = ((ctx.zoom_factor() * 100.0).round() as u16).clamp(50, 200);
ctx.set_zoom_factor(desktop.zoom());
self.engine.set_desktop_prefs(desktop);
}
// The window manager can leave fullscreen without asking (F11, a
// double-click on a title bar), and `F` then toggled the wrong way.
if let Some(actual) = ctx.input(|input| input.viewport().fullscreen) {
+23
View File
@@ -11,6 +11,9 @@ use serde::{Deserialize, Serialize};
/// What the settings page offers for the streaming cache, in GiB.
pub const CACHE_CHOICES: [u32; 5] = [1, 2, 4, 8, 16];
/// What the settings page offers for the size of everything, in percent.
pub const TEXT_SIZE_CHOICES: [u16; 5] = [90, 100, 115, 130, 150];
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
// A file from an older version, or edited by hand, missing a field: the rest
// still counts, and the missing one takes its default.
@@ -27,6 +30,9 @@ pub struct DesktopPrefs {
pub grouped: bool,
/// The library's offer of posters and synopses was turned down.
pub metadata_offer_dismissed: bool,
/// How large text and controls are drawn, in percent. Per computer, not
/// synced: what reads well depends on the screen and how far away it is.
pub text_size: u16,
}
impl Default for DesktopPrefs {
@@ -40,11 +46,17 @@ impl Default for DesktopPrefs {
// of the same picture otherwise.
grouped: true,
metadata_offer_dismissed: false,
text_size: 100,
}
}
}
impl DesktopPrefs {
/// [`Self::text_size`] as egui's zoom factor, kept to what is offered.
pub fn zoom(&self) -> f32 {
f32::from(self.text_size.clamp(50, 200)) / 100.0
}
pub fn cache_bytes(&self) -> u64 {
u64::from(self.cache_gib.clamp(1, 64)) << 30
}
@@ -105,6 +117,17 @@ mod tests {
assert!(prefs.hardware_decoding);
}
#[test]
fn a_text_size_from_an_older_file_is_full_size_and_a_wild_one_is_kept_in_range() {
let prefs: DesktopPrefs = serde_json::from_str(r#"{ "cache_gib": 8 }"#).unwrap();
assert_eq!(prefs.zoom(), 1.0);
let wild = DesktopPrefs {
text_size: 900,
..DesktopPrefs::default()
};
assert_eq!(wild.zoom(), 2.0);
}
#[test]
fn an_unparseable_file_is_never_overwritten() {
let path = std::env::temp_dir().join(format!("pstr-desktop-{}.json", std::process::id()));
+28 -2
View File
@@ -56,7 +56,7 @@ pub fn show(
ui.vertical(|ui| {
ui.set_width(width);
ui.add_space(theme::space::M);
appearance_form(ui, prefs.appearance, actions);
appearance_form(ui, prefs.appearance, prefs.desktop, actions);
playback_form(ui, prefs, actions);
storage_form(ui, prefs, actions);
metadata_form(ui, settings, api_key, actions);
@@ -71,7 +71,12 @@ pub fn show(
/// Every control here repaints the whole window the moment it is clicked —
/// there is no "apply" — because the preview *is* the app. A swatch row that
/// only changed a small square would be asking someone to imagine the result.
fn appearance_form(ui: &mut egui::Ui, appearance: Appearance, actions: &mut Vec<Action>) {
fn appearance_form(
ui: &mut egui::Ui,
appearance: Appearance,
desktop: DesktopPrefs,
actions: &mut Vec<Action>,
) {
ui::widgets::settings_group(ui, "Appearance", None, |ui| {
ui.add_space(theme::space::L);
ui.label(theme::Role::Body.rich("Palette").color(theme::text()));
@@ -108,6 +113,27 @@ fn appearance_form(ui: &mut egui::Ui, appearance: Appearance, actions: &mut Vec<
}
});
ui::widgets::settings_row(
ui,
"Text size",
Some("Everything grows with it. Kept on this computer only."),
|ui| {
let choices: Vec<(u16, String)> = TEXT_SIZE_CHOICES
.iter()
.map(|&percent| (percent, format!("{percent}%")))
.collect();
let labels: Vec<(u16, &str)> = choices
.iter()
.map(|(percent, label)| (*percent, label.as_str()))
.collect();
if let Some(text_size) = ui::widgets::segmented(ui, desktop.text_size, &labels) {
actions.push(Action::SetDesktopPrefs(DesktopPrefs {
text_size,
..desktop
}));
}
},
);
ui::widgets::settings_row(
ui,
"Library tiles",