mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-06 02:19:31 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a02d1d386 | ||
|
|
6af917e869 |
@@ -171,7 +171,9 @@ impl Dictionary {
|
||||
["COMPOUNDWORDMAX", n, ..] => d.compound_max = n.parse().unwrap_or(0),
|
||||
["REP", n] => {
|
||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
||||
// The count is the file's claim: stop when the records run out.
|
||||
let Some(line) = lines.next() else { break };
|
||||
let r: Vec<&str> = line.split_whitespace().collect();
|
||||
if let [_, a, b, ..] = r.as_slice() {
|
||||
d.rep.push((a.replace('_', " "), b.replace('_', " ")));
|
||||
}
|
||||
@@ -182,7 +184,8 @@ impl Dictionary {
|
||||
let Some(flag) = one_flag(Some(flag), mode) else { continue };
|
||||
let cross = cross.eq_ignore_ascii_case("y");
|
||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
||||
let Some(line) = lines.next() else { break };
|
||||
let r: Vec<&str> = line.split_whitespace().collect();
|
||||
let [_, _, strip, add, rest @ ..] = r.as_slice() else { continue };
|
||||
let (add, cont) = match add.split_once('/') {
|
||||
Some((a, c)) => (a, parse_flags(c, mode)),
|
||||
@@ -576,6 +579,15 @@ fn cache() -> &'static Cache {
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// Language strings remembered by [`dictionary`]; a string past it is resolved again on each call.
|
||||
const MAX_CACHED_LANGS: usize = 256;
|
||||
|
||||
/// Parsed dictionaries by their affix file, so aliases of one language share it.
|
||||
fn by_files() -> &'static Mutex<HashMap<PathBuf, Arc<Dictionary>>> {
|
||||
static M: OnceLock<Mutex<HashMap<PathBuf, Arc<Dictionary>>>> = OnceLock::new();
|
||||
M.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// The dictionary for `lang` (a BCP-47 tag such as `en-US`), loaded once per process.
|
||||
pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||
let cache = cache();
|
||||
@@ -586,10 +598,23 @@ pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||
return d.clone();
|
||||
}
|
||||
let loaded = find_in(&search_dirs(), lang).and_then(|(a, d)| {
|
||||
let (a, d) = (std::fs::read(a).ok()?, std::fs::read(d).ok()?);
|
||||
Some(Arc::new(Dictionary::parse(&a, &d)))
|
||||
// Tags that resolve to the same files share one parsed dictionary.
|
||||
if let Ok(m) = by_files().lock()
|
||||
&& let Some(dict) = m.get(&a)
|
||||
{
|
||||
return Some(dict.clone());
|
||||
}
|
||||
let (bytes_a, bytes_d) = (std::fs::read(&a).ok()?, std::fs::read(d).ok()?);
|
||||
let dict = Arc::new(Dictionary::parse(&bytes_a, &bytes_d));
|
||||
// A concurrent loader of the same files may have inserted first: share its copy.
|
||||
if let Ok(mut m) = by_files().lock() {
|
||||
return Some(m.entry(a).or_insert(dict).clone());
|
||||
}
|
||||
Some(dict)
|
||||
});
|
||||
if let Ok(mut c) = cache.lock() {
|
||||
if let Ok(mut c) = cache.lock()
|
||||
&& c.len() < MAX_CACHED_LANGS
|
||||
{
|
||||
c.insert(key, loaded.clone());
|
||||
}
|
||||
loaded
|
||||
@@ -880,6 +905,14 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
assert!(d.suggest("workingslides", 5).contains(&"working slides".to_string()));
|
||||
}
|
||||
|
||||
/// An affix header can claim billions of records; parsing stops where the file does.
|
||||
#[test]
|
||||
fn a_header_count_past_the_records_ends_with_them() {
|
||||
let aff = b"SFX S Y 4000000000\nSFX S 0 s .\nREP 4000000000\nREP a b\n";
|
||||
let d = Dictionary::parse(aff, b"1\ncat/S\n".as_slice());
|
||||
assert!(d.check("cats"));
|
||||
}
|
||||
|
||||
/// #1175: compound splitting is quadratic in the word's length, so a word longer than
|
||||
/// `MAX_CHECK_CHARS` is not checked; shorter ones still are.
|
||||
#[test]
|
||||
|
||||
@@ -12,9 +12,6 @@ use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHand
|
||||
|
||||
use crate::icons::{self, Icon};
|
||||
|
||||
/// Video shapes that get a decoder at once; more play their sound without a picture.
|
||||
const MAX_VIDEO_FEEDS: usize = 16;
|
||||
|
||||
/// Who started a voice: the editor's control bar stops when you leave the slide, the slide show
|
||||
/// manages its own.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -230,21 +227,12 @@ impl MediaHost {
|
||||
audio: clip.volume > 0.0,
|
||||
};
|
||||
let source_changed = self.owners.get(&id).is_some_and(|o| o.source != media_key);
|
||||
// A shape whose feed could not be had (the cap, a decode error) tries again on each play.
|
||||
let feedless = self.videos.get(&id).is_some_and(|v| v.feed.is_none());
|
||||
if clip.video && (source_changed || feedless || !self.videos.contains_key(&id)) {
|
||||
// One decoder and frame queue per video shape: past the cap a shape plays its sound only.
|
||||
let live = self.videos.iter().filter(|(vid, v)| **vid != id && v.feed.is_some()).count();
|
||||
let feed = if live >= MAX_VIDEO_FEEDS {
|
||||
log::info!("video {id}: more than {MAX_VIDEO_FEEDS} videos playing; showing no picture");
|
||||
None
|
||||
} else {
|
||||
match VideoFeed::new(bytes.clone()) {
|
||||
Ok(f) => Some(f),
|
||||
Err(e) => {
|
||||
log::info!("video {id}: {e}");
|
||||
None
|
||||
}
|
||||
if clip.video && (source_changed || !self.videos.contains_key(&id)) {
|
||||
let feed = match VideoFeed::new(bytes.clone()) {
|
||||
Ok(f) => Some(f),
|
||||
Err(e) => {
|
||||
log::info!("video {id}: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
self.videos.insert(id, Video { feed, frame: None, tex: None });
|
||||
|
||||
@@ -196,40 +196,18 @@ pub(super) fn part_textures(ctx: &egui::Context, doc: &Presentation, from: Optio
|
||||
]
|
||||
}
|
||||
|
||||
/// Width of a slide-show grid thumbnail, in pixels.
|
||||
const THUMB_WIDTH: f64 = 320.0;
|
||||
/// Tallest grid thumbnail, so an extreme portrait slide cannot ask for a huge raster.
|
||||
const THUMB_MAX_HEIGHT: f64 = 2048.0;
|
||||
|
||||
/// The scale and pixel size of a grid thumbnail of a `width` x `height` slide.
|
||||
fn thumb_size(width: f64, height: f64) -> (f64, f64, f64) {
|
||||
let (sw, sh) = (width.max(1.0), height.max(1.0));
|
||||
let scale = (THUMB_WIDTH / sw).min(THUMB_MAX_HEIGHT / sh);
|
||||
(scale, (sw * scale).round().max(1.0), (sh * scale).round().max(1.0))
|
||||
}
|
||||
|
||||
pub(super) fn render_thumb(ctx: &egui::Context, show: &mut Show, doc: &Presentation, i: usize) -> Option<TextureHandle> {
|
||||
if show.thumbs.len() >= GRID_CAP {
|
||||
return None;
|
||||
}
|
||||
// 320 wide, unless the slide is so tall that this would pass the height limit: fit that box.
|
||||
let (scale, w, h) = thumb_size(doc.slide_size.width, doc.slide_size.height);
|
||||
let img = deckcraft_render::render_slide(doc, i, &RenderOpts { scale, size: Some((w as u32, h as u32)), ..Default::default() });
|
||||
let w = 320.0_f64;
|
||||
let h = (w * doc.slide_size.height / doc.slide_size.width.max(1.0)).round().max(1.0);
|
||||
let img = deckcraft_render::render_slide(
|
||||
doc,
|
||||
i,
|
||||
&RenderOpts { scale: w / doc.slide_size.width.max(1.0), size: Some((w as u32, h as u32)), ..Default::default() },
|
||||
);
|
||||
let tex = crate::textures::upload(ctx, format!("show-grid-{i}"), crate::textures::to_color_image(&img, false));
|
||||
show.thumbs.push((i, tex.clone()));
|
||||
Some(tex)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod thumb_size_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ordinary_slides_are_320_wide_and_extreme_ones_stay_in_the_height_limit() {
|
||||
let (scale, w, h) = thumb_size(960.0, 540.0);
|
||||
assert_eq!((w, h), (320.0, 180.0));
|
||||
assert!((scale - 320.0 / 960.0).abs() < 1e-12);
|
||||
let (_, w, h) = thumb_size(1.0, 1.0e9);
|
||||
assert!(h <= THUMB_MAX_HEIGHT && w >= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user