use std::path::Path;
use eframe::egui::{self, Rect, Sense, Stroke, StrokeKind, Ui, UiBuilder, pos2, vec2};
use odox_core::doc::Presentation;
use odox_core::{Element, Length, Ns};
use odox_ui::i18n::{fill, t};
use odox_ui::{Flow, Pictures, Viewer, fonts};
#[derive(Default)]
pub struct SlideView {
document: Option<Presentation>,
slide: usize,
pictures: Pictures,
show_notes: bool,
}
impl Viewer for SlideView {
fn open(&mut self, ctx: &egui::Context, bytes: &[u8], path: &Path) -> Result<(), String> {
let document = Presentation::read(bytes).map_err(|e| {
fill(
t("{file} could not be opened: {reason}"),
&[
(
"file",
&path.file_name().unwrap_or_default().to_string_lossy(),
),
("reason", &e.to_string()),
],
)
})?;
let mut parts = vec![&document.document.content];
if let Some(styles) = &document.document.styles_part {
parts.push(styles);
}
ctx.set_fonts(fonts::definitions(&fonts::families_used(&parts)));
self.pictures.clear();
self.document = Some(document);
self.slide = 0;
Ok(())
}
fn close(&mut self) {
self.document = None;
self.pictures.clear();
}
fn is_open(&self) -> bool {
self.document.is_some()
}
fn title(&self) -> Option<String> {
self.document.as_ref()?.document.meta.title.clone()
}
fn central(&mut self, ui: &mut Ui, zoom: f32) {
let count = self
.document
.as_ref()
.map_or(0, |document| document.slides().len());
if count == 0 {
ui.centered_and_justified(|ui| {
ui.weak(t("This presentation has no slides."));
});
return;
}
self.step_keys(ui, count);
let slide_index = self.slide;
let show_notes = self.show_notes;
let Some(document) = &self.document else {
return;
};
let pictures = &mut self.pictures;
let slides = document.slides();
let Some(slide) = slides.get(slide_index) else {
return;
};
if show_notes {
let notes = slide.notes();
egui::Panel::bottom("notes")
.default_size(160.0)
.resizable(true)
.show(ui, |ui| {
ui.heading(t("Notes"));
egui::ScrollArea::vertical().show(ui, |ui| match notes {
Some(notes) => {
let width = ui.available_width();
Flow::new(&document.document, pictures, zoom).blocks(ui, notes, width);
}
None => {
ui.weak(t("This slide has no notes."));
}
});
});
}
let layout = document.page_layout(slide);
let available = ui.available_size();
let fit = (available.x / layout.width.points())
.min(available.y / layout.height.points())
.max(0.01)
* zoom;
let size = vec2(layout.width.points() * fit, layout.height.points() * fit);
egui::ScrollArea::both().show(ui, |ui| {
ui.vertical_centered(|ui| {
let (page, _) = ui.allocate_exact_size(size, Sense::hover());
let palette = odox_ui::format::Palette::default();
ui.painter().rect_filled(page, 2.0, palette.paper);
ui.painter().rect_stroke(
page,
2.0,
Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color),
StrokeKind::Inside,
);
for shape in slide.shapes() {
draw_shape(ui, document, pictures, shape, page, fit);
}
});
});
}
fn side(&mut self, ui: &mut Ui) -> bool {
let Some(document) = &self.document else {
return false;
};
let slides = document.slides();
if slides.is_empty() {
return false;
}
ui.heading(t("Slides"));
ui.separator();
for (index, slide) in slides.iter().enumerate() {
let heading = slide
.shapes()
.map(odox_core::Element::plain_text)
.find(|text| !text.trim().is_empty())
.unwrap_or_else(|| slide.name.unwrap_or_default().to_owned());
let label = format!("{}. {}", index + 1, first_line(&heading));
if ui.selectable_label(index == self.slide, label).clicked() {
self.slide = index;
}
}
true
}
fn view_menu(&mut self, ui: &mut Ui) {
ui.separator();
ui.checkbox(&mut self.show_notes, t("Show the speaker's notes"));
}
}
fn draw_shape(
ui: &mut Ui,
document: &Presentation,
pictures: &mut Pictures,
shape: &Element,
page: Rect,
scale: f32,
) {
let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
let (Some(x), Some(y)) = (at("x"), at("y")) else {
return;
};
let width = at("width").map_or(page.width() / 2.0, |w| w.points() * scale);
let height = at("height").map_or(page.height() / 4.0, |h| h.points() * scale);
let rect = Rect::from_min_size(
pos2(
page.left() + x.points() * scale,
page.top() + y.points() * scale,
),
vec2(width, height),
);
let content = shape
.child(&Ns::Draw, "text-box")
.or_else(|| shape.child(&Ns::Draw, "image").map(|_| shape));
if let Some(content) = content {
ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
ui.set_clip_rect(rect.intersect(page));
Flow::new(&document.document, pictures, scale).blocks(ui, content, rect.width());
});
return;
}
ui.painter().rect_stroke(
rect,
0.0,
Stroke::new(1.0, ui.visuals().weak_text_color().gamma_multiply(0.5)),
StrokeKind::Inside,
);
}
impl SlideView {
fn step_keys(&mut self, ui: &Ui, count: usize) {
let mut slide = self.slide;
ui.input(|input| {
let forward = input.key_pressed(egui::Key::PageDown)
|| input.key_pressed(egui::Key::ArrowRight)
|| input.key_pressed(egui::Key::ArrowDown);
let back = input.key_pressed(egui::Key::PageUp)
|| input.key_pressed(egui::Key::ArrowLeft)
|| input.key_pressed(egui::Key::ArrowUp);
if forward {
slide = slide.saturating_add(1);
}
if back {
slide = slide.saturating_sub(1);
}
if input.key_pressed(egui::Key::Home) {
slide = 0;
}
if input.key_pressed(egui::Key::End) {
slide = count.saturating_sub(1);
}
});
self.slide = slide.min(count.saturating_sub(1));
}
}
fn first_line(text: &str) -> String {
let line = text
.lines()
.find(|line| !line.trim().is_empty())
.unwrap_or_default()
.trim();
if line.chars().count() > 48 {
let short: String = line.chars().take(47).collect();
return format!("{short}\u{2026}");
}
line.to_owned()
}