mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
fix(app): the subtitle preview is drawn in the chosen font
The preview drew every font in the app's own Inter, so picking one changed nothing on screen. On Linux it now asks fontconfig for the face libass would use — the family with the weight and slant wanted, or fontconfig's fallback when it is not installed — loads it on a thread of its own and draws the sample in it. A bold or italic the face lacks is made up, as libass does. Windows and macOS have no fc-match and keep the app's font, as the hint there still says. The CJK fallback and the subtitle face are now both kept, so installing one no longer drops the other.
This commit is contained in:
1 parent
ffd6e959bb
commit
648c35e92a
5 files changed
+255
-27
No files matched your search
+214
-15
@@ -186,9 +186,48 @@ pub fn semibold() -> egui::FontFamily {
|
|||||||
egui::FontFamily::Name("semibold".into())
|
egui::FontFamily::Name("semibold".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The family the subtitle preview is drawn in: the face the chosen subtitle
|
||||||
|
/// font resolves to, ahead of the app's own fonts, which stand in until it has
|
||||||
|
/// loaded and for any glyph it lacks.
|
||||||
|
pub fn subtitle_family() -> egui::FontFamily {
|
||||||
|
egui::FontFamily::Name("subtitle".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The system fonts loaded so far. Each install builds the definitions from
|
||||||
|
/// all of them, so loading one does not drop the other.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct SystemFonts {
|
||||||
|
cjk: Option<Arc<egui::FontData>>,
|
||||||
|
subtitle: Option<(SubtitleRequest, Arc<egui::FontData>, SubtitleFace)>,
|
||||||
|
/// The subtitle face last asked for, loading or loaded.
|
||||||
|
wanted: Option<SubtitleRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static SYSTEM_FONTS: parking_lot::Mutex<SystemFonts> = parking_lot::Mutex::new(SystemFonts {
|
||||||
|
cjk: None,
|
||||||
|
subtitle: None,
|
||||||
|
wanted: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct SubtitleRequest {
|
||||||
|
family: String,
|
||||||
|
bold: bool,
|
||||||
|
italic: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the loaded subtitle face draws by itself. A style it lacks is one
|
||||||
|
/// libass makes up, and so must the preview.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct SubtitleFace {
|
||||||
|
pub bold: bool,
|
||||||
|
pub italic: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Inter, the icon font, and egui's own fonts behind them for the emoji and
|
/// Inter, the icon font, and egui's own fonts behind them for the emoji and
|
||||||
/// symbols Inter does not carry. `cjk` goes last, when there is one.
|
/// symbols Inter does not carry. The CJK fallback goes last, when there is
|
||||||
fn font_definitions(cjk: Option<egui::FontData>) -> egui::FontDefinitions {
|
/// one.
|
||||||
|
fn font_definitions(system: &SystemFonts) -> egui::FontDefinitions {
|
||||||
use egui::FontFamily::{Monospace, Proportional};
|
use egui::FontFamily::{Monospace, Proportional};
|
||||||
|
|
||||||
let mut fonts = egui::FontDefinitions::default();
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
@@ -207,21 +246,29 @@ fn font_definitions(cjk: Option<egui::FontData>) -> egui::FontDefinitions {
|
|||||||
// Right behind Inter. The icons are in the private use area, which the
|
// Right behind Inter. The icons are in the private use area, which the
|
||||||
// bundled Inter no longer maps, so neither font shadows the other.
|
// bundled Inter no longer maps, so neither font shadows the other.
|
||||||
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
|
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
|
||||||
if let Some(cjk) = cjk {
|
if let Some(cjk) = &system.cjk {
|
||||||
let name = "system-cjk-fallback".to_owned();
|
let name = "system-cjk-fallback".to_owned();
|
||||||
fonts.font_data.insert(name.clone(), Arc::new(cjk));
|
fonts.font_data.insert(name.clone(), cjk.clone());
|
||||||
for family in [Proportional, Monospace] {
|
for family in [Proportional, Monospace] {
|
||||||
fonts.families.entry(family).or_default().push(name.clone());
|
fonts.families.entry(family).or_default().push(name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let fallbacks = fonts.families[&Proportional]
|
let fallbacks: Vec<String> = fonts.families[&Proportional]
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|name| *name != "inter")
|
.filter(|name| *name != "inter")
|
||||||
.cloned();
|
.cloned()
|
||||||
|
.collect();
|
||||||
let semibold_chain = std::iter::once("inter-semibold".to_owned())
|
let semibold_chain = std::iter::once("inter-semibold".to_owned())
|
||||||
.chain(fallbacks)
|
.chain(fallbacks)
|
||||||
.collect();
|
.collect();
|
||||||
fonts.families.insert(semibold(), semibold_chain);
|
fonts.families.insert(semibold(), semibold_chain);
|
||||||
|
let mut subtitle_chain = fonts.families[&Proportional].clone();
|
||||||
|
if let Some((_, face, _)) = &system.subtitle {
|
||||||
|
let name = "system-subtitle".to_owned();
|
||||||
|
fonts.font_data.insert(name.clone(), face.clone());
|
||||||
|
subtitle_chain.insert(0, name);
|
||||||
|
}
|
||||||
|
fonts.families.insert(subtitle_family(), subtitle_chain);
|
||||||
fonts
|
fonts
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +281,7 @@ fn font_definitions(cjk: Option<egui::FontData>) -> egui::FontDefinitions {
|
|||||||
/// anyway. egui deliberately does not bundle the multi-megabyte CJK families,
|
/// anyway. egui deliberately does not bundle the multi-megabyte CJK families,
|
||||||
/// and native desktops already provide one.
|
/// and native desktops already provide one.
|
||||||
pub fn install_fonts(ctx: &egui::Context) {
|
pub fn install_fonts(ctx: &egui::Context) {
|
||||||
ctx.set_fonts(font_definitions(None));
|
ctx.set_fonts(font_definitions(&SYSTEM_FONTS.lock()));
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
let spawned = std::thread::Builder::new()
|
let spawned = std::thread::Builder::new()
|
||||||
.name("fonts".into())
|
.name("fonts".into())
|
||||||
@@ -251,20 +298,80 @@ fn install_cjk_fallback(ctx: &egui::Context) {
|
|||||||
let Ok(bytes) = std::fs::read(&path) else {
|
let Ok(bytes) = std::fs::read(&path) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(data) = cjk_font_data(bytes, index) else {
|
let Some(data) = system_font_data(bytes, index) else {
|
||||||
tracing::debug!("CJK fallback {}#{index} does not parse", path.display());
|
tracing::debug!("CJK fallback {}#{index} does not parse", path.display());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
ctx.set_fonts(font_definitions(Some(data)));
|
let mut system = SYSTEM_FONTS.lock();
|
||||||
|
system.cjk = Some(Arc::new(data));
|
||||||
|
ctx.set_fonts(font_definitions(&system));
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The fallback font, if egui can read it.
|
/// The face the subtitle preview should be drawn in for this font, bold and
|
||||||
|
/// italic, once [`subtitle_family`] holds it.
|
||||||
|
///
|
||||||
|
/// `None` while it loads, and for good where the system cannot say which file
|
||||||
|
/// a family is — everywhere but Linux. The first call for a font starts the
|
||||||
|
/// load on a thread of its own, since `fc-match` and reading the file are too
|
||||||
|
/// slow for a frame, and asks for a repaint when it is in.
|
||||||
|
pub fn subtitle_face(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
family: &str,
|
||||||
|
bold: bool,
|
||||||
|
italic: bool,
|
||||||
|
) -> Option<SubtitleFace> {
|
||||||
|
let request = SubtitleRequest {
|
||||||
|
family: family.to_owned(),
|
||||||
|
bold,
|
||||||
|
italic,
|
||||||
|
};
|
||||||
|
let mut system = SYSTEM_FONTS.lock();
|
||||||
|
if let Some((loaded, _, face)) = &system.subtitle
|
||||||
|
&& *loaded == request
|
||||||
|
{
|
||||||
|
return Some(*face);
|
||||||
|
}
|
||||||
|
if system.wanted.as_ref() != Some(&request) {
|
||||||
|
system.wanted = Some(request.clone());
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let spawned = std::thread::Builder::new()
|
||||||
|
.name("subtitle-font".into())
|
||||||
|
.spawn(move || install_subtitle_face(&ctx, request));
|
||||||
|
if let Err(error) = spawned {
|
||||||
|
tracing::debug!("subtitle font thread: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_subtitle_face(ctx: &egui::Context, request: SubtitleRequest) {
|
||||||
|
let Some((path, index, face)) = system_subtitle_font(&request) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(bytes) = std::fs::read(&path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(data) = system_font_data(bytes, index) else {
|
||||||
|
tracing::debug!("subtitle font {}#{index} does not parse", path.display());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut system = SYSTEM_FONTS.lock();
|
||||||
|
// Another font was picked while this one loaded.
|
||||||
|
if system.wanted.as_ref() != Some(&request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
system.subtitle = Some((request, Arc::new(data), face));
|
||||||
|
ctx.set_fonts(font_definitions(&system));
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A system font, if egui can read it.
|
||||||
///
|
///
|
||||||
/// egui panics on a font it cannot parse, and this one comes from whatever the
|
/// egui panics on a font it cannot parse, and this one comes from whatever the
|
||||||
/// system has installed. So it is parsed here first, the same way egui does,
|
/// system has installed. So it is parsed here first, the same way egui does,
|
||||||
/// and a font that fails is left out rather than taking the app down.
|
/// and a font that fails is left out rather than taking the app down.
|
||||||
fn cjk_font_data(bytes: Vec<u8>, index: u32) -> Option<egui::FontData> {
|
fn system_font_data(bytes: Vec<u8>, index: u32) -> Option<egui::FontData> {
|
||||||
skrifa::FontRef::from_index(&bytes, index).ok()?;
|
skrifa::FontRef::from_index(&bytes, index).ok()?;
|
||||||
let mut data = egui::FontData::from_owned(bytes);
|
let mut data = egui::FontData::from_owned(bytes);
|
||||||
data.index = index;
|
data.index = index;
|
||||||
@@ -299,6 +406,68 @@ fn system_cjk_font() -> Option<(std::path::PathBuf, u32)> {
|
|||||||
path.is_file().then_some((path, index))
|
path.is_file().then_some((path, index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The file fontconfig picks for a subtitle font, as libass asks for it: the
|
||||||
|
/// family with the weight and slant wanted, falling back as libass would to
|
||||||
|
/// whatever fontconfig likes best when the family is not installed.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn system_subtitle_font(
|
||||||
|
request: &SubtitleRequest,
|
||||||
|
) -> Option<(std::path::PathBuf, u32, SubtitleFace)> {
|
||||||
|
let output = std::process::Command::new("fc-match")
|
||||||
|
.args([
|
||||||
|
"-f",
|
||||||
|
"%{file}\\n%{index}\\n%{weight}\\n%{slant}",
|
||||||
|
&fontconfig_pattern(request),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
let text = std::str::from_utf8(&output.stdout).ok()?;
|
||||||
|
let mut lines = text.lines();
|
||||||
|
let path = std::path::PathBuf::from(lines.next()?);
|
||||||
|
let mut number = || lines.next().and_then(|value| value.parse::<u32>().ok());
|
||||||
|
let index = number().unwrap_or(0);
|
||||||
|
let (weight, slant) = (number().unwrap_or(0), number().unwrap_or(0));
|
||||||
|
// A variable font's named instance is drawn by egui as the default one,
|
||||||
|
// which is upright and regular, whatever fontconfig says it matched.
|
||||||
|
let instance = index != fontconfig_face(index);
|
||||||
|
let face = SubtitleFace {
|
||||||
|
bold: !instance && weight >= FC_WEIGHT_BOLD,
|
||||||
|
italic: !instance && slant > 0,
|
||||||
|
};
|
||||||
|
path.is_file()
|
||||||
|
.then_some((path, fontconfig_face(index), face))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", test))]
|
||||||
|
const FC_WEIGHT_BOLD: u32 = 200;
|
||||||
|
|
||||||
|
/// A fontconfig pattern for the request. A family name is free text, and `-`,
|
||||||
|
/// `:`, `,` and `\\` mean something in a pattern, so those are escaped.
|
||||||
|
#[cfg(any(target_os = "linux", test))]
|
||||||
|
fn fontconfig_pattern(request: &SubtitleRequest) -> String {
|
||||||
|
let mut pattern = String::new();
|
||||||
|
for character in request.family.chars() {
|
||||||
|
if matches!(character, '-' | ':' | ',' | '\\') {
|
||||||
|
pattern.push('\\');
|
||||||
|
}
|
||||||
|
pattern.push(character);
|
||||||
|
}
|
||||||
|
if request.bold {
|
||||||
|
pattern.push_str(&format!(":weight={FC_WEIGHT_BOLD}"));
|
||||||
|
}
|
||||||
|
if request.italic {
|
||||||
|
pattern.push_str(":slant=100");
|
||||||
|
}
|
||||||
|
pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elsewhere there is no asking which file a family is, and the preview
|
||||||
|
/// stays in the app's own font.
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn system_subtitle_font(_: &SubtitleRequest) -> Option<(std::path::PathBuf, u32, SubtitleFace)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn system_cjk_font() -> Option<(std::path::PathBuf, u32)> {
|
fn system_cjk_font() -> Option<(std::path::PathBuf, u32)> {
|
||||||
let path = std::path::PathBuf::from(r"C:\Windows\Fonts\YuGothR.ttc");
|
let path = std::path::PathBuf::from(r"C:\Windows\Fonts\YuGothR.ttc");
|
||||||
@@ -960,10 +1129,40 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_fallback_font_egui_cannot_read_is_left_out() {
|
fn a_system_font_egui_cannot_read_is_left_out() {
|
||||||
assert!(cjk_font_data(INTER.to_vec(), 0).is_some());
|
assert!(system_font_data(INTER.to_vec(), 0).is_some());
|
||||||
assert!(cjk_font_data(INTER.to_vec(), 4).is_none());
|
assert!(system_font_data(INTER.to_vec(), 4).is_none());
|
||||||
assert!(cjk_font_data(b"not a font".to_vec(), 0).is_none());
|
assert!(system_font_data(b"not a font".to_vec(), 0).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_subtitle_font_is_asked_of_fontconfig_with_its_style_and_escaped() {
|
||||||
|
let request = |family: &str, bold, italic| SubtitleRequest {
|
||||||
|
family: family.to_owned(),
|
||||||
|
bold,
|
||||||
|
italic,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
fontconfig_pattern(&request("Noto Sans", false, false)),
|
||||||
|
"Noto Sans"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fontconfig_pattern(&request("Noto Sans", true, true)),
|
||||||
|
"Noto Sans:weight=200:slant=100"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fontconfig_pattern(&request("A-B:C,D\\E", false, true)),
|
||||||
|
"A\\-B\\:C\\,D\\\\E:slant=100"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_subtitle_preview_family_exists_before_any_font_has_loaded() {
|
||||||
|
let fonts = font_definitions(&SystemFonts::default());
|
||||||
|
assert_eq!(
|
||||||
|
fonts.families[&subtitle_family()],
|
||||||
|
fonts.families[&egui::FontFamily::Proportional]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -463,7 +463,11 @@ fn subtitle_form(ui: &mut egui::Ui, style: &SubtitleStyle, actions: &mut Vec<Act
|
|||||||
ui::widgets::settings_row(
|
ui::widgets::settings_row(
|
||||||
ui,
|
ui,
|
||||||
"Font",
|
"Font",
|
||||||
Some("Any font installed here. The sample above is drawn in the app's own font."),
|
Some(if cfg!(target_os = "linux") {
|
||||||
|
"Any font installed here."
|
||||||
|
} else {
|
||||||
|
"Any font installed here. The sample above is drawn in the app's own font."
|
||||||
|
}),
|
||||||
|ui| {
|
|ui| {
|
||||||
ui::widgets::toggle(ui, &mut chosen.italic, "Italic");
|
ui::widgets::toggle(ui, &mut chosen.italic, "Italic");
|
||||||
ui::widgets::toggle(ui, &mut chosen.bold, "Bold");
|
ui::widgets::toggle(ui, &mut chosen.bold, "Bold");
|
||||||
@@ -662,9 +666,11 @@ fn font_picker(ui: &mut egui::Ui, font: &mut Option<String>) {
|
|||||||
/// A sample line, half on a bright sky and half on night, drawn in the
|
/// A sample line, half on a bright sky and half on night, drawn in the
|
||||||
/// chosen style — so whether it reads on both is visible before a film is.
|
/// chosen style — so whether it reads on both is visible before a film is.
|
||||||
///
|
///
|
||||||
/// An approximation: the app's own font rather than the chosen one, egui's
|
/// An approximation. The font is the face fontconfig picks for the chosen one,
|
||||||
/// slant for italic, and no blur. Proportions follow mpv's, where sizes are
|
/// as libass's is, but only on Linux; elsewhere, and while it loads, it is the
|
||||||
/// in pixels of a 38-pixel font.
|
/// app's own. A bold or italic the face lacks is made up, as libass does, and
|
||||||
|
/// there is no blur. Proportions follow mpv's, where sizes are in pixels of a
|
||||||
|
/// 38-pixel font.
|
||||||
fn subtitle_preview(ui: &mut egui::Ui, style: &SubtitleStyle) {
|
fn subtitle_preview(ui: &mut egui::Ui, style: &SubtitleStyle) {
|
||||||
const HEIGHT: f32 = 132.0;
|
const HEIGHT: f32 = 132.0;
|
||||||
/// mpv's `sub-font-size`, the unit every other size is relative to.
|
/// mpv's `sub-font-size`, the unit every other size is relative to.
|
||||||
@@ -694,17 +700,27 @@ fn subtitle_preview(ui: &mut egui::Ui, style: &SubtitleStyle) {
|
|||||||
|
|
||||||
let font_px = 22.0 * style.scale as f32;
|
let font_px = 22.0 * style.scale as f32;
|
||||||
let unit = font_px / MPV_FONT;
|
let unit = font_px / MPV_FONT;
|
||||||
let family = if style.bold {
|
let face = theme::subtitle_face(
|
||||||
theme::semibold()
|
ui.ctx(),
|
||||||
} else {
|
style.font.as_deref().unwrap_or(SubtitleStyle::DEFAULT_FONT),
|
||||||
egui::FontFamily::Proportional
|
style.bold,
|
||||||
|
style.italic,
|
||||||
|
);
|
||||||
|
let (family, made_up_bold, made_up_italic) = match face {
|
||||||
|
Some(face) => (
|
||||||
|
theme::subtitle_family(),
|
||||||
|
style.bold && !face.bold,
|
||||||
|
style.italic && !face.italic,
|
||||||
|
),
|
||||||
|
None if style.bold => (theme::semibold(), false, style.italic),
|
||||||
|
None => (egui::FontFamily::Proportional, false, style.italic),
|
||||||
};
|
};
|
||||||
let job = egui::text::LayoutJob::single_section(
|
let job = egui::text::LayoutJob::single_section(
|
||||||
"Can you read this on both sides?".to_owned(),
|
"Can you read this on both sides?".to_owned(),
|
||||||
egui::TextFormat {
|
egui::TextFormat {
|
||||||
font_id: egui::FontId::new(font_px, family),
|
font_id: egui::FontId::new(font_px, family),
|
||||||
color: egui::Color32::PLACEHOLDER,
|
color: egui::Color32::PLACEHOLDER,
|
||||||
italics: style.italic,
|
italics: made_up_italic,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -763,6 +779,15 @@ fn subtitle_preview(ui: &mut egui::Ui, style: &SubtitleStyle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if made_up_bold {
|
||||||
|
// libass thickens a face with no bold by a stroke of its own colour.
|
||||||
|
let thicken = (font_px / 40.0).max(0.5);
|
||||||
|
painter.galley_with_override_text_color(
|
||||||
|
at + egui::vec2(thicken, 0.0),
|
||||||
|
galley.clone(),
|
||||||
|
colour(style.colour),
|
||||||
|
);
|
||||||
|
}
|
||||||
painter.galley_with_override_text_color(at, galley, colour(style.colour));
|
painter.galley_with_override_text_color(at, galley, colour(style.colour));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ pub struct SubtitleStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SubtitleStyle {
|
impl SubtitleStyle {
|
||||||
|
/// The family a style with no font of its own is set in: mpv's default,
|
||||||
|
/// which fontconfig resolves to the system's sans-serif face.
|
||||||
|
pub const DEFAULT_FONT: &str = "sans-serif";
|
||||||
pub const MIN_SCALE: f64 = 0.5;
|
pub const MIN_SCALE: f64 = 0.5;
|
||||||
pub const MAX_SCALE: f64 = 2.5;
|
pub const MAX_SCALE: f64 = 2.5;
|
||||||
pub const MAX_OUTLINE: f64 = 10.0;
|
pub const MAX_OUTLINE: f64 = 10.0;
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ pub fn subtitle_options(style: &SubtitleStyle) -> Vec<(&'static str, String)> {
|
|||||||
style
|
style
|
||||||
.font
|
.font
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "sans-serif".to_owned()),
|
.unwrap_or_else(|| SubtitleStyle::DEFAULT_FONT.to_owned()),
|
||||||
),
|
),
|
||||||
("sub-bold", flag(style.bold)),
|
("sub-bold", flag(style.bold)),
|
||||||
("sub-italic", flag(style.italic)),
|
("sub-italic", flag(style.italic)),
|
||||||
|
|||||||
+3
-2
@@ -196,8 +196,9 @@ How subtitles look. A sample line at the top shows the style on a bright and a d
|
|||||||
a video that is playing changes at once.
|
a video that is playing changes at once.
|
||||||
|
|
||||||
- **Size** and **Position**: how big the line is and how far up the picture it sits.
|
- **Size** and **Position**: how big the line is and how far up the picture it sits.
|
||||||
- **Font**, **Bold** and **Italic**: any font installed on this computer. The sample line uses the
|
- **Font**, **Bold** and **Italic**: any font installed on this computer. On Linux the sample
|
||||||
app's own font; the video uses the one you chose.
|
line shows the font too; on Windows and macOS it is drawn in the app's own font, and only the
|
||||||
|
video uses the one you chose.
|
||||||
- **Text colour**, and a **Background** of **Outline** or **Box**, each with its own colour. A
|
- **Text colour**, and a **Background** of **Outline** or **Box**, each with its own colour. A
|
||||||
colour's opacity sets how much of the picture shows through. Choose **Box** if text is hard to
|
colour's opacity sets how much of the picture shows through. Choose **Box** if text is hard to
|
||||||
read on bright scenes.
|
read on bright scenes.
|
||||||
|
|||||||
Reference in new issue
Block a user