Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: render typst/latex/mermaid asynchronously #273

Merged
merged 1 commit into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ default-features = false
features = ["parsing", "default-themes", "regex-onig", "plist-load"]

[dev-dependencies]
rstest = { version = "0.19", default-features = false }
rstest = { version = "0.21", default-features = false }

[features]
default = []
Expand Down
15 changes: 14 additions & 1 deletion src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ impl ContentDiff for RenderOperation {
(RenderDynamic(original), RenderDynamic(updated)) => {
original.diffable_content() != updated.diffable_content()
}
(RenderAsync(original), RenderAsync(updated)) if original.type_id() != updated.type_id() => true,
(RenderAsync(original), RenderAsync(updated)) => original.diffable_content() != updated.diffable_content(),
_ => false,
}
}
Expand Down Expand Up @@ -108,7 +110,7 @@ where
mod test {
use super::*;
use crate::{
presentation::{AsRenderOperations, PreformattedLine, Slide, SlideBuilder},
presentation::{AsRenderOperations, PreformattedLine, RenderAsync, RenderAsyncState, Slide, SlideBuilder},
render::properties::WindowSize,
style::{Color, Colors},
theme::{Alignment, Margin},
Expand All @@ -129,6 +131,16 @@ mod test {
}
}

impl RenderAsync for Dynamic {
fn start_render(&self) -> bool {
false
}

fn poll_state(&self) -> RenderAsyncState {
RenderAsyncState::Rendered
}
}

#[rstest]
#[case(RenderOperation::ClearScreen)]
#[case(RenderOperation::JumpToVerticalCenter)]
Expand All @@ -145,6 +157,7 @@ mod test {
}
))]
#[case(RenderOperation::RenderDynamic(Rc::new(Dynamic)))]
#[case(RenderOperation::RenderAsync(Rc::new(Dynamic)))]
#[case(RenderOperation::InitColumnLayout{ columns: vec![1, 2] })]
#[case(RenderOperation::EnterColumn{ column: 1 })]
#[case(RenderOperation::ExitLayout)]
Expand Down
4 changes: 2 additions & 2 deletions src/input/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ pub(crate) enum Command {
/// Go to one particular slide.
GoToSlide(u32),

/// Render any widgets in the currently visible slide.
RenderWidgets,
/// Render any async render operations in the current slide.
RenderAsyncOperations,

/// Exit the presentation.
Exit,
Expand Down
4 changes: 2 additions & 2 deletions src/input/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl CommandKeyBindings {
MatchContext::Number(number) => Command::GoToSlide(number),
}
}
RenderWidgets => Command::RenderWidgets,
RenderAsyncOperations => Command::RenderAsyncOperations,
Exit => Command::Exit,
Reload => Command::Reload,
HardReload => Command::HardReload,
Expand Down Expand Up @@ -136,7 +136,7 @@ impl TryFrom<KeyBindingsConfig> for CommandKeyBindings {
.chain(zip(CommandDiscriminants::HardReload, config.reload))
.chain(zip(CommandDiscriminants::ToggleSlideIndex, config.toggle_slide_index))
.chain(zip(CommandDiscriminants::ToggleKeyBindingsConfig, config.toggle_bindings))
.chain(zip(CommandDiscriminants::RenderWidgets, config.execute_code))
.chain(zip(CommandDiscriminants::RenderAsyncOperations, config.execute_code))
.chain(zip(CommandDiscriminants::CloseModal, config.close_modal))
.collect();
Self::validate_conflicts(bindings.iter().map(|binding| &binding.0))?;
Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::{
env, io,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};

const DEFAULT_THEME: &str = "dark";
Expand Down Expand Up @@ -208,7 +209,7 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
let resources_path = path.parent().unwrap_or(Path::new("/"));
let mut options = make_builder_options(&config, &mode, force_default_theme);
let graphics_mode = select_graphics_mode(&cli, &config);
let printer = Rc::new(ImagePrinter::new(graphics_mode.clone())?);
let printer = Arc::new(ImagePrinter::new(graphics_mode.clone())?);
let registry = ImageRegistry(printer.clone());
let resources = Resources::new(resources_path, registry.clone());
let third_party_config =
Expand Down
8 changes: 4 additions & 4 deletions src/media/image.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::media::printer::{ImageResource, ResourceProperties};
use std::{fmt::Debug, ops::Deref, path::PathBuf, rc::Rc};
use std::{fmt::Debug, ops::Deref, path::PathBuf, sync::Arc};

/// An image.
///
/// This stores the image in an [std::rc::Rc] so it's cheap to clone.
/// This stores the image in an [std::sync::Arc] so it's cheap to clone.
#[derive(Clone)]
pub(crate) struct Image {
pub(crate) resource: Rc<ImageResource>,
pub(crate) resource: Arc<ImageResource>,
pub(crate) source: ImageSource,
}

Expand All @@ -26,7 +26,7 @@ impl Debug for Image {
impl Image {
/// Constructs a new image.
pub(crate) fn new(resource: ImageResource, source: ImageSource) -> Self {
Self { resource: Rc::new(resource), source }
Self { resource: Arc::new(resource), source }
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/media/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use super::{
};
use crate::ImagePrinter;
use image::DynamicImage;
use std::{path::PathBuf, rc::Rc};
use std::{path::PathBuf, sync::Arc};

#[derive(Clone, Default)]
pub struct ImageRegistry(pub Rc<ImagePrinter>);
pub struct ImageRegistry(pub Arc<ImagePrinter>);

impl ImageRegistry {
pub(crate) fn register_image(&self, image: DynamicImage) -> Result<Image, RegisterImageError> {
Expand Down
19 changes: 9 additions & 10 deletions src/media/sixel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,12 @@ impl ResourceProperties for SixelResource {
}
}

pub struct SixelPrinter {
encoder: Encoder,
}
#[derive(Default)]
pub struct SixelPrinter;

impl SixelPrinter {
pub(crate) fn new() -> Result<Self, CreatePrinterError> {
let encoder =
Encoder::new().map_err(|e| CreatePrinterError::Other(format!("creating sixel encoder: {e:?}")))?;
encoder
.set_encode_policy(EncodePolicy::Fast)
.map_err(|e| CreatePrinterError::Other(format!("setting encoder policy: {e:?}")))?;
Ok(Self { encoder })
Ok(Self)
}
}

Expand All @@ -52,6 +46,11 @@ impl PrintImage for SixelPrinter {
// We're already positioned in the right place but we may not have flushed that yet.
writer.flush()?;

let encoder = Encoder::new().map_err(|e| PrintImageError::other(format!("creating sixel encoder: {e:?}")))?;
encoder
.set_encode_policy(EncodePolicy::Fast)
.map_err(|e| PrintImageError::other(format!("setting encoder policy: {e:?}")))?;

// This check was taken from viuer: it seems to be a bug in xterm
let width = (options.column_width * options.columns).min(1000);
let height = options.row_height * options.rows;
Expand All @@ -64,7 +63,7 @@ impl PrintImage for SixelPrinter {
.format(PixelFormat::RGBA8888)
.pixels(bytes);

self.encoder.encode_bytes(frame).map_err(|e| PrintImageError::other(format!("encoding sixel image: {e:?}")))?;
encoder.encode_bytes(frame).map_err(|e| PrintImageError::other(format!("encoding sixel image: {e:?}")))?;
Ok(())
}
}
83 changes: 61 additions & 22 deletions src/presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ use crate::{
theme::{Alignment, Margin, PresentationTheme},
};
use serde::Deserialize;
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
use std::{
cell::RefCell,
collections::HashSet,
fmt::Debug,
ops::Deref,
rc::Rc,
sync::{Arc, Mutex},
};

#[derive(Debug)]
pub(crate) struct Modals {
Expand All @@ -20,7 +27,7 @@ pub(crate) struct Modals {
pub(crate) struct Presentation {
slides: Vec<Slide>,
modals: Modals,
state: PresentationState,
pub(crate) state: PresentationState,
}

impl Presentation {
Expand Down Expand Up @@ -133,30 +140,47 @@ impl Presentation {
self.current_slide().current_chunk_index()
}

/// Render all widgets in this slide.
pub(crate) fn render_slide_widgets(&mut self) -> bool {
/// Render all async render operations in this slide.
pub(crate) fn trigger_slide_async_renders(&mut self) -> bool {
let slide = self.current_slide_mut();
let mut any_rendered = false;
for operation in slide.iter_operations_mut() {
if let RenderOperation::RenderOnDemand(operation) = operation {
any_rendered = any_rendered || operation.start_render();
if let RenderOperation::RenderAsync(operation) = operation {
let is_rendered = operation.start_render();
any_rendered = any_rendered || is_rendered;
}
}
any_rendered
}

/// Poll every widget in the current slide and check whether they're rendered.
pub(crate) fn widgets_rendered(&mut self) -> bool {
// Get all slides that contain async render operations.
pub(crate) fn slides_with_async_renders(&self) -> HashSet<usize> {
let mut indexes = HashSet::new();
for (index, slide) in self.slides.iter().enumerate() {
for operation in slide.iter_operations() {
if let RenderOperation::RenderAsync(operation) = operation {
if matches!(operation.poll_state(), RenderAsyncState::Rendering) {
indexes.insert(index);
break;
}
}
}
}
indexes
}

/// Poll every async render operation in the current slide and check whether they're completed.
pub(crate) fn async_renders_completed(&mut self) -> bool {
let slide = self.current_slide_mut();
let mut all_rendered = true;
for operation in slide.iter_operations_mut() {
if let RenderOperation::RenderOnDemand(operation) = operation {
all_rendered = all_rendered && matches!(operation.poll_state(), RenderOnDemandState::Rendered);
if let RenderOperation::RenderAsync(operation) = operation {
let is_rendered = matches!(operation.poll_state(), RenderAsyncState::Rendered);
all_rendered = all_rendered && is_rendered;
}
}
all_rendered
}

/// Run a callback through every operation and let it mutate it in place.
///
/// This should be used with care!
Expand Down Expand Up @@ -210,9 +234,18 @@ impl From<Vec<Slide>> for Presentation {
}
}

#[derive(Debug)]
pub(crate) struct AsyncPresentationError {
pub(crate) slide: usize,
pub(crate) error: String,
}

pub(crate) type AsyncPresentationErrorHolder = Arc<Mutex<Option<AsyncPresentationError>>>;

#[derive(Debug, Default)]
pub(crate) struct PresentationStateInner {
current_slide_index: usize,
async_error_holder: AsyncPresentationErrorHolder,
}

#[derive(Clone, Debug, Default)]
Expand All @@ -221,6 +254,10 @@ pub(crate) struct PresentationState {
}

impl PresentationState {
pub(crate) fn async_error_holder(&self) -> AsyncPresentationErrorHolder {
self.inner.deref().borrow().async_error_holder.clone()
}

pub(crate) fn current_slide_index(&self) -> usize {
self.inner.deref().borrow().current_slide_index
}
Expand Down Expand Up @@ -509,8 +546,8 @@ pub(crate) enum RenderOperation {
/// [RenderOperation] with the screen itself.
RenderDynamic(Rc<dyn AsRenderOperations>),

/// An operation that is rendered on demand.
RenderOnDemand(Rc<dyn RenderOnDemand>),
/// An operation that is rendered asynchronously.
RenderAsync(Rc<dyn RenderAsync>),

/// Initialize a column layout.
///
Expand Down Expand Up @@ -569,18 +606,21 @@ pub(crate) trait AsRenderOperations: Debug + 'static {
fn diffable_content(&self) -> Option<&str>;
}

/// A type that can be rendered on demand.
pub(crate) trait RenderOnDemand: AsRenderOperations {
/// Start the on demand render for this operation.
/// An operation that can be rendered asynchronously.
pub(crate) trait RenderAsync: AsRenderOperations {
/// Start the render for this operation.
///
/// Should return true if the invocation triggered the rendering (aka if rendering wasn't
/// already started before).
fn start_render(&self) -> bool;

/// Poll and update the internal on demand state and return the latest.
fn poll_state(&self) -> RenderOnDemandState;
/// Update the internal state and return the updated state.
fn poll_state(&self) -> RenderAsyncState;
}

/// The state of a [RenderOnDemand].
/// The state of a [RenderAsync].
#[derive(Clone, Debug, Default)]
pub(crate) enum RenderOnDemandState {
pub(crate) enum RenderAsyncState {
#[default]
NotStarted,
Rendering,
Expand All @@ -589,10 +629,9 @@ pub(crate) enum RenderOnDemandState {

#[cfg(test)]
mod test {
use std::cell::RefCell;

use super::*;
use rstest::rstest;
use std::cell::RefCell;

#[derive(Clone)]
enum Jump {
Expand Down
Loading
Loading