use super::bigtext::BigText;
use super::markdown::Markdown;
use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::utils::figlet::FigletFont;
use crate::utils::text_sizing::is_supported as text_sizing_supported;
use crate::widget::slides::{SlideContent, SlideNav};
use crate::widget::theme::{DARK_GRAY, DISABLED_FG, SEPARATOR_COLOR};
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ViewMode {
#[default]
Preview,
Slides,
}
#[derive(Debug, Clone)]
pub struct MarkdownPresentation {
source: String,
nav: SlideNav,
mode: ViewMode,
scroll_offset: usize,
use_text_sizing: bool,
figlet_font: FigletFont,
bg: Color,
accent: Color,
show_numbers: bool,
show_progress: bool,
heading_fg: Color,
link_fg: Color,
code_fg: Color,
props: WidgetProps,
}
impl MarkdownPresentation {
pub fn new(source: impl Into<String>) -> Self {
let source = source.into();
let nav = SlideNav::new(&source);
Self {
source,
nav,
mode: ViewMode::Preview,
scroll_offset: 0,
use_text_sizing: text_sizing_supported(),
figlet_font: FigletFont::Block,
bg: Color::rgb(20, 20, 30),
accent: Color::CYAN,
show_numbers: true,
show_progress: true,
heading_fg: Color::WHITE,
link_fg: Color::CYAN,
code_fg: Color::YELLOW,
props: WidgetProps::new(),
}
}
pub fn from_slides(slides: Vec<SlideContent>) -> Self {
let source = slides
.iter()
.map(|s| s.markdown().to_string())
.collect::<Vec<_>>()
.join("\n---\n");
let nav = SlideNav::from_slides(slides);
Self {
source,
nav,
mode: ViewMode::Preview,
scroll_offset: 0,
use_text_sizing: text_sizing_supported(),
figlet_font: FigletFont::Block,
bg: Color::rgb(20, 20, 30),
accent: Color::CYAN,
show_numbers: true,
show_progress: true,
heading_fg: Color::WHITE,
link_fg: Color::CYAN,
code_fg: Color::YELLOW,
props: WidgetProps::new(),
}
}
pub fn text_sizing(mut self, enable: bool) -> Self {
self.use_text_sizing = enable && text_sizing_supported();
self
}
pub fn figlet_font(mut self, font: FigletFont) -> Self {
self.figlet_font = font;
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = color;
self
}
pub fn accent(mut self, color: Color) -> Self {
self.accent = color;
self
}
pub fn heading_fg(mut self, color: Color) -> Self {
self.heading_fg = color;
self
}
pub fn link_fg(mut self, color: Color) -> Self {
self.link_fg = color;
self
}
pub fn code_fg(mut self, color: Color) -> Self {
self.code_fg = color;
self
}
pub fn numbers(mut self, show: bool) -> Self {
self.show_numbers = show;
self
}
pub fn progress(mut self, show: bool) -> Self {
self.show_progress = show;
self
}
pub fn mode(mut self, mode: ViewMode) -> Self {
self.mode = mode;
self
}
pub fn current_mode(&self) -> ViewMode {
self.mode
}
pub fn toggle_mode(&mut self) {
self.mode = match self.mode {
ViewMode::Preview => ViewMode::Slides,
ViewMode::Slides => ViewMode::Preview,
};
}
pub fn next_slide(&mut self) -> bool {
self.nav.advance()
}
pub fn prev_slide(&mut self) -> bool {
self.nav.prev()
}
pub fn goto(&mut self, index: usize) {
self.nav.goto(index);
}
pub fn first(&mut self) {
self.nav.first();
}
pub fn last(&mut self) {
self.nav.last();
}
pub fn current_index(&self) -> usize {
self.nav.current_index()
}
pub fn slide_count(&self) -> usize {
self.nav.slide_count()
}
pub fn current_slide(&self) -> Option<&SlideContent> {
self.nav.current_slide()
}
pub fn current_notes(&self) -> Option<&str> {
self.nav.current_slide().and_then(|s| s.notes())
}
pub fn indicator(&self) -> String {
self.nav.indicator()
}
pub fn indicator_bracketed(&self) -> String {
self.nav.indicator_bracketed()
}
pub fn progress_value(&self) -> f32 {
self.nav.progress()
}
pub fn is_first(&self) -> bool {
self.nav.is_first()
}
pub fn is_last(&self) -> bool {
self.nav.is_last()
}
pub fn slides(&self) -> &[SlideContent] {
self.nav.slides()
}
pub fn source(&self) -> &str {
&self.source
}
pub fn scroll_up(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_sub(lines);
}
pub fn scroll_down(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_add(lines);
}
pub fn scroll_to_top(&mut self) {
self.scroll_offset = 0;
}
pub fn reload(&mut self, source: impl Into<String>) {
self.source = source.into();
self.nav = SlideNav::new(&self.source);
self.scroll_offset = 0;
}
fn render_preview(&self, ctx: &mut RenderContext) {
self.fill_background(ctx);
let md = Markdown::new(&self.source)
.link_fg(self.link_fg)
.code_fg(self.code_fg)
.heading_fg(self.heading_fg);
md.render(ctx);
self.render_mode_indicator(ctx, "PREVIEW");
}
fn render_slide(&self, ctx: &mut RenderContext) {
let area = ctx.area;
self.fill_background(ctx);
if let Some(slide) = self.nav.current_slide() {
let mut content_start_y = 0u16;
if let Some(title) = slide.title() {
let bt = BigText::new(title, 1)
.fg(self.heading_fg)
.figlet_font(self.figlet_font)
.force_figlet(!self.use_text_sizing);
let title_height = bt.height();
let title_area = ctx.sub_area(
0,
1,
area.width,
title_height.min(area.height.saturating_sub(1)),
);
let mut title_ctx = RenderContext::new(ctx.buffer, title_area);
bt.render(&mut title_ctx);
content_start_y = title_height + 2;
if content_start_y < area.height {
let sep_len = (area.width as usize).min(title.len() * 2).max(20);
let sep_start = (area.width as usize - sep_len) / 2;
for i in 0..sep_len {
let mut cell = Cell::new('─');
cell.fg = Some(self.accent);
ctx.set(sep_start as u16 + i as u16, content_start_y, cell);
}
content_start_y += 2;
}
}
let content = self.strip_title(slide.markdown());
if !content.trim().is_empty() {
let content_area = ctx.sub_area(
2,
content_start_y,
area.width.saturating_sub(4),
area.height.saturating_sub(content_start_y + 2),
);
let md = Markdown::new(&content)
.link_fg(self.link_fg)
.code_fg(self.code_fg)
.heading_fg(self.heading_fg);
let mut content_ctx = RenderContext::new(ctx.buffer, content_area);
md.render(&mut content_ctx);
}
}
self.render_footer(ctx);
}
fn strip_title(&self, markdown: &str) -> String {
let mut lines = markdown.lines().peekable();
let mut result = String::new();
let mut skipped_title = false;
while let Some(line) = lines.next() {
if !skipped_title
&& (line.trim_start().starts_with("# ") || line.trim_start().starts_with("## "))
{
skipped_title = true;
while lines.peek().is_some_and(|l| l.trim().is_empty()) {
lines.next();
}
continue;
}
result.push_str(line);
result.push('\n');
}
result
}
fn fill_background(&self, ctx: &mut RenderContext) {
let area = ctx.area;
for y in 0..area.height {
for x in 0..area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(self.bg);
ctx.set(x, y, cell);
}
}
}
fn render_mode_indicator(&self, ctx: &mut RenderContext, mode_text: &str) {
let area = ctx.area;
let text = format!(" {} ", mode_text);
let start_x = area.width - text.len() as u16 - 1;
let y: u16 = 1;
for (i, ch) in text.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(Color::BLACK);
cell.bg = Some(self.accent);
cell.modifier = Modifier::BOLD;
ctx.set(start_x + i as u16, y, cell);
}
}
fn render_footer(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let footer_y = area.height - 1;
if self.show_numbers && self.nav.slide_count() > 0 {
let num_str = self.nav.indicator();
let start_x = area.width - num_str.len() as u16 - 1;
for (i, ch) in num_str.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(DISABLED_FG);
ctx.set(start_x + i as u16, footer_y, cell);
}
}
if self.show_progress && self.nav.slide_count() > 0 {
let bar_width = (area.width / 3).max(10);
let progress = self.nav.progress();
let filled = (bar_width as f32 * progress) as u16;
for i in 0..bar_width {
let ch = if i < filled { '━' } else { '─' };
let mut cell = Cell::new(ch);
cell.fg = Some(if i < filled {
self.accent
} else {
SEPARATOR_COLOR
});
ctx.set(1 + i, footer_y, cell);
}
}
let mode_str = match self.mode {
ViewMode::Preview => "[P]",
ViewMode::Slides => "[S]",
};
let mode_x = area.width / 2 - 1;
for (i, ch) in mode_str.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(DARK_GRAY);
ctx.set(mode_x + i as u16, footer_y, cell);
}
}
}
impl Default for MarkdownPresentation {
fn default() -> Self {
Self::new("")
}
}
impl View for MarkdownPresentation {
crate::impl_view_meta!("MarkdownPresentation");
fn render(&self, ctx: &mut RenderContext) {
if ctx.area.width == 0 || ctx.area.height == 0 {
return;
}
match self.mode {
ViewMode::Preview => self.render_preview(ctx),
ViewMode::Slides => self.render_slide(ctx),
}
}
}
impl_styled_view!(MarkdownPresentation);
impl_props_builders!(MarkdownPresentation);
pub fn markdown_presentation(source: impl Into<String>) -> MarkdownPresentation {
MarkdownPresentation::new(source)
}