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.
This commit is contained in:
nvrl committed 2026-10-27 22:44:35 +01:00
1 parent 69d961d7b8
commit ffd6e959bb
6 files changed
+78 -29

No files matched your search

+43 -2
View File
@@ -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));
}
}
+4 -4
View File
@@ -432,14 +432,14 @@ fn languages(ui: &mut egui::Ui, playback: &PlaybackPrefs, actions: &mut Vec<Acti
/// How subtitles look, with a sample line to judge it by.
///
/// Every change reaches a playing file as it is made, so the film itself is
/// the other preview. A styled track takes only the size and the position
/// unless it is asked to take the rest; see [`SubtitleStyle`].
/// the other preview. A styled track takes none of it unless it is asked to;
/// see [`SubtitleStyle`].
fn subtitle_form(ui: &mut egui::Ui, style: &SubtitleStyle, actions: &mut Vec<Action>) {
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<Act
ui,
"Restyle styled subtitles",
Some(
"Repaint styled (ASS) dialogue in this style too. Signs, songs and karaoke can come out wrong.",
"Draw styled (ASS) dialogue in this style too, size and position included. Signs, songs and karaoke can come out wrong.",
),
|ui| {
ui::widgets::toggle(ui, &mut chosen.restyle_styled, "");
+6 -4
View File
@@ -70,8 +70,9 @@ pub struct PlaybackPrefs {
/// Every default is mpv's own, so a viewer who never opens the section sees
/// what any mpv shows. Plain (SRT) tracks take all of it. A styled (ASS) track
/// was typeset by somebody, its signs placed and coloured by hand, and painting
/// over that undoes the work — so by default it takes only the size and the
/// position, and the rest reaches it only with [`Self::restyle_styled`].
/// over that undoes the work — so by default it takes none of this, and moves
/// only to stay clear of the player's controls. All of it reaches a styled
/// track only with [`Self::restyle_styled`].
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct SubtitleStyle {
@@ -100,8 +101,9 @@ pub struct SubtitleStyle {
/// Where the line sits, as a percentage of the picture's height from the
/// top: 100 is the bottom edge, mpv's usual place.
pub position: f64,
/// Whether styled (ASS) tracks are repainted in this style too. Off by
/// default: it turns signs and karaoke into dialogue-looking text.
/// Whether styled (ASS) tracks are drawn in this style too, size and
/// position included. Off by default: it turns signs and karaoke into
/// dialogue-looking text.
pub restyle_styled: bool,
}
+18 -14
View File
@@ -619,21 +619,19 @@ const BORDER_STYLE: &str = "sub-border-style";
/// The mpv options that draw `style`, every one of them every time, so that
/// changing a value back to its default while playing lands too.
///
/// `sub-ass-override` is `scale` unless the viewer asked otherwise: a styled
/// track then takes the size and keeps its own fonts, colours and placement.
/// `force` repaints it whole. See [`SubtitleStyle`] for why that is opt-in.
/// `sub-ass-override` is `yes` unless the viewer asked otherwise, so a styled
/// track keeps its own size, fonts and colours. Not mpv's default, `scale`,
/// which hands it `sub-scale` as well. `force` repaints it whole. See
/// [`SubtitleStyle`] for why that is opt-in. `sub-pos` reaches a styled track
/// under `yes` too, so where a styled track sits is the caller's to decide:
/// see `pstr-app`'s `Playback::lift_subtitles`.
/// The border options come last, led by [`BORDER_STYLE`].
pub fn subtitle_options(style: &SubtitleStyle) -> 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()
+2 -2
View File
@@ -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.
+5 -3
View File
@@ -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**