Add slide index modal

This commit is contained in:
Matias Fontanini 2024-01-10 19:17:14 -08:00
parent ceeac45b75
commit 6ff3098552
10 changed files with 358 additions and 100 deletions

View File

@ -134,7 +134,6 @@ impl Person {
println!("hello, I'm {}", self.name)
}
}
```
<!-- end_slide -->

View File

@ -10,7 +10,8 @@ use crate::{
},
presentation::{
AsRenderOperations, ChunkMutator, MarginProperties, PreformattedLine, Presentation, PresentationMetadata,
PresentationThemeMetadata, RenderOnDemand, RenderOnDemandState, RenderOperation, Slide, SlideChunk,
PresentationState, PresentationThemeMetadata, RenderOnDemand, RenderOnDemandState, RenderOperation, Slide,
SlideBuilder, SlideChunk,
},
render::{
highlighting::{CodeHighlighter, HighlightThemeSet, LanguageHighlighter, StyledTokens},
@ -87,6 +88,7 @@ pub(crate) struct PresentationBuilder<'a> {
slide_state: SlideState,
footer_context: Rc<RefCell<FooterContext>>,
themes: &'a Themes,
index_builder: IndexBuilder,
options: PresentationBuilderOptions,
}
@ -112,6 +114,7 @@ impl<'a> PresentationBuilder<'a> {
slide_state: Default::default(),
footer_context: Default::default(),
themes,
index_builder: Default::default(),
options,
}
}
@ -146,7 +149,10 @@ impl<'a> PresentationBuilder<'a> {
}
self.footer_context.borrow_mut().total_slides = self.slides.len();
let presentation = Presentation::new(self.slides);
// TODO consider a separate color palette
let presentation_state = PresentationState::default();
let index = self.index_builder.build(self.theme.default_style.colors.clone(), presentation_state.clone());
let presentation = Presentation::new(self.slides, index, presentation_state);
Ok(presentation)
}
@ -289,6 +295,7 @@ impl<'a> PresentationBuilder<'a> {
};
self.push_text(Text::from(text), ElementType::PresentationAuthor);
}
self.slide_state.title = Some(Text::from("[Introduction]"));
self.terminate_slide();
}
@ -374,6 +381,10 @@ impl<'a> PresentationBuilder<'a> {
self.terminate_slide();
}
if self.slide_state.title.is_none() {
self.slide_state.title = Some(text.clone());
}
let style = self.theme.slide_title.clone();
text.apply_style(&TextStyle::default().bold().colors(style.colors.clone()));
@ -625,7 +636,12 @@ impl<'a> PresentationBuilder<'a> {
self.slide_chunks.push(SlideChunk::new(operations, mutators));
let chunks = mem::take(&mut self.slide_chunks);
self.slides.push(Slide::new(chunks, footer));
let slide = SlideBuilder::default().chunks(chunks).footer(footer).build();
self.index_builder
.titles
.push(self.slide_state.title.take().unwrap_or_else(|| StyledText::from("<no title>").into()));
self.slides.push(slide);
self.push_slide_prelude();
self.slide_state = Default::default();
self.slide_state.last_element = LastElement::None;
@ -698,6 +714,124 @@ impl<'a> PresentationBuilder<'a> {
}
}
#[derive(Default)]
struct IndexBuilder {
titles: Vec<Text>,
}
impl IndexBuilder {
fn build(self, colors: Colors, state: PresentationState) -> Vec<RenderOperation> {
if self.titles.is_empty() {
return Vec::new();
}
let heading = "Slides";
let longest_line = self.titles.iter().map(Text::width).max().unwrap_or(0) as u16;
let longest_line = longest_line.max(heading.len() as u16);
// Ensure we have a minimum width so it doesn't look too narrow.
let longest_line = longest_line.max(12);
let numbers_length = self.titles.len().ilog10() as usize + 1;
// The final text looks like "| <number>: <content> |"
let content_width = longest_line + numbers_length as u16 + 6;
let mut prefix = vec![RenderOperation::SetColors(colors)];
prefix.extend(Self::make_horizontal_border(content_width, '┌', '┐'));
prefix.extend([
RenderOperation::RenderText {
line: Self::build_line(heading, [], content_width),
alignment: Default::default(),
},
RenderOperation::RenderLineBreak,
]);
prefix.extend(Self::make_horizontal_border(content_width, '├', '┤'));
let mut titles = Vec::new();
for (index, title) in self.titles.into_iter().enumerate() {
let index = pad_right(index + 1, numbers_length);
titles.push(Self::build_line(format!("{index}:"), title.chunks, content_width));
}
let suffix = Self::make_horizontal_border(content_width, '└', '┘').into_iter().collect();
let drawer = IndexDrawer { prefix, titles, suffix, state, content_width };
vec![RenderOperation::RenderDynamic(Rc::new(drawer))]
}
fn build_line<S, C>(prefix: S, text_chunks: C, content_width: u16) -> WeightedLine
where
S: Display,
C: IntoIterator<Item = StyledText>,
{
let mut chunks = vec![WeightedText::from(format!("{prefix} "))];
chunks.extend(text_chunks.into_iter().map(WeightedText::from));
let missing = content_width as usize - 1 - chunks.iter().map(|c| c.width()).sum::<usize>();
chunks.extend([WeightedText::from(" ".repeat(missing)), WeightedText::from("")]);
WeightedLine::from(chunks)
}
fn make_horizontal_border(content_length: u16, opening: char, closing: char) -> [RenderOperation; 2] {
let mut line = String::from(opening);
line.push_str(&"".repeat((content_length - 2) as usize));
line.push(closing);
let horizontal_border = WeightedLine::from(vec![WeightedText::from(line)]);
[
RenderOperation::RenderText { line: horizontal_border.clone(), alignment: Default::default() },
RenderOperation::RenderLineBreak,
]
}
}
#[derive(Debug)]
struct IndexDrawer {
prefix: Vec<RenderOperation>,
titles: Vec<WeightedLine>,
suffix: Vec<RenderOperation>,
content_width: u16,
state: PresentationState,
}
impl IndexDrawer {
fn initialize_layout(&self, dimensions: &WindowSize, visible_titles: usize) -> Vec<RenderOperation> {
let margin = dimensions.columns.saturating_sub(self.content_width) / 2;
let properties = MarginProperties { horizontal_margin: Margin::Fixed(margin), bottom_slide_margin: 0 };
// However many we see + 3 for the title and 1 at the bottom.
let content_height = (visible_titles + 4) as u16;
let target_row = dimensions.rows.saturating_sub(content_height) / 2;
vec![RenderOperation::ApplyMargin(properties), RenderOperation::JumpToRow { index: target_row }]
}
}
impl AsRenderOperations for IndexDrawer {
fn as_render_operations(&self, dimensions: &WindowSize) -> Vec<RenderOperation> {
let current_slide_index = self.state.current_slide_index();
let max_rows = (dimensions.rows as f64 * 0.8) as u16;
let titles = self.titles.iter().cloned();
let (skip, take) = match titles.len() as u16 > max_rows {
true => {
let start = (current_slide_index as u16).saturating_sub(max_rows / 2);
let start = start.min(titles.len() as u16 - max_rows);
(start as usize, max_rows as usize)
}
false => (0, titles.len()),
};
let visible_titles = self.titles.iter().cloned().enumerate().skip(skip).take(take);
let mut operations = self.initialize_layout(dimensions, take);
operations.extend(self.prefix.iter().cloned());
for (index, mut title) in visible_titles {
if index == current_slide_index {
title.apply_style(TextStyle::default().bold());
}
let operation = RenderOperation::RenderText { line: title, alignment: Default::default() };
operations.extend([operation, RenderOperation::RenderLineBreak]);
}
operations.extend(self.suffix.iter().cloned());
operations
}
fn diffable_content(&self) -> Option<&str> {
// The index is just a view over the underlying data so it won't change in isolation.
None
}
}
struct CodePreparer<'a> {
theme: &'a PresentationTheme,
}
@ -723,17 +857,13 @@ impl<'a> CodePreparer<'a> {
}
let padding = " ".repeat(horizontal_padding as usize);
let total_lines_width = code.contents.lines().count().ilog10();
let total_lines_width = code.contents.lines().count().ilog10() as usize + 1;
for (index, line) in code.contents.lines().enumerate() {
let mut line = line.to_string();
let mut prefix = padding.clone();
if code.attributes.line_numbers {
let line_number = index + 1;
let line_number_width = line_number.ilog10();
// Suffix this with padding to make all numbers pad to the right
let number_padding = total_lines_width - line_number_width;
prefix.push_str(&" ".repeat(number_padding as usize));
prefix.push_str(&line_number.to_string());
prefix.push_str(&pad_right(line_number, total_lines_width));
prefix.push(' ');
}
line.push('\n');
@ -859,6 +989,7 @@ struct SlideState {
last_element: LastElement,
incremental_lists: Option<bool>,
layout: LayoutState,
title: Option<Text>,
}
#[derive(Debug, Default)]
@ -1235,6 +1366,16 @@ struct IndexedListItem {
item: ListItem,
}
fn pad_right(number: usize, pad: usize) -> String {
let line_number_width = number.ilog10() as usize + 1;
let number_padding = pad - line_number_width;
let mut output = String::new();
output.extend(iter::repeat(' ').take(number_padding));
output.push_str(&number.to_string());
output
}
#[cfg(test)]
mod test {
use super::*;
@ -1291,6 +1432,7 @@ mod test {
ClearScreen
| SetColors(_)
| JumpToVerticalCenter
| JumpToRow { .. }
| JumpToBottomRow { .. }
| InitColumnLayout { .. }
| EnterColumn { .. }

View File

@ -108,7 +108,7 @@ where
mod test {
use super::*;
use crate::{
presentation::{AsRenderOperations, PreformattedLine, Slide},
presentation::{AsRenderOperations, PreformattedLine, Slide, SlideBuilder},
render::properties::WindowSize,
style::{Color, Colors},
theme::{Alignment, Margin},
@ -196,7 +196,7 @@ mod test {
#[test]
fn no_slide_changes() {
let presentation = Presentation::new(vec![
let presentation = Presentation::from(vec![
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::from(vec![RenderOperation::ClearScreen]),
@ -206,11 +206,11 @@ mod test {
#[test]
fn slides_truncated() {
let lhs = Presentation::new(vec![
let lhs = Presentation::from(vec![
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::from(vec![RenderOperation::ClearScreen]),
]);
let rhs = Presentation::new(vec![Slide::from(vec![RenderOperation::ClearScreen])]);
let rhs = Presentation::from(vec![Slide::from(vec![RenderOperation::ClearScreen])]);
assert_eq!(
PresentationDiffer::find_first_modification(&lhs, &rhs),
@ -220,8 +220,8 @@ mod test {
#[test]
fn slides_added() {
let lhs = Presentation::new(vec![Slide::from(vec![RenderOperation::ClearScreen])]);
let rhs = Presentation::new(vec![
let lhs = Presentation::from(vec![Slide::from(vec![RenderOperation::ClearScreen])]);
let rhs = Presentation::from(vec![
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::from(vec![RenderOperation::ClearScreen]),
]);
@ -234,12 +234,12 @@ mod test {
#[test]
fn second_slide_content_changed() {
let lhs = Presentation::new(vec![
let lhs = Presentation::from(vec![
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::from(vec![RenderOperation::ClearScreen]),
]);
let rhs = Presentation::new(vec![
let rhs = Presentation::from(vec![
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::from(vec![RenderOperation::JumpToVerticalCenter]),
Slide::from(vec![RenderOperation::ClearScreen]),
@ -253,11 +253,11 @@ mod test {
#[test]
fn presentation_changed_style() {
let lhs = Presentation::new(vec![Slide::from(vec![RenderOperation::SetColors(Colors {
let lhs = Presentation::from(vec![Slide::from(vec![RenderOperation::SetColors(Colors {
background: None,
foreground: Some(Color::new(255, 0, 0)),
})])]);
let rhs = Presentation::new(vec![Slide::from(vec![RenderOperation::SetColors(Colors {
let rhs = Presentation::from(vec![Slide::from(vec![RenderOperation::SetColors(Colors {
background: None,
foreground: Some(Color::new(0, 0, 0)),
})])]);
@ -267,22 +267,20 @@ mod test {
#[test]
fn chunk_change() {
let lhs = Presentation::new(vec![
let lhs = Presentation::from(vec![
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::new(
vec![SlideChunk::default(), SlideChunk::new(vec![RenderOperation::ClearScreen], vec![])],
vec![],
),
SlideBuilder::default()
.chunks(vec![SlideChunk::default(), SlideChunk::new(vec![RenderOperation::ClearScreen], vec![])])
.build(),
]);
let rhs = Presentation::new(vec![
let rhs = Presentation::from(vec![
Slide::from(vec![RenderOperation::ClearScreen]),
Slide::new(
vec![
SlideBuilder::default()
.chunks(vec![
SlideChunk::default(),
SlideChunk::new(vec![RenderOperation::ClearScreen, RenderOperation::ClearScreen], vec![]),
],
vec![],
),
])
.build(),
]);
assert_eq!(

View File

@ -64,4 +64,7 @@ pub(crate) enum Command {
///
/// Like [Command::Reload] but also reloads any external resources like images and themes.
HardReload,
/// Toggle the slide index view.
ToggleSlideIndex,
}

View File

@ -53,6 +53,9 @@ impl UserInput {
KeyCode::Char('r') if event.modifiers == KeyModifiers::CONTROL => {
(Some(Command::HardReload), InputState::Empty)
}
KeyCode::Char('p') if event.modifiers == KeyModifiers::CONTROL => {
(Some(Command::ToggleSlideIndex), InputState::Empty)
}
_ => (None, InputState::Empty),
}
}

View File

@ -24,6 +24,13 @@ impl WeightedLine {
pub(crate) fn iter_texts(&self) -> impl Iterator<Item = &WeightedText> {
self.0.iter()
}
/// Apply a style to all text chunks in this line.
pub(crate) fn apply_style(&mut self, style: TextStyle) {
for text in &mut self.0 {
text.text.style = style.clone();
}
}
}
impl From<Vec<WeightedText>> for WeightedLine {
@ -34,7 +41,7 @@ impl From<Vec<WeightedText>> for WeightedLine {
impl From<String> for WeightedLine {
fn from(text: String) -> Self {
let texts = vec![WeightedText::from(StyledText::from(text))];
let texts = vec![WeightedText::from(text)];
Self(texts)
}
}
@ -57,11 +64,17 @@ impl WeightedText {
WeightedTextRef { text: &self.text.text, accumulators: &self.accumulators, style: self.text.style.clone() }
}
fn width(&self) -> usize {
pub(crate) fn width(&self) -> usize {
self.accumulators.last().map(|a| a.width).unwrap_or(0)
}
}
impl<S: Into<String>> From<S> for WeightedText {
fn from(text: S) -> Self {
Self::from(StyledText::from(text.into()))
}
}
impl From<StyledText> for WeightedText {
fn from(text: StyledText) -> Self {
let mut accumulators = Vec::new();
@ -199,7 +212,7 @@ mod test {
#[test]
fn text_creation() {
let text = WeightedText::from(StyledText::from("hello world"));
let text = WeightedText::from("hello world");
let text_ref = text.to_ref();
assert_eq!(text_ref.width(), 11);
@ -207,7 +220,7 @@ mod test {
#[test]
fn text_creation_utf8() {
let text = WeightedText::from(StyledText::from("█████"));
let text = WeightedText::from("█████");
let text_ref = text.to_ref();
assert_eq!(text_ref.width(), 5);
@ -232,7 +245,7 @@ mod test {
#[test]
fn minimal_split() {
let text = WeightedText::from(StyledText::from("█████"));
let text = WeightedText::from("█████");
let text_ref = text.to_ref();
let (head, rest) = text_ref.word_split_at_length(1);
assert_eq!(head.width(), 1);
@ -241,7 +254,7 @@ mod test {
#[test]
fn no_spaces_split() {
let text = WeightedText::from(StyledText::from("█████"));
let text = WeightedText::from("█████");
let text_ref = text.to_ref();
let (head, rest) = text_ref.word_split_at_length(2);
assert_eq!(head.width(), 2);
@ -250,7 +263,7 @@ mod test {
#[test]
fn make_ref() {
let text = WeightedText::from(StyledText::from("hello world"));
let text = WeightedText::from("hello world");
let text_ref = text.to_ref();
let head = text_ref.make_ref(0, 1);
assert_eq!(head.text, "h");
@ -263,7 +276,7 @@ mod test {
#[test]
fn word_split() {
let text = WeightedText::from(StyledText::from("short string"));
let text = WeightedText::from("short string");
let (head, rest) = text.to_ref().word_split_at_length(7);
assert_eq!(head.text, "short");
assert_eq!(rest.text, " string");
@ -271,7 +284,7 @@ mod test {
#[test]
fn split_at_full_length() {
let text = WeightedLine(vec![WeightedText::from(StyledText::from("hello world"))]);
let text = WeightedLine(vec![WeightedText::from("hello world")]);
let lines = join_lines(text.split(11));
let expected = vec!["hello world"];
assert_eq!(lines, expected);
@ -279,10 +292,7 @@ mod test {
#[test]
fn no_split_necessary() {
let text = WeightedLine(vec![
WeightedText::from(StyledText::from("short")),
WeightedText::from(StyledText::from("text")),
]);
let text = WeightedLine(vec![WeightedText::from("short"), WeightedText::from("text")]);
let lines = join_lines(text.split(50));
let expected = vec!["short text"];
assert_eq!(lines, expected);
@ -290,7 +300,7 @@ mod test {
#[test]
fn split_lines_single() {
let text = WeightedLine(vec![WeightedText::from(StyledText::from("this is a slightly long line"))]);
let text = WeightedLine(vec![WeightedText::from("this is a slightly long line")]);
let lines = join_lines(text.split(6));
let expected = vec!["this", "is a", "slight", "ly", "long", "line"];
assert_eq!(lines, expected);
@ -299,9 +309,9 @@ mod test {
#[test]
fn split_lines_multi() {
let text = WeightedLine(vec![
WeightedText::from(StyledText::from("this is a slightly long line")),
WeightedText::from(StyledText::from("another chunk")),
WeightedText::from(StyledText::from("yet some other piece")),
WeightedText::from("this is a slightly long line"),
WeightedText::from("another chunk"),
WeightedText::from("yet some other piece"),
]);
let lines = join_lines(text.split(10));
let expected = vec!["this is a", "slightly", "long line", "another", "chunk yet", "some other", "piece"];
@ -311,9 +321,9 @@ mod test {
#[test]
fn long_splits() {
let text = WeightedLine(vec![
WeightedText::from(StyledText::from("this is a slightly long line")),
WeightedText::from(StyledText::from("another chunk")),
WeightedText::from(StyledText::from("yet some other piece")),
WeightedText::from("this is a slightly long line"),
WeightedText::from("another chunk"),
WeightedText::from("yet some other piece"),
]);
let lines = join_lines(text.split(50));
let expected = vec!["this is a slightly long line another chunk yet some", "other piece"];
@ -322,7 +332,7 @@ mod test {
#[test]
fn prefixed_by_whitespace() {
let text = WeightedLine(vec![WeightedText::from(StyledText::from(" * bullet"))]);
let text = WeightedLine(vec![WeightedText::from(" * bullet")]);
let lines = join_lines(text.split(50));
let expected = vec![" * bullet"];
assert_eq!(lines, expected);
@ -330,7 +340,7 @@ mod test {
#[test]
fn utf8_character() {
let text = WeightedLine(vec![WeightedText::from(StyledText::from("• A"))]);
let text = WeightedLine(vec![WeightedText::from("• A")]);
let lines = join_lines(text.split(50));
let expected = vec!["• A"];
assert_eq!(lines, expected);
@ -339,7 +349,7 @@ mod test {
#[test]
fn many_utf8_characters() {
let content = "█████ ██";
let text = WeightedLine(vec![WeightedText::from(StyledText::from(content))]);
let text = WeightedLine(vec![WeightedText::from(content)]);
let lines = join_lines(text.split(3));
let expected = vec!["███", "██", "██"];
assert_eq!(lines, expected);
@ -348,7 +358,7 @@ mod test {
#[test]
fn no_whitespaces_ascii() {
let content = "X".repeat(10);
let text = WeightedLine(vec![WeightedText::from(StyledText::from(content))]);
let text = WeightedLine(vec![WeightedText::from(content)]);
let lines = join_lines(text.split(3));
let expected = vec!["XXX", "XXX", "XXX", "X"];
assert_eq!(lines, expected);
@ -357,7 +367,7 @@ mod test {
#[test]
fn no_whitespaces_utf8() {
let content = "".repeat(10);
let text = WeightedLine(vec![WeightedText::from(StyledText::from(content))]);
let text = WeightedLine(vec![WeightedText::from(content)]);
let lines = join_lines(text.split(3));
let expected = vec!["───", "───", "───", ""];
assert_eq!(lines, expected);
@ -366,7 +376,7 @@ mod test {
#[test]
fn wide_characters() {
let content = " ";
let text = WeightedLine(vec![WeightedText::from(StyledText::from(content))]);
let text = WeightedLine(vec![WeightedText::from(content)]);
let lines = join_lines(text.split(10));
// Each word is 10 characters long
let expected = vec!["", ""];

View File

@ -6,19 +6,20 @@ use crate::{
theme::{Alignment, Margin, PresentationTheme},
};
use serde::Deserialize;
use std::{fmt::Debug, rc::Rc};
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
/// A presentation.
#[derive(Debug)]
pub(crate) struct Presentation {
slides: Vec<Slide>,
current_slide_index: usize,
slide_index: Vec<RenderOperation>,
state: PresentationState,
}
impl Presentation {
/// Construct a new presentation.
pub(crate) fn new(slides: Vec<Slide>) -> Self {
Self { slides, current_slide_index: 0 }
pub(crate) fn new(slides: Vec<Slide>, slide_index: Vec<RenderOperation>, state: PresentationState) -> Self {
Self { slides, slide_index, state }
}
/// Iterate the slides in this presentation.
@ -26,6 +27,11 @@ impl Presentation {
self.slides.iter()
}
/// Iterate the operations that render the slide index.
pub(crate) fn iter_index_operations(&self) -> impl Iterator<Item = &RenderOperation> {
self.slide_index.iter()
}
/// Consume this presentation and return its slides.
#[cfg(test)]
pub(crate) fn into_slides(self) -> Vec<Slide> {
@ -34,12 +40,12 @@ impl Presentation {
/// Get the current slide.
pub(crate) fn current_slide(&self) -> &Slide {
&self.slides[self.current_slide_index]
&self.slides[self.current_slide_index()]
}
/// Get the current slide index.
pub(crate) fn current_slide_index(&self) -> usize {
self.current_slide_index
self.state.current_slide_index()
}
/// Jump to the next slide.
@ -48,8 +54,9 @@ impl Presentation {
if current_slide.move_next() {
return true;
}
if self.current_slide_index < self.slides.len() - 1 {
self.current_slide_index += 1;
let current_slide_index = self.current_slide_index();
if current_slide_index < self.slides.len() - 1 {
self.state.set_current_slide_index(current_slide_index + 1);
// Going forward we show only the first chunk.
self.current_slide_mut().show_first_chunk();
true
@ -64,8 +71,9 @@ impl Presentation {
if current_slide.move_previous() {
return true;
}
if self.current_slide_index > 0 {
self.current_slide_index -= 1;
let current_slide_index = self.current_slide_index();
if current_slide_index > 0 {
self.state.set_current_slide_index(current_slide_index - 1);
// Going backwards we show all chunks.
self.current_slide_mut().show_all_chunks();
true
@ -88,7 +96,7 @@ impl Presentation {
/// Jump to a specific slide.
pub(crate) fn jump_slide(&mut self, slide_index: usize) -> bool {
if slide_index < self.slides.len() {
self.current_slide_index = slide_index;
self.state.set_current_slide_index(slide_index);
// Always show only the first slide when jumping to a particular one.
self.current_slide_mut().show_first_chunk();
true
@ -148,7 +156,57 @@ impl Presentation {
}
fn current_slide_mut(&mut self) -> &mut Slide {
&mut self.slides[self.current_slide_index]
let index = self.current_slide_index();
&mut self.slides[index]
}
}
impl From<Vec<Slide>> for Presentation {
fn from(slides: Vec<Slide>) -> Self {
Self::new(slides, vec![], Default::default())
}
}
#[derive(Debug, Default)]
pub(crate) struct PresentationStateInner {
current_slide_index: usize,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct PresentationState {
inner: Rc<RefCell<PresentationStateInner>>,
}
impl PresentationState {
pub(crate) fn current_slide_index(&self) -> usize {
self.inner.deref().borrow().current_slide_index
}
fn set_current_slide_index(&self, value: usize) {
self.inner.deref().borrow_mut().current_slide_index = value;
}
}
/// A slide builder.
#[derive(Default)]
pub(crate) struct SlideBuilder {
chunks: Vec<SlideChunk>,
footer: Vec<RenderOperation>,
}
impl SlideBuilder {
pub(crate) fn chunks(mut self, chunks: Vec<SlideChunk>) -> Self {
self.chunks = chunks;
self
}
pub(crate) fn footer(mut self, footer: Vec<RenderOperation>) -> Self {
self.footer = footer;
self
}
pub(crate) fn build(self) -> Slide {
Slide::new(self.chunks, self.footer)
}
}
@ -372,7 +430,12 @@ pub(crate) enum RenderOperation {
/// Jump the draw cursor into the vertical center, that is, at `screen_height / 2`.
JumpToVerticalCenter,
/// Jumps to the N-th to last row in the slide.
/// Jumps to the N-th row in the current layout.
///
/// The index is zero based where 0 represents the top row.
JumpToRow { index: u16 },
/// Jumps to the N-th to last row in the current layout.
///
/// The index is zero based where 0 represents the bottom row.
JumpToBottomRow { index: u16 },
@ -555,7 +618,7 @@ mod test {
#[case] expected_slide: usize,
#[case] expected_chunk: usize,
) {
let mut presentation = Presentation::new(vec![
let mut presentation = Presentation::from(vec![
Slide::new(vec![SlideChunk::from(SlideChunk::default()), SlideChunk::default()], vec![]),
Slide::new(vec![SlideChunk::from(SlideChunk::default()), SlideChunk::default()], vec![]),
Slide::new(vec![SlideChunk::from(SlideChunk::default()), SlideChunk::default()], vec![]),
@ -592,24 +655,22 @@ mod test {
#[case] expected_slide: usize,
#[case] expected_chunk: usize,
) {
let mut presentation = Presentation::new(vec![
Slide::new(
vec![
let mut presentation = Presentation::from(vec![
SlideBuilder::default()
.chunks(vec![
SlideChunk::from(SlideChunk::new(
vec![],
vec![Box::new(DummyMutator::new(1)), Box::new(DummyMutator::new(2))],
)),
SlideChunk::default(),
],
vec![],
),
Slide::new(
vec![
])
.build(),
SlideBuilder::default()
.chunks(vec![
SlideChunk::from(SlideChunk::new(vec![], vec![Box::new(DummyMutator::new(2))])),
SlideChunk::default(),
],
vec![],
),
])
.build(),
]);
presentation.jump_slide(from);

View File

@ -70,7 +70,7 @@ impl<'a> Presenter<'a> {
/// Run a presentation.
pub fn present(mut self, path: &Path) -> Result<(), PresentationError> {
self.state = PresenterState::Presenting(Presentation::new(vec![]));
self.state = PresenterState::Presenting(Presentation::new(vec![], vec![], Default::default()));
self.try_reload(path);
let graphics_mode = match self.options.mode {
@ -121,6 +121,10 @@ impl<'a> Presenter<'a> {
fn render(&mut self, drawer: &mut TerminalDrawer<Stdout>) -> RenderResult {
let result = match &self.state {
PresenterState::Presenting(presentation) => drawer.render_slide(presentation),
PresenterState::SlideIndex(presentation) => {
drawer.render_slide(presentation)?;
drawer.render_slide_index(presentation)
}
PresenterState::Failure { error, .. } => drawer.render_error(error),
PresenterState::Empty => panic!("cannot render without state"),
};
@ -146,8 +150,11 @@ impl<'a> Presenter<'a> {
};
// Now apply the commands that require a presentation.
let PresenterState::Presenting(presentation) = &mut self.state else {
return CommandSideEffect::None;
let presentation = match &mut self.state {
PresenterState::Presenting(presentation) | PresenterState::SlideIndex(presentation) => presentation,
_ => {
return CommandSideEffect::None;
}
};
let needs_redraw = match command {
Command::Redraw => true,
@ -164,8 +171,14 @@ impl<'a> Presenter<'a> {
return CommandSideEffect::None;
}
}
Command::ToggleSlideIndex => {
self.toggle_slide_index();
true
}
// These are handled above as they don't require the presentation
Command::Reload | Command::HardReload | Command::Exit => panic!("unreachable commands"),
Command::Reload | Command::HardReload | Command::Exit => {
panic!("unreachable commands")
}
};
if needs_redraw { CommandSideEffect::Redraw } else { CommandSideEffect::None }
}
@ -213,6 +226,15 @@ impl<'a> Presenter<'a> {
Ok(presentation)
}
fn toggle_slide_index(&mut self) {
let state = mem::take(&mut self.state);
match state {
PresenterState::Presenting(presentation) => self.state = PresenterState::SlideIndex(presentation),
PresenterState::SlideIndex(presentation) => self.state = PresenterState::Presenting(presentation),
other => self.state = other,
}
}
}
enum CommandSideEffect {
@ -228,6 +250,7 @@ enum PresenterState {
#[default]
Empty,
Presenting(Presentation),
SlideIndex(Presentation),
Failure {
error: String,
presentation: Presentation,
@ -237,24 +260,27 @@ enum PresenterState {
impl PresenterState {
fn presentation(&self) -> &Presentation {
match self {
Self::Presenting(presentation) => presentation,
Self::Failure { presentation, .. } => presentation,
Self::Presenting(presentation) | Self::SlideIndex(presentation) | Self::Failure { presentation, .. } => {
presentation
}
Self::Empty => panic!("state is empty"),
}
}
fn presentation_mut(&mut self) -> &mut Presentation {
match self {
Self::Presenting(presentation) => presentation,
Self::Failure { presentation, .. } => presentation,
Self::Presenting(presentation) | Self::SlideIndex(presentation) | Self::Failure { presentation, .. } => {
presentation
}
Self::Empty => panic!("state is empty"),
}
}
fn into_presentation(self) -> Presentation {
match self {
Self::Presenting(presentation) => presentation,
Self::Failure { presentation, .. } => presentation,
Self::Presenting(presentation) | Self::SlideIndex(presentation) | Self::Failure { presentation, .. } => {
presentation
}
Self::Empty => panic!("state is empty"),
}
}

View File

@ -33,11 +33,10 @@ where
/// Render a slide.
pub(crate) fn render_slide(&mut self, presentation: &Presentation) -> RenderResult {
let window_dimensions = WindowSize::current(self.font_size_fallback)?;
let dimensions = WindowSize::current(self.font_size_fallback)?;
let slide = presentation.current_slide();
let engine = RenderEngine::new(&mut self.terminal, window_dimensions, self.graphics_mode.clone());
let engine = self.create_engine(dimensions);
engine.render(slide.iter_operations())?;
self.terminal.flush()?;
Ok(())
}
@ -46,7 +45,7 @@ where
let dimensions = WindowSize::current(self.font_size_fallback)?;
let heading = vec![
WeightedText::from(StyledText::new("Error loading presentation", TextStyle::default().bold())),
WeightedText::from(StyledText::from(": ")),
WeightedText::from(": "),
];
let alignment = Alignment::Center { minimum_size: 0, minimum_margin: Margin::Percent(8) };
@ -62,15 +61,25 @@ where
RenderOperation::RenderLineBreak,
];
for line in message.lines() {
let error = vec![WeightedText::from(StyledText::from(line))];
let error = vec![WeightedText::from(line)];
let op = RenderOperation::RenderText { line: WeightedLine::from(error), alignment: alignment.clone() };
operations.extend([op, RenderOperation::RenderLineBreak]);
}
let engine = RenderEngine::new(&mut self.terminal, dimensions, self.graphics_mode.clone());
let engine = self.create_engine(dimensions);
engine.render(operations.iter())?;
self.terminal.flush()?;
Ok(())
}
pub(crate) fn render_slide_index(&mut self, presentation: &Presentation) -> RenderResult {
let dimensions = WindowSize::current(self.font_size_fallback)?;
let engine = self.create_engine(dimensions);
engine.render(presentation.iter_index_operations())?;
Ok(())
}
fn create_engine(&mut self, dimensions: WindowSize) -> RenderEngine<W> {
RenderEngine::new(&mut self.terminal, dimensions, self.graphics_mode.clone())
}
}
/// A rendering error.

View File

@ -53,6 +53,7 @@ where
for operation in operations {
self.render_one(operation)?;
}
self.terminal.flush()?;
Ok(())
}
@ -63,8 +64,9 @@ where
RenderOperation::PopMargin => self.pop_margin(),
RenderOperation::SetColors(colors) => self.set_colors(colors),
RenderOperation::JumpToVerticalCenter => self.jump_to_vertical_center(),
RenderOperation::JumpToRow { index } => self.jump_to_row(*index),
RenderOperation::JumpToBottomRow { index } => self.jump_to_bottom(*index),
RenderOperation::RenderText { line: texts, alignment } => self.render_text(texts, alignment),
RenderOperation::RenderText { line, alignment } => self.render_text(line, alignment),
RenderOperation::RenderLineBreak => self.render_line_break(),
RenderOperation::RenderImage(image) => self.render_image(image),
RenderOperation::RenderPreformattedLine(operation) => self.render_preformatted_line(operation),
@ -127,6 +129,11 @@ where
Ok(())
}
fn jump_to_row(&mut self, index: u16) -> RenderResult {
self.terminal.move_to_row(index)?;
Ok(())
}
fn jump_to_bottom(&mut self, index: u16) -> RenderResult {
let target_row = self.current_dimensions().rows.saturating_sub(index).saturating_sub(1);
self.terminal.move_to_row(target_row)?;