Skip to content

Commit

Permalink
Pauses create slide chunks rather than slides
Browse files Browse the repository at this point in the history
  • Loading branch information
mfontanini committed Oct 21, 2023
1 parent 521dd43 commit fc7f04e
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 87 deletions.
48 changes: 23 additions & 25 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
},
presentation::{
AsRenderOperations, MarginProperties, PreformattedLine, Presentation, PresentationMetadata,
PresentationThemeMetadata, RenderOnDemand, RenderOnDemandState, RenderOperation, Slide,
PresentationThemeMetadata, RenderOnDemand, RenderOnDemandState, RenderOperation, Slide, SlideChunk,
},
render::{
highlighting::{CodeHighlighter, CodeLine},
Expand All @@ -31,6 +31,7 @@ static DEFAULT_BOTTOM_SLIDE_MARGIN: u16 = 3;
/// This type transforms [MarkdownElement]s and turns them into a presentation, which is made up of
/// render operations.
pub(crate) struct PresentationBuilder<'a> {
slide_chunks: Vec<SlideChunk>,
slide_operations: Vec<RenderOperation>,
slides: Vec<Slide>,
highlighter: CodeHighlighter,
Expand All @@ -51,6 +52,7 @@ impl<'a> PresentationBuilder<'a> {
resources: &'a mut Resources,
) -> Self {
Self {
slide_chunks: Vec::new(),
slide_operations: Vec::new(),
slides: Vec::new(),
highlighter: default_highlighter,
Expand Down Expand Up @@ -82,8 +84,8 @@ impl<'a> PresentationBuilder<'a> {
self.push_line_break();
}
}
if !self.slide_operations.is_empty() {
self.terminate_slide(TerminateMode::ResetState);
if !self.slide_operations.is_empty() || !self.slide_chunks.is_empty() {
self.terminate_slide();
}
self.footer_context.borrow_mut().total_slides = self.slides.len();

Expand Down Expand Up @@ -217,7 +219,7 @@ impl<'a> PresentationBuilder<'a> {
};
self.push_text(Text::from(text), ElementType::PresentationAuthor);
}
self.terminate_slide(TerminateMode::ResetState);
self.terminate_slide();
}

fn process_comment(&mut self, comment: String) -> Result<(), BuildError> {
Expand All @@ -228,7 +230,7 @@ impl<'a> PresentationBuilder<'a> {
let comment = comment.parse::<CommentCommand>()?;
match comment {
CommentCommand::Pause => self.process_pause(),
CommentCommand::EndSlide => self.terminate_slide(TerminateMode::ResetState),
CommentCommand::EndSlide => self.terminate_slide(),
CommentCommand::InitColumnLayout(columns) => {
Self::validate_column_layout(&columns)?;
self.layout = LayoutState::InLayout { columns_count: columns.len() };
Expand Down Expand Up @@ -276,9 +278,9 @@ impl<'a> PresentationBuilder<'a> {
self.slide_operations.pop();
}

let next_operations = self.slide_operations.clone();
self.terminate_slide(TerminateMode::KeepState);
self.slide_operations = next_operations;
let chunk_operations = mem::take(&mut self.slide_operations);
self.slide_chunks.push(SlideChunk::new(chunk_operations));
// self.terminate_slide(TerminateMode::KeepState);
}

fn push_slide_title(&mut self, mut text: Text) {
Expand Down Expand Up @@ -482,34 +484,35 @@ impl<'a> PresentationBuilder<'a> {
self.slide_operations.push(operation);
}

fn terminate_slide(&mut self, mode: TerminateMode) {
self.push_footer();
fn terminate_slide(&mut self) {
let footer = self.generate_footer();

let operations = mem::take(&mut self.slide_operations);
self.slides.push(Slide::new(operations));
self.slide_chunks.push(SlideChunk::new(operations));

let chunks = mem::take(&mut self.slide_chunks);
self.slides.push(Slide::new(chunks, footer));
self.push_slide_prelude();
if matches!(mode, TerminateMode::ResetState) {
self.ignore_element_line_break = true;
self.needs_enter_column = false;
self.layout = Default::default();
}
self.ignore_element_line_break = true;
self.needs_enter_column = false;
self.layout = Default::default();
}

fn push_footer(&mut self) {
fn generate_footer(&mut self) -> Vec<RenderOperation> {
let generator = FooterGenerator {
style: self.theme.footer.clone(),
current_slide: self.slides.len(),
context: self.footer_context.clone(),
};
self.slide_operations.extend([
vec![
// Exit any layout we're in so this gets rendered on a default screen size.
RenderOperation::ExitLayout,
// Pop the slide margin so we're at the terminal rect.
RenderOperation::PopMargin,
// Jump to the very bottom of the terminal rect and draw the footer.
RenderOperation::JumpToBottom,
RenderOperation::RenderDynamic(Rc::new(generator)),
]);
]
}

fn push_table(&mut self, table: Table) {
Expand Down Expand Up @@ -564,11 +567,6 @@ impl<'a> PresentationBuilder<'a> {
}
}

enum TerminateMode {
KeepState,
ResetState,
}

#[derive(Debug, Default)]
enum LayoutState {
#[default]
Expand Down Expand Up @@ -1078,6 +1076,6 @@ mod test {
fn pause_inside_layout() {
let elements = vec![build_column_layout(1), build_pause(), build_column(0)];
let presentation = build_presentation(elements);
assert_eq!(presentation.iter_slides().count(), 2);
assert_eq!(presentation.iter_slides().count(), 1);
}
}
134 changes: 94 additions & 40 deletions src/diff.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
use crate::presentation::{Presentation, RenderOperation, Slide};
use std::{cmp::Ordering, mem};
use crate::presentation::{Presentation, RenderOperation, SlideChunk};
use std::{cmp::Ordering, fmt::Debug, mem};

/// Allow diffing presentations.
pub(crate) struct PresentationDiffer;

impl PresentationDiffer {
/// Find the first modified slide between original and updated.
///
/// This tries to take into account both content and style changes such that changing
pub(crate) fn first_modified_slide(original: &Presentation, updated: &Presentation) -> Option<usize> {
/// Find the first modification between two presentations.
pub(crate) fn find_first_modification(original: &Presentation, updated: &Presentation) -> Option<Modification> {
let original_slides = original.iter_slides();
let updated_slides = updated.iter_slides();
for (index, (original, updated)) in original_slides.zip(updated_slides).enumerate() {
if original.is_content_different(updated) {
return Some(index);
for (slide_index, (original, updated)) in original_slides.zip(updated_slides).enumerate() {
for (chunk_index, (original, updated)) in original.iter_chunks().zip(updated.iter_chunks()).enumerate() {
if original.is_content_different(updated) {
return Some(Modification { slide_index, chunk_index });
}
}
let total_original = original.iter_chunks().count();
let total_updated = updated.iter_chunks().count();
match total_original.cmp(&total_updated) {
Ordering::Equal => (),
Ordering::Less => return Some(Modification { slide_index, chunk_index: total_original }),
Ordering::Greater => {
return Some(Modification { slide_index, chunk_index: total_updated.saturating_sub(1) });
}
}
}
let total_original = original.iter_slides().count();
Expand All @@ -22,18 +31,26 @@ impl PresentationDiffer {
// If they have the same number of slides there's no difference.
Ordering::Equal => None,
// If the original had fewer, let's scroll to the first new one.
Ordering::Less => Some(total_original),
Ordering::Less => Some(Modification { slide_index: total_original, chunk_index: 0 }),
// If the original had more, let's scroll to the last one.
Ordering::Greater => Some(total_updated.saturating_sub(1)),
Ordering::Greater => {
Some(Modification { slide_index: total_updated.saturating_sub(1), chunk_index: usize::MAX })
}
}
}
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Modification {
pub(crate) slide_index: usize,
pub(crate) chunk_index: usize,
}

trait ContentDiff {
fn is_content_different(&self, other: &Self) -> bool;
}

impl ContentDiff for Slide {
impl ContentDiff for SlideChunk {
fn is_content_different(&self, other: &Self) -> bool {
self.iter_operations().is_content_different(&other.iter_operations())
}
Expand Down Expand Up @@ -70,26 +87,27 @@ impl ContentDiff for RenderOperation {
impl<'a, T, U> ContentDiff for T
where
T: IntoIterator<Item = &'a U> + Clone,
U: ContentDiff + 'a,
// TODO no debu
U: ContentDiff + 'a + Debug,
{
fn is_content_different(&self, other: &Self) -> bool {
let mut lhs = self.clone().into_iter();
let mut rhs = other.clone().into_iter();
for (lhs, rhs) in lhs.by_ref().zip(rhs.by_ref()) {
let lhs = self.clone().into_iter();
let rhs = other.clone().into_iter();
for (lhs, rhs) in lhs.zip(rhs) {
if lhs.is_content_different(rhs) {
return true;
}
}
// If either have more than the other, they've changed
lhs.next().is_some() != rhs.next().is_some()
self.clone().into_iter().count() != other.clone().into_iter().count()
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::{
presentation::{AsRenderOperations, PreformattedLine},
presentation::{AsRenderOperations, PreformattedLine, Slide},
render::properties::WindowSize,
style::{Color, Colors},
theme::{Alignment, Margin},
Expand Down Expand Up @@ -157,62 +175,98 @@ mod test {
#[test]
fn no_slide_changes() {
let presentation = Presentation::new(vec![
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
]);
assert_eq!(PresentationDiffer::first_modified_slide(&presentation, &presentation), None);
assert_eq!(PresentationDiffer::find_first_modification(&presentation, &presentation), None);
}

#[test]
fn slides_truncated() {
let lhs = Presentation::new(vec![
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
]);
let rhs = Presentation::new(vec![Slide::new(vec![RenderOperation::JumpToBottom])]);
let rhs = Presentation::new(vec![Slide::from(vec![RenderOperation::JumpToBottom])]);

assert_eq!(PresentationDiffer::first_modified_slide(&lhs, &rhs), Some(0));
assert_eq!(
PresentationDiffer::find_first_modification(&lhs, &rhs),
Some(Modification { slide_index: 0, chunk_index: usize::MAX })
);
}

#[test]
fn slides_added() {
let lhs = Presentation::new(vec![Slide::new(vec![RenderOperation::JumpToBottom])]);
let lhs = Presentation::new(vec![Slide::from(vec![RenderOperation::JumpToBottom])]);
let rhs = Presentation::new(vec![
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
]);

assert_eq!(PresentationDiffer::first_modified_slide(&lhs, &rhs), Some(1));
assert_eq!(
PresentationDiffer::find_first_modification(&lhs, &rhs),
Some(Modification { slide_index: 1, chunk_index: 0 })
);
}

#[test]
fn second_slide_content_changed() {
let lhs = Presentation::new(vec![
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
]);
let rhs = Presentation::new(vec![
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::new(vec![RenderOperation::JumpToVerticalCenter]),
Slide::new(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::from(vec![RenderOperation::JumpToVerticalCenter]),
Slide::from(vec![RenderOperation::JumpToBottom]),
]);

assert_eq!(PresentationDiffer::first_modified_slide(&lhs, &rhs), Some(1));
assert_eq!(
PresentationDiffer::find_first_modification(&lhs, &rhs),
Some(Modification { slide_index: 1, chunk_index: 0 })
);
}

#[test]
fn presentation_changed_style() {
let lhs = Presentation::new(vec![Slide::new(vec![RenderOperation::SetColors(Colors {
let lhs = Presentation::new(vec![Slide::from(vec![RenderOperation::SetColors(Colors {
background: None,
foreground: Some(Color::new(255, 0, 0)),
})])]);
let rhs = Presentation::new(vec![Slide::new(vec![RenderOperation::SetColors(Colors {
let rhs = Presentation::new(vec![Slide::from(vec![RenderOperation::SetColors(Colors {
background: None,
foreground: Some(Color::new(0, 0, 0)),
})])]);

assert_eq!(PresentationDiffer::first_modified_slide(&lhs, &rhs), None);
assert_eq!(PresentationDiffer::find_first_modification(&lhs, &rhs), None);
}

#[test]
fn chunk_change() {
let lhs = Presentation::new(vec![
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::new(vec![SlideChunk::default(), SlideChunk::new(vec![RenderOperation::JumpToBottom])], vec![]),
]);
let rhs = Presentation::new(vec![
Slide::from(vec![RenderOperation::JumpToBottom]),
Slide::new(
vec![
SlideChunk::default(),
SlideChunk::new(vec![RenderOperation::JumpToBottom, RenderOperation::JumpToBottom]),
],
vec![],
),
]);

assert_eq!(
PresentationDiffer::find_first_modification(&lhs, &rhs),
Some(Modification { slide_index: 1, chunk_index: 1 })
);
assert_eq!(
PresentationDiffer::find_first_modification(&rhs, &lhs),
Some(Modification { slide_index: 1, chunk_index: 1 })
);
}
}
Loading

0 comments on commit fc7f04e

Please sign in to comment.