Desktop: global UI scale (egui zoom_factor), persisted
The card-density slider only resized the channel-grid cards — the sidebar, settings, detail panel, and all text stayed fixed. Add a real global scale that grows/shrinks the *entire* UI crisply. egui already handles Ctrl + / Ctrl - / Ctrl 0 via zoom_factor (it re-rasterizes fonts rather than bitmap-stretching), but it wasn't persisted and had no visible control. Now: - New `[ui] ui_scale` config (default 1.0), applied with set_zoom_factor at startup. - update() mirrors ctx.zoom_factor() back into config each frame and saves on a 500ms debounce, so both the slider and the built-in keyboard zoom persist across restarts without writing config.toml every frame of a drag. - A "UI scale" slider (0.5–3.0×) + Reset button in the Settings screen, driving set_zoom_factor live. The card-density "Size:" slider stays as a separate grid-density preference (it composes with the global zoom). 95 tests pass; GUI verified running with the per-frame zoom-sync path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fd9063b5cb
commit
b8a742535b
2 changed files with 61 additions and 0 deletions
52
src/app.rs
52
src/app.rs
|
|
@ -138,6 +138,10 @@ pub struct App {
|
||||||
plex_status: String,
|
plex_status: String,
|
||||||
db: Database,
|
db: Database,
|
||||||
card_density: f32,
|
card_density: f32,
|
||||||
|
/// When set, the global UI zoom changed and config should be saved at
|
||||||
|
/// this instant (debounced so a slider drag / key-repeat doesn't write
|
||||||
|
/// config.toml every frame). Synced from `ctx.zoom_factor()` in update().
|
||||||
|
scale_save_at: Option<Instant>,
|
||||||
sort_mode: SortMode,
|
sort_mode: SortMode,
|
||||||
watched: HashSet<String>,
|
watched: HashSet<String>,
|
||||||
/// Per-video flag sets (bookmark/favourite/waiting/archive). Loaded from
|
/// Per-video flag sets (bookmark/favourite/waiting/archive). Loaded from
|
||||||
|
|
@ -283,6 +287,10 @@ impl App {
|
||||||
};
|
};
|
||||||
|
|
||||||
theme::apply(&cc.egui_ctx, &config.ui.theme);
|
theme::apply(&cc.egui_ctx, &config.ui.theme);
|
||||||
|
// Restore the persisted global UI zoom. egui's built-in Ctrl +/-/0
|
||||||
|
// also drives zoom_factor; update() keeps config.ui.ui_scale synced
|
||||||
|
// and persists changes so the size survives restarts.
|
||||||
|
cc.egui_ctx.set_zoom_factor(config.ui.ui_scale.clamp(0.5, 3.0));
|
||||||
|
|
||||||
let channels_root = config.backup.directory.clone();
|
let channels_root = config.backup.directory.clone();
|
||||||
let settings_dir = channels_root.display().to_string();
|
let settings_dir = channels_root.display().to_string();
|
||||||
|
|
@ -433,6 +441,7 @@ impl App {
|
||||||
plex_status: String::new(),
|
plex_status: String::new(),
|
||||||
db,
|
db,
|
||||||
card_density: 1.0,
|
card_density: 1.0,
|
||||||
|
scale_save_at: None,
|
||||||
sort_mode: SortMode::Title,
|
sort_mode: SortMode::Title,
|
||||||
watched,
|
watched,
|
||||||
flags,
|
flags,
|
||||||
|
|
@ -2450,6 +2459,29 @@ impl App {
|
||||||
});
|
});
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("UI scale:");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Drive zoom_factor directly; update() mirrors it
|
||||||
|
// back into config.ui.ui_scale and persists it.
|
||||||
|
let mut scale = ctx.zoom_factor();
|
||||||
|
let resp = ui.add(
|
||||||
|
egui::Slider::new(&mut scale, 0.5_f32..=3.0_f32)
|
||||||
|
.step_by(0.1)
|
||||||
|
.suffix("×"),
|
||||||
|
);
|
||||||
|
if resp.changed() {
|
||||||
|
ctx.set_zoom_factor(scale);
|
||||||
|
}
|
||||||
|
if ui.button("Reset").on_hover_text("Back to 1.0× (Ctrl+0)").clicked() {
|
||||||
|
ctx.set_zoom_factor(1.0);
|
||||||
|
}
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new("scales the whole UI · also Ctrl + / Ctrl - / Ctrl 0")
|
||||||
|
.small().weak(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("Minimize to tray:");
|
ui.label("Minimize to tray:");
|
||||||
ui.checkbox(&mut self.config.ui.minimize_to_tray, "hide window on close instead of quitting")
|
ui.checkbox(&mut self.config.ui.minimize_to_tray, "hide window on close instead of quitting")
|
||||||
.on_hover_text("Requires a working StatusNotifier host (KDE native, GNOME with AppIndicator extension). Ctrl+Q always quits.");
|
.on_hover_text("Requires a working StatusNotifier host (KDE native, GNOME with AppIndicator extension). Ctrl+Q always quits.");
|
||||||
|
|
@ -3581,6 +3613,26 @@ impl App {
|
||||||
|
|
||||||
impl eframe::App for App {
|
impl eframe::App for App {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
// ── Global UI zoom sync + persistence ───────────────────────────
|
||||||
|
// egui's built-in Ctrl +/-/0 (and our Settings slider) mutate
|
||||||
|
// ctx.zoom_factor() directly. Mirror it into config and persist on
|
||||||
|
// a short debounce so the chosen scale survives restarts without
|
||||||
|
// writing config.toml on every frame of a slider drag / key repeat.
|
||||||
|
let zoom = ctx.zoom_factor();
|
||||||
|
if (zoom - self.config.ui.ui_scale).abs() > f32::EPSILON {
|
||||||
|
self.config.ui.ui_scale = zoom;
|
||||||
|
self.scale_save_at =
|
||||||
|
Some(Instant::now() + std::time::Duration::from_millis(500));
|
||||||
|
// Ensure the debounce fires even if the UI would otherwise idle.
|
||||||
|
ctx.request_repaint_after(std::time::Duration::from_millis(550));
|
||||||
|
}
|
||||||
|
if let Some(at) = self.scale_save_at {
|
||||||
|
if Instant::now() >= at {
|
||||||
|
let _ = self.config.save(&self.config_path);
|
||||||
|
self.scale_save_at = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── System-tray event drain ─────────────────────────────────────
|
// ── System-tray event drain ─────────────────────────────────────
|
||||||
// Tray menu activations arrive on a background-thread channel.
|
// Tray menu activations arrive on a background-thread channel.
|
||||||
// Drain them all each frame and translate into viewport commands.
|
// Drain them all each frame and translate into viewport commands.
|
||||||
|
|
|
||||||
|
|
@ -161,13 +161,22 @@ pub struct UiSection {
|
||||||
/// hidden window.
|
/// hidden window.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub minimize_to_tray: bool,
|
pub minimize_to_tray: bool,
|
||||||
|
/// Global egui zoom factor — scales the *entire* desktop UI (fonts,
|
||||||
|
/// spacing, widgets), not just the card grid. 1.0 = native. Persisted
|
||||||
|
/// here so the chosen size survives restarts and is kept in sync with
|
||||||
|
/// the built-in Ctrl + / Ctrl - / Ctrl 0 keyboard zoom.
|
||||||
|
#[serde(default = "default_ui_scale")]
|
||||||
|
pub ui_scale: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_ui_scale() -> f32 { 1.0 }
|
||||||
|
|
||||||
impl Default for UiSection {
|
impl Default for UiSection {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
theme: default_theme(),
|
theme: default_theme(),
|
||||||
minimize_to_tray: false,
|
minimize_to_tray: false,
|
||||||
|
ui_scale: default_ui_scale(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue