use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::widget::theme::{DISABLED_FG, LIGHT_GRAY, 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 Transition {
#[default]
None,
Fade,
SlideLeft,
SlideRight,
SlideUp,
ZoomIn,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SlideAlign {
Left,
#[default]
Center,
Right,
}
#[derive(Clone, Debug)]
pub struct Slide {
pub title: String,
pub content: Vec<String>,
pub notes: String,
pub bg: Option<Color>,
pub title_color: Color,
pub content_color: Color,
pub align: SlideAlign,
}
impl Slide {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
content: Vec::new(),
notes: String::new(),
bg: None,
title_color: Color::CYAN,
content_color: Color::WHITE,
align: SlideAlign::Center,
}
}
pub fn line(mut self, text: impl Into<String>) -> Self {
self.content.push(text.into());
self
}
pub fn lines(mut self, lines: &[&str]) -> Self {
for line in lines {
self.content.push((*line).to_string());
}
self
}
pub fn bullet(mut self, text: impl Into<String>) -> Self {
self.content.push(format!(" • {}", text.into()));
self
}
pub fn numbered(mut self, num: usize, text: impl Into<String>) -> Self {
self.content.push(format!(" {}. {}", num, text.into()));
self
}
pub fn code(mut self, code: impl Into<String>) -> Self {
self.content.push(String::new());
for line in code.into().lines() {
self.content.push(format!(" {}", line));
}
self.content.push(String::new());
self
}
pub fn notes(mut self, notes: impl Into<String>) -> Self {
self.notes = notes.into();
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn title_color(mut self, color: Color) -> Self {
self.title_color = color;
self
}
pub fn content_color(mut self, color: Color) -> Self {
self.content_color = color;
self
}
pub fn align(mut self, align: SlideAlign) -> Self {
self.align = align;
self
}
}
pub struct Presentation {
title: String,
author: String,
slides: Vec<Slide>,
current: usize,
transition: Transition,
transition_progress: f32,
show_numbers: bool,
show_progress: bool,
timer: Option<u64>,
bg: Color,
accent: Color,
props: WidgetProps,
}
impl Presentation {
pub fn new() -> Self {
Self {
title: String::new(),
author: String::new(),
slides: Vec::new(),
current: 0,
transition: Transition::None,
transition_progress: 1.0,
show_numbers: true,
show_progress: true,
timer: None,
bg: Color::rgb(20, 20, 30),
accent: Color::CYAN,
props: WidgetProps::new(),
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = author.into();
self
}
pub fn slide(mut self, slide: Slide) -> Self {
self.slides.push(slide);
self
}
pub fn slides(mut self, slides: Vec<Slide>) -> Self {
self.slides.extend(slides);
self
}
pub fn transition(mut self, transition: Transition) -> Self {
self.transition = transition;
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 bg(mut self, color: Color) -> Self {
self.bg = color;
self
}
pub fn accent(mut self, color: Color) -> Self {
self.accent = color;
self
}
pub fn timer(mut self, seconds: u64) -> Self {
self.timer = Some(seconds);
self
}
pub fn next_slide(&mut self) -> bool {
if self.current < self.slides.len().saturating_sub(1) {
self.current += 1;
self.transition_progress = 0.0;
true
} else {
false
}
}
pub fn prev(&mut self) -> bool {
if self.current > 0 {
self.current -= 1;
self.transition_progress = 0.0;
true
} else {
false
}
}
pub fn goto(&mut self, index: usize) {
if index < self.slides.len() {
self.current = index;
self.transition_progress = 0.0;
}
}
pub fn first(&mut self) {
self.goto(0);
}
pub fn last(&mut self) {
self.goto(self.slides.len().saturating_sub(1));
}
pub fn current_index(&self) -> usize {
self.current
}
pub fn slide_count(&self) -> usize {
self.slides.len()
}
pub fn current_slide(&self) -> Option<&Slide> {
self.slides.get(self.current)
}
pub fn current_notes(&self) -> Option<&str> {
self.current_slide().map(|s| s.notes.as_str())
}
pub fn tick(&mut self, dt: f32) {
if self.transition_progress < 1.0 {
self.transition_progress = (self.transition_progress + dt * 3.0).min(1.0);
}
}
fn render_title_slide(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let center_y = area.height / 2;
let title_y = center_y.saturating_sub(2);
self.render_centered_text(ctx, &self.title, title_y, self.accent, Modifier::BOLD);
if !self.author.is_empty() {
let author_y = center_y + 1;
self.render_centered_text(ctx, &self.author, author_y, LIGHT_GRAY, Modifier::ITALIC);
}
let hint = "Press → or Space to start";
let hint_y = area.height - 2;
self.render_centered_text(ctx, hint, hint_y, DISABLED_FG, Modifier::empty());
}
fn render_content_slide(&self, ctx: &mut RenderContext, slide: &Slide) {
let area = ctx.area;
let bg = slide.bg.unwrap_or(self.bg);
for y in 0..area.height {
for x in 0..area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(bg);
ctx.set(x, y, cell);
}
}
let title_y = 2;
self.render_centered_text(
ctx,
&slide.title,
title_y,
slide.title_color,
Modifier::BOLD,
);
let sep_y = 4;
let sep_len = slide.title.chars().count().min(area.width as usize - 4);
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, sep_y, cell);
}
let content_start_y = 6;
for (i, line) in slide.content.iter().enumerate() {
let y = content_start_y + i as u16;
if y >= area.height - 3 {
break;
}
match slide.align {
SlideAlign::Left => {
for (j, ch) in line.chars().enumerate() {
if j as u16 + 2 >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(slide.content_color);
ctx.set(2 + j as u16, y, cell);
}
}
SlideAlign::Center => {
self.render_centered_text(ctx, line, y, slide.content_color, Modifier::empty());
}
SlideAlign::Right => {
let line_len = line.chars().count();
let start_x = area.width.saturating_sub(line_len as u16 + 2);
for (j, ch) in line.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(slide.content_color);
ctx.set(start_x + j as u16, y, cell);
}
}
}
}
}
fn render_centered_text(
&self,
ctx: &mut RenderContext,
text: &str,
y: u16,
fg: Color,
modifier: Modifier,
) {
let area = ctx.area;
let text_len = text.chars().count();
let start_x = (area.width as usize).saturating_sub(text_len) / 2;
for (i, ch) in text.chars().enumerate() {
let x = start_x as u16 + i as u16;
if x >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
cell.modifier = modifier;
ctx.set(x, y, cell);
}
}
fn render_footer(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let footer_y = area.height - 1;
if self.show_numbers && !self.slides.is_empty() {
let num_str = format!("{}/{}", self.current + 1, self.slides.len());
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.slides.is_empty() {
let bar_width = (area.width / 3).max(10);
let progress = (self.current + 1) as f32 / self.slides.len() as f32;
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);
}
}
}
}
impl Default for Presentation {
fn default() -> Self {
Self::new()
}
}
impl View for Presentation {
crate::impl_view_meta!("Presentation");
fn render(&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);
}
}
if self.slides.is_empty() || self.current == 0 && !self.title.is_empty() {
self.render_title_slide(ctx);
} else if let Some(slide) = self.slides.get(self.current) {
self.render_content_slide(ctx, slide);
}
self.render_footer(ctx);
}
}
impl_styled_view!(Presentation);
impl_props_builders!(Presentation);
pub fn presentation() -> Presentation {
Presentation::new()
}
pub fn slide(title: impl Into<String>) -> Slide {
Slide::new(title)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
use crate::render::Buffer;
#[test]
fn test_render_title_slide() {
let pres = Presentation::new()
.title("Test Title")
.author("Test Author");
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
pres.render(&mut ctx);
}
#[test]
fn test_render_content_slide() {
let slide = Slide::new("Content").line("Line 1").line("Line 2");
let mut pres = Presentation::new().slide(slide);
pres.goto(1);
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
pres.render(&mut ctx);
}
#[test]
fn test_render_centered_text() {
let pres = Presentation::new().slide(Slide::new("Test"));
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
pres.render(&mut ctx);
}
#[test]
fn test_render_footer() {
let pres = Presentation::new()
.slide(Slide::new("Slide 1"))
.slide(Slide::new("Slide 2"));
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
pres.render(&mut ctx);
}
}