Compare commits

..
11 changed files with 73 additions and 219 deletions
Generated
+28 -28
View File
@@ -984,7 +984,7 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "deckcraft"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"arboard",
"chrono",
@@ -1006,7 +1006,7 @@ dependencies = [
[[package]]
name = "deckcraft-anim"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-geom",
"deckcraft-model",
@@ -1015,7 +1015,7 @@ dependencies = [
[[package]]
name = "deckcraft-av1"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-bitstream",
"rayon",
@@ -1024,14 +1024,14 @@ dependencies = [
[[package]]
name = "deckcraft-bitstream"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"craft-media",
]
[[package]]
name = "deckcraft-cli"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-engine",
"deckcraft-mcp",
@@ -1041,14 +1041,14 @@ dependencies = [
[[package]]
name = "deckcraft-color"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"serde",
]
[[package]]
name = "deckcraft-engine"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"chrono",
"deckcraft-anim",
@@ -1074,7 +1074,7 @@ dependencies = [
[[package]]
name = "deckcraft-fonts"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"epaint_default_fonts",
"harfrust 0.13.3",
@@ -1086,7 +1086,7 @@ dependencies = [
[[package]]
name = "deckcraft-format"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-model",
"serde",
@@ -1097,7 +1097,7 @@ dependencies = [
[[package]]
name = "deckcraft-geom"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"kurbo",
"serde",
@@ -1105,7 +1105,7 @@ dependencies = [
[[package]]
name = "deckcraft-h264"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-bitstream",
"rayon",
@@ -1114,7 +1114,7 @@ dependencies = [
[[package]]
name = "deckcraft-hevc"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-bitstream",
"rayon",
@@ -1123,21 +1123,21 @@ dependencies = [
[[package]]
name = "deckcraft-isobmff"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"craft-media",
]
[[package]]
name = "deckcraft-matroska"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"craft-media",
]
[[package]]
name = "deckcraft-mcp"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-engine",
"deckcraft-render",
@@ -1147,7 +1147,7 @@ dependencies = [
[[package]]
name = "deckcraft-media"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-av1",
"deckcraft-h264",
@@ -1165,7 +1165,7 @@ dependencies = [
[[package]]
name = "deckcraft-metafile"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"image",
"kurbo",
@@ -1174,7 +1174,7 @@ dependencies = [
[[package]]
name = "deckcraft-model"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-color",
"deckcraft-geom",
@@ -1186,15 +1186,15 @@ dependencies = [
[[package]]
name = "deckcraft-ogg"
version = "0.15.1"
version = "0.15.0"
[[package]]
name = "deckcraft-opus"
version = "0.15.1"
version = "0.15.0"
[[package]]
name = "deckcraft-pdf"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"craft-pdf",
"deckcraft-fonts",
@@ -1212,7 +1212,7 @@ dependencies = [
[[package]]
name = "deckcraft-pptx"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-color",
"deckcraft-engine",
@@ -1228,7 +1228,7 @@ dependencies = [
[[package]]
name = "deckcraft-render"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-anim",
"deckcraft-color",
@@ -1247,7 +1247,7 @@ dependencies = [
[[package]]
name = "deckcraft-text"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"chrono",
"deckcraft-color",
@@ -1261,7 +1261,7 @@ dependencies = [
[[package]]
name = "deckcraft-ui-egui"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"craft-ui",
"craft-update",
@@ -1286,7 +1286,7 @@ dependencies = [
[[package]]
name = "deckcraft-vp9"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-bitstream",
"rayon",
@@ -1295,7 +1295,7 @@ dependencies = [
[[package]]
name = "deckcraft-web"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"deckcraft-engine",
"deckcraft-ui-egui",
@@ -6177,7 +6177,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
[[package]]
name = "xtask"
version = "0.15.1"
version = "0.15.0"
dependencies = [
"serde_json",
]
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/deckcraft", "apps/deckcraft-cli"]
[workspace.package]
version = "0.15.1"
version = "0.15.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
-3
View File
@@ -251,9 +251,6 @@ impl Popup {
if value.get("nextChanged").and_then(|v| v.as_bool()) == Some(false) {
data.next = previous.next.clone();
}
if data.slides.is_none() {
data.slides = previous.slides.clone();
}
}
*target.borrow_mut() = Some(data);
*received.borrow_mut() = js_sys::Date::now();
+11 -7
View File
@@ -1,6 +1,8 @@
//! Fonts a presentation uses that this machine doesn't have, so opening it can say so (its text
//! draws in a stand-in; see `FontDb::document_face`).
use std::collections::HashSet;
use deckcraft_fonts::FontDb;
use deckcraft_model::resolve::{self, Ctx};
use deckcraft_model::text::{Bullet, TextBody};
@@ -12,8 +14,13 @@ use deckcraft_model::{Presentation, Shape, ShapeKind};
/// font to load and are left for the round trip.
pub fn install_embedded_fonts(p: &Presentation) {
let db = FontDb::global();
// Faces can name one media item many times; each is copied and enumerated once.
let mut seen = HashSet::new();
for font in &p.embedded_fonts {
for id in font.faces().into_iter().flatten() {
if !seen.insert(id) {
continue;
}
let Some(item) = p.media(id) else { continue };
let Some(sfnt) = deckcraft_fonts::embedded_sfnt(&item.data) else { continue };
db.add_font(sfnt.to_vec());
@@ -27,12 +34,8 @@ pub fn install_embedded_fonts(p: &Presentation) {
/// and can't be installed, so they aren't reported.
pub fn missing_fonts(p: &Presentation) -> Vec<String> {
let db = FontDb::global();
let mut out: Vec<String> = Vec::new();
for family in used_fonts(p) {
if !family.starts_with('.') && !db.has_family(&family) && !out.iter().any(|f| f.eq_ignore_ascii_case(&family)) {
out.push(family);
}
}
// used_fonts is already free of case-insensitive duplicates.
let mut out: Vec<String> = used_fonts(p).into_iter().filter(|f| !f.starts_with('.') && !db.has_family(f)).collect();
out.sort_by_key(|f| f.to_lowercase());
out
}
@@ -40,9 +43,10 @@ pub fn missing_fonts(p: &Presentation) -> Vec<String> {
/// The families the slides' text runs and bullets are drawn in.
pub fn used_fonts(p: &Presentation) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let mut add = |f: String| {
let f = f.trim().to_string();
if !f.is_empty() && !out.iter().any(|o| o.eq_ignore_ascii_case(&f)) {
if !f.is_empty() && seen.insert(f.to_ascii_lowercase()) {
out.push(f);
}
};
+21
View File
@@ -6483,6 +6483,27 @@ fn master_body_placeholder_format_keeps_per_level_formatting() {
assert_eq!(shown(&s), vec![Some(28.0), Some(24.0)]);
}
#[test]
fn many_distinct_fonts_are_listed_once_each_without_a_quadratic_scan() {
// 30,000 runs in 15,000 families, each written twice in different case.
let mut p = deckcraft_model::Presentation::default();
let slide = std::sync::Arc::make_mut(&mut p.slides[0]);
let mut body = deckcraft_model::text::TextBody::from_text("x");
let run = body.paragraphs[0].runs[0].clone();
body.paragraphs[0].runs = (0..30_000)
.map(|i| {
let mut r = run.clone();
let name = format!("Qqq Font {}", i / 2);
r.props.font = Some(if i % 2 == 0 { name } else { name.to_uppercase() });
r
})
.collect();
slide.shapes[0].text = Some(body);
let used = crate::fonts::used_fonts(&p);
assert_eq!(used.iter().filter(|f| f.starts_with("Qqq Font ")).count(), 15_000);
assert_eq!(crate::fonts::missing_fonts(&p).iter().filter(|f| f.to_ascii_lowercase().starts_with("qqq font ")).count(), 15_000);
}
#[test]
fn slide_master_view_rejects_invalid_indices() {
let mut s = Session::with_new();
-4
View File
@@ -144,10 +144,6 @@ impl SlideApp {
let visible = show::viewport_visible(ctx);
let Some(doc) = self.session.active().map(|d| d.doc.clone()) else { return };
let Some(show) = self.show.as_mut() else { return };
if show.presenter_grid && self.services.presenter.as_ref().is_some_and(|host| !host.is_open()) {
// A web popup closed with its grid open must not hold auto-advance while this tab is hidden (#1866).
show.presenter_grid = false;
}
if visible {
// A visible logic-only pass (`run_logic`) carries the last full frame's input time.
// Anchoring that stale time to `now` would move the hidden clock backwards if the
+5 -102
View File
@@ -49,15 +49,10 @@ pub struct Snapshot {
pub paused: bool,
pub notes_px: f32,
pub blank: Option<String>,
/// All slides is open in the presenter window (#1866); the audience keeps the current slide.
pub grid: bool,
pub zoom_armed: bool,
pub current: Option<Preview>,
pub next: Option<Preview>,
/// The All slides thumbnails, in slide order, while `grid` is on. `None` in a sent delta
/// means "unchanged since the last snapshot the window received".
#[serde(default)]
pub slides: Option<Vec<Preview>>,
}
/// Images acknowledged by a successful transport send, never merely attempted images.
@@ -65,7 +60,6 @@ pub struct Snapshot {
pub struct DeltaCache {
current: Option<Preview>,
next: Option<Preview>,
slides: Option<Vec<Preview>>,
token: String,
}
impl DeltaCache {
@@ -81,12 +75,6 @@ impl DeltaCache {
let next_changed = fresh || self.next != snapshot.next;
let current = snapshot.current.clone();
let next = snapshot.next.clone();
let slides = snapshot.slides.clone();
if !fresh && self.slides == slides {
snapshot.slides = None;
} else if snapshot.slides.is_none() {
snapshot.slides = Some(Vec::new());
}
if !current_changed {
snapshot.current = None;
}
@@ -96,28 +84,14 @@ impl DeltaCache {
send(snapshot, current_changed, next_changed)?;
self.current = current;
self.next = next;
self.slides = slides;
self.token = token.to_owned();
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Command {
Key {
name: String,
command: bool,
shift: bool,
alt: bool,
ctrl: bool,
},
Zoom {
x: f32,
y: f32,
},
/// A slide chosen in the presenter window's All slides grid (#1866).
Goto {
slide: usize,
},
Key { name: String, command: bool, shift: bool, alt: bool, ctrl: bool },
Zoom { x: f32, y: f32 },
End,
}
/// Implemented by the browser host. Native hosts retain the existing egui viewport.
@@ -138,8 +112,6 @@ pub struct View {
next: Option<egui::TextureHandle>,
previous_current: Option<Preview>,
previous_next: Option<Preview>,
grid_source: Option<Vec<Preview>>,
grid: Vec<Option<egui::TextureHandle>>,
}
impl View {
/// Install the same interface and dynamic fallback fonts as SlideApp before layout.
@@ -175,10 +147,6 @@ impl View {
.collect::<Vec<_>>()
});
for (key, modifiers) in keys {
if snapshot.grid && matches!(key, egui::Key::Space | egui::Key::Enter) {
// A focused thumbnail takes these to choose its slide; the show ignores them while the grid is open.
continue;
}
// Consume once before widgets see Space/Enter, avoiding a second button activation.
ui.input_mut(|i| i.consume_key(modifiers, key));
commands.push(Command::Key {
@@ -232,11 +200,6 @@ impl View {
}
});
ui.separator();
if snapshot.grid {
// All slides opens here, as in the desktop Presenter View; the audience keeps the current slide.
self.grid_ui(ui, snapshot, &mut commands);
return commands;
}
ui.columns(2, |columns| {
let left = &mut columns[0];
if snapshot.ended {
@@ -264,6 +227,9 @@ impl View {
if snapshot.zoom_armed {
left.label("Click the preview to zoom.");
}
if snapshot.grid {
left.label("Choose a slide in the audience window, or press G to return.");
}
let right = &mut columns[1];
right.label("Next");
if let Some(texture) = &self.next {
@@ -294,43 +260,6 @@ impl View {
commands
}
}
impl View {
fn grid_ui(&mut self, ui: &mut egui::Ui, snapshot: &Snapshot, commands: &mut Vec<Command>) {
let slides = snapshot.slides.as_deref().unwrap_or_default();
if self.grid_source.as_deref() != Some(slides) {
self.grid_source = Some(slides.to_vec());
self.grid = slides
.iter()
.enumerate()
.map(|(i, p)| p.image().map(|image| ui.ctx().load_texture(format!("remote-grid-{i}"), image, egui::TextureOptions::LINEAR)))
.collect();
}
ui.heading("All slides");
ui.label("Choose a slide to show it, or press G to return.");
if snapshot.total > crate::show::GRID_CAP {
ui.label(format!("First {} slides", crate::show::GRID_CAP));
}
egui::ScrollArea::vertical().show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
for (i, texture) in self.grid.iter().enumerate() {
let size = egui::vec2(160.0, 90.0);
let label = format!("{}", i + 1);
let label = if i == snapshot.slide { egui::RichText::new(label).strong() } else { egui::RichText::new(label) };
let response = match texture {
Some(t) => ui.add(egui::Button::image_and_text(
egui::Image::new(t).fit_to_exact_size(fit_preview(t.size_vec2(), size.x, size.y)),
label,
)),
None => ui.add_sized(size, egui::Button::new(label)),
};
if response.clicked() {
commands.push(Command::Goto { slide: i });
}
}
});
});
}
}
fn fit_preview(size: egui::Vec2, width: f32, height: f32) -> egui::Vec2 {
size * (width.max(1.0) / size.x.max(1.0)).min(height.max(1.0) / size.y.max(1.0))
}
@@ -353,7 +282,6 @@ mod tests {
zoom_armed: false,
current: Some(Preview { width: 1, height: 1, rgba: vec![255; 4] }),
next: Some(Preview { width: 1, height: 1, rgba: vec![0; 4] }),
slides: None,
}
}
@@ -490,7 +418,6 @@ mod tests {
zoom_armed: false,
current: None,
next: None,
slides: None,
};
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1000.0, 700.0)).build_ui_state(
|ui, (view, snapshot, sent): &mut (View, Snapshot, Vec<Command>)| {
@@ -510,30 +437,6 @@ mod tests {
assert!(matches!(h.state().2.last(), Some(Command::Key { name, .. }) if name == egui::Key::Space.name()));
}
#[test]
fn all_slides_opens_in_the_popup_and_picks_a_slide() {
use egui_kittest::kittest::Queryable;
let thumb = Preview { width: 16, height: 9, rgba: vec![200; 16 * 9 * 4] };
let mut snapshot = snapshot();
snapshot.total = 3;
snapshot.grid = true;
snapshot.slides = Some(vec![thumb.clone(), thumb.clone(), thumb]);
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1000.0, 700.0)).build_ui_state(
|ui, (view, snapshot, sent): &mut (View, Snapshot, Vec<Command>)| {
sent.extend(view.ui(ui, snapshot));
},
(View::default(), snapshot, Vec::new()),
);
h.run_steps(2);
assert!(h.query_by_label("Notes").is_none(), "the grid replaces the presenter panes, as on desktop (#1866)");
h.get_by_label("3").click();
h.run_steps(2);
assert_eq!(h.state().2, vec![Command::Goto { slide: 2 }]);
h.key_press(egui::Key::Enter);
h.run_steps(2);
assert!(!h.state().2.iter().any(|c| matches!(c, Command::Key { .. })), "Enter is left to the focused thumbnail");
}
#[test]
fn previews_are_bounded_and_bad_dimensions_are_rejected() {
let image = egui::ColorImage::filled([1280, 720], egui::Color32::RED);
-4
View File
@@ -18,7 +18,6 @@ mod presenter;
mod render;
mod ui;
mod zoom;
pub(crate) use zoom::GRID_CAP;
#[cfg(test)]
use media::{on_screen, pointer_on_media_bar, shown_slide_rect, slide_media_bars};
@@ -66,8 +65,6 @@ pub struct Show {
preview_spawns: usize,
remote_preview: Option<crate::presenter::Preview>,
remote_next: Option<(usize, crate::presenter::Preview)>,
/// All slides thumbnails sent to the web presenter window while its grid is open (#1866).
remote_grid: Vec<crate::presenter::Preview>,
remote_sent: f64,
remote_opened: bool,
/// The next slide's first frame, rendered in the background while this one is shown so
@@ -240,7 +237,6 @@ impl Show {
preview_spawns: 0,
remote_preview: None,
remote_next: None,
remote_grid: vec![],
remote_sent: f64::NEG_INFINITY,
remote_opened: false,
prefetch: None,
+2 -33
View File
@@ -1132,8 +1132,6 @@ fn remote_presenter_uses_the_audience_state_and_closes_with_the_show() {
opens: usize,
closes: usize,
commands: Vec<Command>,
last: Option<Snapshot>,
gone: bool,
}
struct Mock(Rc<RefCell<Bridge>>);
impl Host for Mock {
@@ -1142,14 +1140,12 @@ fn remote_presenter_uses_the_audience_state_and_closes_with_the_show() {
Ok(())
}
fn is_open(&self) -> bool {
!self.0.borrow().gone
true
}
fn commands(&mut self) -> Vec<Command> {
std::mem::take(&mut self.0.borrow_mut().commands)
}
fn update(&mut self, snapshot: Snapshot) {
self.0.borrow_mut().last = Some(snapshot);
}
fn update(&mut self, _: Snapshot) {}
fn close(&mut self) {
self.0.borrow_mut().closes += 1;
}
@@ -1183,33 +1179,6 @@ fn remote_presenter_uses_the_audience_state_and_closes_with_the_show() {
h.run_steps(2);
assert_eq!(h.state().show.as_ref().unwrap().state.slide, slide, "{name}");
}
// #1866: All slides opens in the web Presenter View, as on desktop; the audience keeps its slide.
bridge.borrow_mut().commands.push(Command::Key { name: "G".into(), command: false, shift: false, alt: false, ctrl: false });
h.run_steps(2);
let show = h.state().show.as_ref().unwrap();
assert!(show.presenter_grid && !show.grid, "G from the popup opens its own grid, not the audience's");
for _ in 0..20 {
h.step();
std::thread::sleep(std::time::Duration::from_millis(15));
}
let last = bridge.borrow().last.clone().unwrap();
assert!(last.grid);
assert_eq!(last.slides.as_ref().map(Vec::len), Some(3), "the popup gets a thumbnail per slide");
assert!(last.slides.iter().flatten().all(|p| p.width.max(p.height) <= 96), "thumbnails stay small enough for one message");
assert!(last.current.is_none() && last.next.is_none(), "the grid replaces both previews");
bridge.borrow_mut().commands.push(Command::Goto { slide: 2 });
h.run_steps(2);
let show = h.state().show.as_ref().unwrap();
assert!(!show.presenter_grid && !show.grid);
assert_eq!(show.state.slide, 2);
// Closing the popup with its grid open must not hold auto-advance for the rest of the show.
bridge.borrow_mut().commands.push(Command::Key { name: "G".into(), command: false, shift: false, alt: false, ctrl: false });
h.run_steps(2);
assert!(h.state().show.as_ref().unwrap().presenter_grid);
bridge.borrow_mut().gone = true;
h.run_steps(2);
assert!(!h.state().show.as_ref().unwrap().presenter_grid);
bridge.borrow_mut().gone = false;
bridge.borrow_mut().commands.push(Command::End);
h.run_steps(2);
assert!(h.state().show.is_none());
+4 -36
View File
@@ -80,10 +80,6 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
&& !show.reading
&& let Some(host) = app.services.presenter.as_mut()
{
if show.remote_opened && show.presenter_grid && !host.is_open() {
// A grid left open in the closed popup would hold auto-advance for the rest of the show.
show.presenter_grid = false;
}
if !show.remote_opened {
show.remote_opened = true;
if let Err(message) = host.open() {
@@ -100,18 +96,9 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
crate::presenter::Command::Key { name, command, shift, alt, ctrl } => {
if let Some(key) = egui::Key::from_name(&name) {
let modifiers = egui::Modifiers { command, shift, alt, ctrl, mac_cmd: command && !ctrl };
// The popup is the Presenter View: G opens All slides there, not on the audience screen (#1866).
out.keep &= handle_keys(&mut show, &doc, now, vec![(key, modifiers)], true).0;
out.keep &= handle_keys(&mut show, &doc, now, vec![(key, modifiers)], false).0;
}
}
crate::presenter::Command::Goto { slide } if show.presenter_grid && slide < doc.slides.len() => {
show.presenter_grid = false;
show.blank = None;
show.zoom = None;
show.zoom_arm = false;
let action = show.state.goto(&doc, slide);
out.keep &= show.apply(action, &doc, now);
}
_ => {}
}
}
@@ -156,20 +143,6 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
);
show.remote_next = crate::presenter::Preview::from_image(&crate::textures::to_color_image(&image, false)).map(|image| (n, image));
}
if !show.presenter_grid {
show.remote_grid.clear();
} else if show.remote_grid.len() < grid_indices(doc.slides.len()).len() {
// A few thumbnails per update keep the audience window responsive on the web.
for i in grid_indices(doc.slides.len()).into_iter().skip(show.remote_grid.len()).take(4) {
// At most 96 px on the long side: 48 thumbnails stay well under the popup's message limit.
let (w, h) = (doc.slide_size.width.max(1.0), doc.slide_size.height.max(1.0));
let scale = REMOTE_THUMB / w.max(h);
let size = (((w * scale).round() as u32).max(1), ((h * scale).round() as u32).max(1));
let image = deckcraft_render::render_slide(&doc, i, &RenderOpts { scale, size: Some(size), ..Default::default() });
let preview = crate::presenter::Preview::from_image(&crate::textures::to_color_image(&image, false));
show.remote_grid.push(preview.unwrap_or(crate::presenter::Preview { width: 1, height: 1, rgba: vec![0; 4] }));
}
}
host.update(crate::presenter::Snapshot {
slide: show.state.slide,
total: doc.slides.len(),
@@ -179,12 +152,10 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
paused: show.timer.paused(),
notes_px: show.notes_px,
blank: show.blank_name().map(str::to_owned),
grid: show.presenter_grid,
grid: show.grid,
zoom_armed: show.zoom_arm,
// The grid replaces both previews in the popup; leaving them out keeps one message small.
current: show.remote_preview.clone().filter(|_| !show.presenter_grid),
next: show.remote_next.as_ref().filter(|(n, _)| Some(*n) == next && !show.presenter_grid).map(|(_, image)| image.clone()),
slides: show.presenter_grid.then(|| show.remote_grid.clone()),
current: show.remote_preview.clone(),
next: show.remote_next.as_ref().filter(|(n, _)| Some(*n) == next).map(|(_, image)| image.clone()),
});
show.remote_sent = now;
}
@@ -193,9 +164,6 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
finish(app, show, &f, out.keep);
}
/// Longest side of an All slides thumbnail sent to the web presenter window (#1866).
const REMOTE_THUMB: f64 = 96.0;
fn paint_hidden(ui: &Ui, doc: &Presentation, show: &Show) {
if show.started {
let full = ui.max_rect();
+1 -1
View File
@@ -182,7 +182,7 @@ const WHEEL_NOTCHES_PER_DOUBLING: f32 = 4.0;
/// PowerPoint's slide show magnifier goes up to 400%.
const WHEEL_ZOOM_MAX: f32 = 4.0;
pub(crate) const GRID_CAP: usize = 48;
pub(super) const GRID_CAP: usize = 48;
const TOOL_BAR_W: f32 = 520.0;