use crate::{
code::padding::NumberPadder,
commands::keyboard::KeyBinding,
config::KeyBindingsConfig,
markdown::{
elements::{Line, Text},
text::WeightedLine,
text_style::TextStyle,
},
presentation::PresentationState,
render::{
operation::{
AsRenderOperations, ImagePosition, ImageRenderProperties, ImageSize, MarginProperties, RenderOperation,
},
properties::WindowSize,
},
terminal::image::Image,
theme::{Margin, PresentationTheme},
};
use std::{iter, rc::Rc};
use unicode_width::UnicodeWidthStr;
static MODAL_Z_INDEX: i32 = -1;
#[derive(Default)]
pub(crate) struct IndexBuilder {
titles: Vec<Line>,
background: Option<Image>,
}
impl IndexBuilder {
pub(crate) fn add_title(&mut self, title: Line) {
self.titles.push(title);
}
pub(crate) fn set_background(&mut self, background: Image) {
self.background = Some(background);
}
pub(crate) fn build(self, theme: &PresentationTheme, state: PresentationState) -> Vec<RenderOperation> {
let mut builder = ModalBuilder::new("Slides");
let padder = NumberPadder::new(self.titles.len());
for (index, mut title) in self.titles.into_iter().enumerate() {
let index = padder.pad_right(index + 1);
title.0.insert(0, format!("{index}: ").into());
builder.content.push(title);
}
let base_style = theme.modals.style;
let selection_style = theme.modals.selection_style;
let ModalContent { prefix, content, suffix, content_width } = builder.build(base_style);
let drawer = IndexDrawer {
prefix,
rows: content,
suffix,
state,
content_width,
selection_style,
background: self.background,
};
vec![RenderOperation::RenderDynamicTopLevel(Rc::new(drawer))]
}
}
#[derive(Debug)]
struct IndexDrawer {
prefix: Vec<RenderOperation>,
rows: Vec<ContentRow>,
suffix: Vec<RenderOperation>,
content_width: u16,
state: PresentationState,
selection_style: TextStyle,
background: Option<Image>,
}
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 (skip, take) = match self.rows.len() as u16 > max_rows {
true => {
let start = (current_slide_index as u16).saturating_sub(max_rows / 2);
let start = start.min(self.rows.len() as u16 - max_rows);
(start as usize, max_rows as usize)
}
false => (0, self.rows.len()),
};
let visible_rows = self.rows.iter().enumerate().skip(skip).take(take);
let mut operations = vec![CenterModalContent::new(self.content_width, take, self.background.clone()).into()];
operations.extend(self.prefix.iter().cloned());
for (index, row) in visible_rows {
let mut row = row.clone();
if index == current_slide_index {
row = row.with_style(self.selection_style);
}
let operation = RenderOperation::RenderText { line: row.build(), alignment: Default::default() };
operations.extend([operation, RenderOperation::RenderLineBreak]);
}
operations.extend(self.suffix.iter().cloned());
operations
}
}
#[derive(Default)]
pub(crate) struct KeyBindingsModalBuilder {
background: Option<Image>,
}
impl KeyBindingsModalBuilder {
pub(crate) fn set_background(&mut self, background: Image) {
self.background = Some(background);
}
pub(crate) fn build(self, theme: &PresentationTheme, config: &KeyBindingsConfig) -> Vec<RenderOperation> {
let mut builder = ModalBuilder::new("Key bindings");
builder.content.extend([
Self::build_line("Next", &config.next),
Self::build_line("Next (fast)", &config.next_fast),
Self::build_line("Previous", &config.previous),
Self::build_line("Previous (fast)", &config.previous_fast),
Self::build_line("First slide", &config.first_slide),
Self::build_line("Last slide", &config.last_slide),
Self::build_line("Go to slide", &config.go_to_slide),
Self::build_line("Execute code", &config.execute_code),
Self::build_line("Reload", &config.reload),
Self::build_line("Toggle slide index", &config.toggle_slide_index),
Self::build_line("Close modal", &config.close_modal),
Self::build_line("Exit", &config.exit),
]);
let lines = builder.content.len();
let style = theme.modals.style;
let content = builder.build(style);
let content_width = content.content_width;
let mut operations = content.into_operations();
operations.insert(0, CenterModalContent::new(content_width, lines, self.background).into());
operations
}
fn build_line(label: &str, bindings: &[KeyBinding]) -> Line {
let mut text = vec![Text::new(label, TextStyle::default().bold()), ": ".into()];
for (index, binding) in bindings.iter().enumerate() {
if index > 0 {
text.push(", ".into());
}
text.push(Text::new(binding.to_string(), TextStyle::default().italics()));
}
Line(text)
}
}
struct ModalBuilder {
heading: String,
content: Vec<Line>,
}
impl ModalBuilder {
fn new<S: Into<String>>(heading: S) -> Self {
Self { heading: heading.into(), content: Vec::new() }
}
fn build(self, style: TextStyle) -> ModalContent {
let longest_line = self.content.iter().map(Line::width).max().unwrap_or(0) as u16;
let longest_line = longest_line.max(self.heading.len() as u16);
let longest_line = longest_line.max(12);
let content_width = longest_line + 6;
let mut prefix = vec![RenderOperation::SetColors(style.colors)];
let heading = Self::center_line(self.heading, longest_line as usize);
prefix.extend(Border::Top.render_line(content_width));
prefix.extend([
RenderOperation::RenderText {
line: Self::build_line(vec![Text::from(heading)], content_width).build(),
alignment: Default::default(),
},
RenderOperation::RenderLineBreak,
]);
prefix.extend(Border::Separator.render_line(content_width));
let mut content = Vec::new();
for title in self.content {
content.push(Self::build_line(title.0, content_width));
}
let suffix = Border::Bottom.render_line(content_width).into_iter().collect();
ModalContent { prefix, content, suffix, content_width }
}
fn center_line(text: String, longest_line: usize) -> String {
let missing = longest_line.saturating_sub(text.len());
let padding = missing / 2;
let mut output = " ".repeat(padding);
output.push_str(&text);
output.extend(iter::repeat_n(' ', padding));
output
}
fn build_line(text_chunks: Vec<Text>, content_width: u16) -> ContentRow {
let (opening, closing) = Border::Regular.edges();
let prefix = Text::from(format!("{opening} "));
let content = text_chunks;
let total_width = content.iter().map(|c| c.content.width()).sum::<usize>() + prefix.content.width();
let missing = content_width as usize - 1 - total_width;
let mut suffix = " ".repeat(missing);
suffix.push(closing);
ContentRow { prefix, content, suffix: suffix.into() }
}
}
struct ModalContent {
prefix: Vec<RenderOperation>,
content: Vec<ContentRow>,
suffix: Vec<RenderOperation>,
content_width: u16,
}
impl ModalContent {
fn into_operations(self) -> Vec<RenderOperation> {
let mut operations = self.prefix;
operations.extend(self.content.into_iter().flat_map(|c| {
[
RenderOperation::RenderText { line: c.build(), alignment: Default::default() },
RenderOperation::RenderLineBreak,
]
}));
operations.extend(self.suffix);
operations
}
}
#[derive(Clone, Debug)]
struct ContentRow {
prefix: Text,
content: Vec<Text>,
suffix: Text,
}
impl ContentRow {
fn with_style(mut self, style: TextStyle) -> ContentRow {
for chunk in &mut self.content {
chunk.style.merge(&style);
}
self
}
fn build(self) -> WeightedLine {
let mut chunks = self.content;
chunks.insert(0, self.prefix);
chunks.push(self.suffix);
WeightedLine::from(chunks)
}
}
enum Border {
Regular,
Top,
Separator,
Bottom,
}
impl Border {
fn render_line(&self, content_length: u16) -> [RenderOperation; 2] {
let (opening, closing) = self.edges();
let mut line = String::from(opening);
line.push_str(&"─".repeat(content_length.saturating_sub(2) as usize));
line.push(closing);
let horizontal_border = WeightedLine::from(vec![Text::from(line)]);
[
RenderOperation::RenderText { line: horizontal_border.clone(), alignment: Default::default() },
RenderOperation::RenderLineBreak,
]
}
fn edges(&self) -> (char, char) {
match self {
Self::Regular => ('│', '│'),
Self::Top => ('┌', '┐'),
Self::Separator => ('├', '┤'),
Self::Bottom => ('└', '┘'),
}
}
}
#[derive(Debug)]
struct CenterModalContent {
content_width: u16,
content_height: usize,
background: Option<Image>,
}
impl CenterModalContent {
fn new(content_width: u16, content_height: usize, background: Option<Image>) -> Self {
Self { content_width, content_height, background }
}
}
impl AsRenderOperations for CenterModalContent {
fn as_render_operations(&self, dimensions: &WindowSize) -> Vec<RenderOperation> {
let margin = dimensions.columns.saturating_sub(self.content_width) / 2;
let properties = MarginProperties { horizontal: Margin::Fixed(margin), top: 0, bottom: 0 };
let content_height = (self.content_height + 4) as u16;
let target_row = dimensions.rows.saturating_sub(content_height) / 2;
let mut operations =
vec![RenderOperation::ApplyMargin(properties), RenderOperation::JumpToRow { index: target_row }];
if let Some(image) = &self.background {
let properties = ImageRenderProperties {
z_index: MODAL_Z_INDEX,
size: ImageSize::Specific(self.content_width, content_height),
restore_cursor: true,
background_color: None,
position: ImagePosition::Center,
};
operations.push(RenderOperation::RenderImage(image.clone(), properties));
}
operations
}
}
impl From<CenterModalContent> for RenderOperation {
fn from(op: CenterModalContent) -> Self {
Self::RenderDynamicTopLevel(Rc::new(op))
}
}