From ffd6e959bbac092a2b50554cfe8f0693bf9b821e Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 27 Oct 2026 22:44:35 +0100 Subject: [PATCH] fix(core,player,app): styled subtitles keep their size and place unless restyled mpv's default sub-ass-override, `scale`, hands an ASS track sub-scale, and sub-pos reaches it under every mode but `no`. So with "Restyle styled subtitles" off, the subtitle size and position settings still resized and moved typeset dialogue and signs. The override is now `yes` when restyling is off, which keeps sub-scale off ASS. A styled track also ignores the style's position, and moves only to stay clear of the player's controls. A frame from an ASS track rendered with every other option changed now matches the untouched one pixel for pixel. Plain (SRT) tracks still take the whole style. --- crates/pstr-app/src/playback.rs | 45 ++++++++++++++++++++++++++++-- crates/pstr-app/src/ui/settings.rs | 8 +++--- crates/pstr-core/src/prefs.rs | 10 ++++--- crates/pstr-player/src/player.rs | 32 +++++++++++---------- docs/FAQ.md | 4 +-- docs/USER_GUIDE.md | 8 ++++-- 6 files changed, 78 insertions(+), 29 deletions(-) diff --git a/crates/pstr-app/src/playback.rs b/crates/pstr-app/src/playback.rs index d2d0e8a..168c7aa 100644 --- a/crates/pstr-app/src/playback.rs +++ b/crates/pstr-app/src/playback.rs @@ -302,6 +302,11 @@ impl Playback { } else { 100.0 }; + self.place_subtitles(); + } + + /// Move subtitles to [`Self::subtitle_position`], when that has changed. + fn place_subtitles(&mut self) { let position = self.subtitle_position(); if position != self.subtitle_position { self.subtitle_position = position; @@ -311,8 +316,21 @@ impl Playback { /// Where subtitles go: where the style puts them, unless the controls /// would cover that, in which case just above the controls. + /// + /// A styled track ignores the style's position unless it is restyled: + /// mpv applies `sub-pos` to it either way, and its signs were placed by + /// hand. The lift still applies, because a line under the seek bar cannot + /// be read however carefully it was placed. fn subtitle_position(&self) -> f64 { - self.subtitle_style.position.min(self.lifted_position) + let styled = self + .selected_track(TrackKind::Subtitle) + .is_some_and(|track| is_styled(track.codec.as_deref())); + let position = if styled && !self.subtitle_style.restyle_styled { + 100.0 + } else { + self.subtitle_style.position + }; + position.min(self.lifted_position) } /// Restyle the subtitles of this file, now. @@ -409,7 +427,11 @@ impl Playback { PlayerEvent::FileLoaded => self.loaded = true, PlayerEvent::Volume(volume) => self.volume = *volume, PlayerEvent::Muted(muted) => self.muted = *muted, - PlayerEvent::Tracks(tracks) => self.tracks = tracks.clone(), + PlayerEvent::Tracks(tracks) => { + self.tracks = tracks.clone(); + // A switch between a plain and a styled track moves the line. + self.place_subtitles(); + } PlayerEvent::Chapters(chapters) => { self.chapters = chapters.clone(); self.reclassify(); @@ -516,6 +538,11 @@ fn build_player( Ok((Arc::new(player), None)) } +/// Whether a subtitle codec carries its own styling, as mpv names them. +fn is_styled(codec: Option<&str>) -> bool { + matches!(codec, Some("ass" | "ssa")) +} + /// The player defaults with the viewer's preferences folded in. fn from_prefs( prefs: &PlaybackPrefs, @@ -863,3 +890,17 @@ fn save( watch_state(position, duration, watched), ); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_ass_and_ssa_tracks_count_as_styled() { + assert!(is_styled(Some("ass"))); + assert!(is_styled(Some("ssa"))); + assert!(!is_styled(Some("subrip"))); + assert!(!is_styled(Some("hdmv_pgs_subtitle"))); + assert!(!is_styled(None)); + } +} diff --git a/crates/pstr-app/src/ui/settings.rs b/crates/pstr-app/src/ui/settings.rs index ceb0035..64f92dd 100644 --- a/crates/pstr-app/src/ui/settings.rs +++ b/crates/pstr-app/src/ui/settings.rs @@ -432,14 +432,14 @@ fn languages(ui: &mut egui::Ui, playback: &PlaybackPrefs, actions: &mut Vec) { ui::widgets::settings_group( ui, "Subtitles", Some( - "Changes show on the playing video straight away. Plain subtitles (SRT) take all of this; styled ones (ASS) only the size and the position, unless you restyle them below.", + "Changes show on the playing video straight away. Plain subtitles (SRT) take all of this; styled ones (ASS) keep their own look unless you restyle them below.", ), |ui| { let mut chosen = style.clone(); @@ -532,7 +532,7 @@ fn subtitle_form(ui: &mut egui::Ui, style: &SubtitleStyle, actions: &mut Vec Vec<(&'static str, String)> { let flag = |on: bool| if on { "yes" } else { "no" }.to_owned(); let mut options = vec![ ( "sub-ass-override", - if style.restyle_styled { - "force" - } else { - "scale" - } - .to_owned(), + if style.restyle_styled { "force" } else { "yes" }.to_owned(), ), ("sub-scale", format!("{:.2}", style.scale)), ( @@ -723,9 +721,10 @@ mod tests { #[test] fn the_default_subtitle_style_is_what_mpv_would_do_anyway() { let options = subtitle_options(&SubtitleStyle::default()); - // mpv 0.41's own defaults, from `mpv --list-options`. + // mpv 0.41's own defaults, from `mpv --list-options` — all but + // `sub-ass-override`, which mpv has at `scale`. for (name, value) in [ - ("sub-ass-override", "scale"), + ("sub-ass-override", "yes"), ("sub-scale", "1.00"), ("sub-font", "sans-serif"), ("sub-bold", "no"), @@ -756,9 +755,14 @@ mod tests { } #[test] - fn styled_subtitles_are_repainted_only_when_asked() { - let kept = subtitle_options(&SubtitleStyle::default()); - assert_eq!(option(&kept, "sub-ass-override"), Some("scale")); + fn styled_subtitles_are_repainted_or_resized_only_when_asked() { + // `scale` would hand a styled track the size; `yes` hands it nothing + // but the `sub-ass-*` options, none of which this sets. + let kept = subtitle_options(&SubtitleStyle { + scale: 1.5, + ..SubtitleStyle::default() + }); + assert_eq!(option(&kept, "sub-ass-override"), Some("yes")); let repainted = subtitle_options(&SubtitleStyle { restyle_styled: true, ..SubtitleStyle::default() diff --git a/docs/FAQ.md b/docs/FAQ.md index 7a1eee4..3d39179 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -25,8 +25,8 @@ with hardware decoding where the device has it. Yes. In **Settings → Subtitles** you can set the size, position, font, colours, outline, box, shadow and blur. A **Box** background is easiest to read on bright scenes. Styled subtitles (ASS) -keep the look their makers gave them and take only the size and position, unless you turn on -**Restyle styled subtitles**. +keep the look, size and place their makers gave them, unless you turn on **Restyle styled +subtitles**. ## The subtitles are early or late. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index ac313d7..fb24f91 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -86,7 +86,8 @@ The picture plays inside the app's window. Move the mouse to show the controls. **Play now**, **Cancel** or **Watch till the end**. - **Subtitles and audio** are chosen from the buttons at the bottom right. The app remembers your choice for each show. The subtitle menu also has **Size** and **Position**, which are saved like - the ones in Settings, and **Delay**, which moves subtitles earlier or later for this file only. + the ones in Settings and change styled subtitles only when they are restyled, and **Delay**, + which moves subtitles earlier or later for this file only. - **Speed** goes from 0.5× to 3×, and stays for the next episode. ### Keyboard @@ -202,8 +203,9 @@ a video that is playing changes at once. read on bright scenes. - **Shadow** and **Blur**. - **Restyle styled subtitles** (off). Plain subtitles (SRT) always take the whole style. Styled - subtitles (ASS) keep the look their makers gave them and take only the size and position, - unless this is on. Turned on, signs and songs can come out wrong. + subtitles (ASS) keep the look, size and place their makers gave them unless this is on; they + only move up while the player's controls are showing. Turned on, signs and songs can come out + wrong. - **Reset** goes back to the default look. **Storage**