forked from mirror/presenterm
Add slide index modal
This commit is contained in:
parent
ceeac45b75
commit
6ff3098552
@ -134,7 +134,6 @@ impl Person {
|
||||
println!("hello, I'm {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<!-- end_slide -->
|
||||
|
||||
160
src/builder.rs
160
src/builder.rs
@ -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 { .. }
|
||||
|
||||
40
src/diff.rs
40
src/diff.rs
@ -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!(
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = "Hello world";
|
||||
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!["Hello", "world"];
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)?;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user