1
0
mirror of https://github.com/mfontanini/presenterm.git synced 2026-09-01 23:33:21 -06:00

feat: add theme support for column layout margin

This commit is contained in:
Matias Fontanini 2026-03-04 18:15:27 -08:00
parent b2fbe59cdf
commit 0a15d502dc
19 changed files with 179 additions and 51 deletions

View File

@ -57,7 +57,11 @@ impl PresentationBuilder<'_, '_> {
} else {
LayoutGrid::None
};
self.chunk_operations.push(RenderOperation::InitColumnLayout { columns, grid });
self.chunk_operations.push(RenderOperation::InitColumnLayout {
columns,
grid,
margin: self.theme.column_layout.margin,
});
self.slide_state.needs_enter_column = true;
}
CommentCommand::ResetLayout => {
@ -297,12 +301,11 @@ impl fmt::Display for CommandParseError {
#[cfg(test)]
mod tests {
use std::{fs, io::BufWriter};
use super::*;
use crate::presentation::builder::{PresentationBuilderOptions, utils::Test};
use image::{DynamicImage, ImageEncoder, codecs::png::PngEncoder};
use rstest::rstest;
use std::{fs, io::BufWriter};
use tempfile::tempdir;
#[rstest]
@ -416,7 +419,14 @@ mod tests {
#[test]
fn columns() {
let input = "
let input = "---
theme:
override:
column_layout:
margin:
fixed: 2
---
<!-- column_layout: [1, 1] -->
<!-- column: 0 -->
foo1
@ -433,15 +443,15 @@ bar2
---
";
let lines = Test::new(input).render().rows(7).columns(24).into_lines();
let lines = Test::new(input).render().rows(7).columns(20).into_lines();
let expected = &[
" ",
"foo1 bar1 ",
" ",
"foo2 bar2 ",
" ",
"———————— ————————",
" ",
" ",
"foo1 bar1 ",
" ",
"foo2 bar2 ",
" ",
"———————— ————————",
" ",
];
assert_eq!(lines, expected);
}
@ -449,7 +459,14 @@ bar2
#[test]
fn columns_back_and_forth() {
// this is the same as the above but we run back and forth between the columns
let input = "
let input = "---
theme:
override:
column_layout:
margin:
fixed: 2
---
<!-- column_layout: [1, 1] -->
<!-- column: 0 -->
@ -472,22 +489,29 @@ bar2
---
";
let lines = Test::new(input).render().rows(7).columns(24).into_lines();
let lines = Test::new(input).render().rows(7).columns(20).into_lines();
let expected = &[
" ",
"foo1 bar1 ",
" ",
"foo2 bar2 ",
" ",
"———————— ————————",
" ",
" ",
"foo1 bar1 ",
" ",
"foo2 bar2 ",
" ",
"———————— ————————",
" ",
];
assert_eq!(lines, expected);
}
#[test]
fn uneven_columns() {
let input = "
let input = "---
theme:
override:
column_layout:
margin:
fixed: 4
---
<!-- column_layout: [2, 1] -->
<!-- column: 0 -->
foo1
@ -519,7 +543,14 @@ bar2
#[test]
fn uneven_three_columns() {
let input = "
let input = "---
theme:
override:
column_layout:
margin:
fixed: 4
---
<!-- column_layout: [1, 2, 1] -->
<!-- column: 0 -->
@ -574,7 +605,14 @@ bye
#[test]
fn pause_layout_new_slide() {
let input = r"
let input = r"---
theme:
override:
column_layout:
margin:
fixed: 4
---
<!-- column_layout: [1, 1] -->
<!-- column: 0 -->
hi

View File

@ -166,7 +166,7 @@ mod test {
))]
#[case(RenderOperation::RenderDynamic(Rc::new(Dynamic)))]
#[case(RenderOperation::RenderAsync(Rc::new(Dynamic)))]
#[case(RenderOperation::InitColumnLayout{ columns: vec![1, 2], grid: LayoutGrid::None })]
#[case(RenderOperation::InitColumnLayout{ columns: vec![1, 2], grid: LayoutGrid::None, margin: Default::default() })]
#[case(RenderOperation::EnterColumn{ column: 1 })]
#[case(RenderOperation::ExitLayout)]
fn same_not_modified(#[case] operation: RenderOperation) {
@ -203,8 +203,16 @@ mod test {
#[test]
fn different_column_layout() {
let lhs = RenderOperation::InitColumnLayout { columns: vec![1, 2], grid: LayoutGrid::None };
let rhs = RenderOperation::InitColumnLayout { columns: vec![1, 3], grid: LayoutGrid::None };
let lhs = RenderOperation::InitColumnLayout {
columns: vec![1, 2],
grid: LayoutGrid::None,
margin: Default::default(),
};
let rhs = RenderOperation::InitColumnLayout {
columns: vec![1, 3],
grid: LayoutGrid::None,
margin: Default::default(),
};
assert!(lhs.is_content_different(&rhs));
}

View File

@ -19,7 +19,7 @@ use crate::{
},
printer::{TerminalCommand, TerminalIo},
},
theme::Alignment,
theme::{Alignment, Margin},
};
use std::mem;
@ -44,17 +44,10 @@ impl Default for MaxSize {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Default, Debug)]
pub(crate) struct RenderEngineOptions {
pub(crate) validate_overflows: bool,
pub(crate) max_size: MaxSize,
pub(crate) column_layout_margin: u16,
}
impl Default for RenderEngineOptions {
fn default() -> Self {
Self { validate_overflows: false, max_size: Default::default(), column_layout_margin: 4 }
}
}
pub(crate) struct RenderEngine<'a, T>
@ -151,7 +144,9 @@ where
RenderOperation::RenderDynamic(generator) => self.render_dynamic(generator.as_ref()),
RenderOperation::RenderDynamicTopLevel(generator) => self.render_dynamic_top_level(generator.as_ref()),
RenderOperation::RenderAsync(generator) => self.render_async(generator.as_ref()),
RenderOperation::InitColumnLayout { columns, grid } => self.init_column_layout(columns, *grid),
RenderOperation::InitColumnLayout { columns, grid, margin } => {
self.init_column_layout(columns, *grid, *margin)
}
RenderOperation::EnterColumn { column } => self.enter_column(*column),
RenderOperation::ExitLayout => self.exit_layout(),
}?;
@ -375,7 +370,7 @@ where
Ok(())
}
fn init_column_layout(&mut self, widths: &[u8], grid: LayoutGrid) -> RenderResult {
fn init_column_layout(&mut self, widths: &[u8], grid: LayoutGrid, margin: Margin) -> RenderResult {
if !matches!(self.layout, LayoutState::Default) {
self.exit_layout()?;
}
@ -392,23 +387,23 @@ where
columns.push(Column { start_column: current_column, end_column, current_row });
current_column = end_column;
}
self.layout = LayoutState::InitializedColumn { columns, grid, start_row: current_row };
self.layout = LayoutState::InitializedColumn { columns, grid, margin, start_row: current_row };
Ok(())
}
fn enter_column(&mut self, column_index: usize) -> RenderResult {
let (columns, grid, start_row) = match mem::take(&mut self.layout) {
let (columns, margin, grid, start_row) = match mem::take(&mut self.layout) {
LayoutState::Default => return Err(RenderError::InvalidLayoutEnter),
LayoutState::InitializedColumn { columns, .. } | LayoutState::EnteredColumn { columns, .. }
if column_index >= columns.len() =>
{
return Err(RenderError::InvalidLayoutEnter);
}
LayoutState::InitializedColumn { columns, grid, start_row } => (columns, grid, start_row),
LayoutState::EnteredColumn { columns, grid, start_row, .. } => {
LayoutState::InitializedColumn { columns, margin, grid, start_row } => (columns, margin, grid, start_row),
LayoutState::EnteredColumn { columns, margin, grid, start_row, .. } => {
// Pop this one and start clean
self.pop_margin()?;
(columns, grid, start_row)
(columns, margin, grid, start_row)
}
};
let column = &columns[column_index];
@ -421,18 +416,19 @@ where
let mut dimensions =
WindowRect { dimensions: new_size, start_column: column.start_column, start_row: column.current_row };
let total_margin = margin.as_characters(self.window_rects[0].dimensions.columns);
if column_index == 0 {
dimensions = dimensions.shrink_right(self.options.column_layout_margin);
dimensions = dimensions.shrink_right(total_margin);
} else if column_index == columns.len() - 1 {
dimensions = dimensions.shrink_left(self.options.column_layout_margin);
dimensions = dimensions.shrink_left(total_margin);
} else {
let margin = self.options.column_layout_margin / 2;
let margin = total_margin / 2;
dimensions = dimensions.shrink_left(margin).shrink_right(margin);
}
self.window_rects.push(dimensions);
self.terminal.execute(&TerminalCommand::MoveToRow(column.current_row))?;
self.layout = LayoutState::EnteredColumn { column: column_index, columns, grid, start_row };
self.layout = LayoutState::EnteredColumn { column: column_index, columns, margin, grid, start_row };
Ok(())
}
@ -440,7 +436,7 @@ where
fn exit_layout(&mut self) -> RenderResult {
match &self.layout {
LayoutState::Default => return Ok(()),
LayoutState::InitializedColumn { columns, grid, start_row }
LayoutState::InitializedColumn { columns, grid, start_row, .. }
| LayoutState::EnteredColumn { columns, grid, start_row, .. } => {
if let LayoutGrid::Draw(style) = grid {
let style = *style;
@ -479,12 +475,14 @@ enum LayoutState {
InitializedColumn {
start_row: u16,
columns: Vec<Column>,
margin: Margin,
grid: LayoutGrid,
},
EnteredColumn {
start_row: u16,
column: usize,
columns: Vec<Column>,
margin: Margin,
grid: LayoutGrid,
},
}
@ -699,7 +697,7 @@ mod tests {
fn do_render(max_size: MaxSize, operations: &[RenderOperation]) -> Vec<Instruction> {
let mut buf = TerminalBuf::default();
let dimensions = WindowSize { rows: 100, columns: 100, height: 200, width: 200 };
let options = RenderEngineOptions { validate_overflows: false, max_size, column_layout_margin: 0 };
let options = RenderEngineOptions { validate_overflows: false, max_size };
let mut engine = RenderEngine::new(&mut buf, dimensions, options);
engine.image_scaler = Box::new(DummyImageScaler);
engine.render(operations.iter()).expect("render failed");
@ -723,7 +721,11 @@ mod tests {
#[test]
fn columns() {
let ops = render(&[
RenderOperation::InitColumnLayout { columns: vec![1, 1], grid: LayoutGrid::None },
RenderOperation::InitColumnLayout {
columns: vec![1, 1],
grid: LayoutGrid::None,
margin: Default::default(),
},
// print on column 0
RenderOperation::EnterColumn { column: 0 },
RenderOperation::RenderText { line: "A".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },

View File

@ -84,7 +84,7 @@ pub(crate) enum RenderOperation {
///
/// The value for each column is the width of the column in column-unit units, where the entire
/// screen contains `columns.sum()` column-units.
InitColumnLayout { columns: Vec<u8>, grid: LayoutGrid },
InitColumnLayout { columns: Vec<u8>, grid: LayoutGrid, margin: Margin },
/// Enter a column in a column layout.
///

View File

@ -44,6 +44,7 @@ pub(crate) struct PresentationTheme {
pub(crate) block_quote: BlockQuoteStyle,
pub(crate) alert: AlertStyle,
pub(crate) default_style: DefaultStyle,
pub(crate) column_layout: ColumnLayoutStyle,
pub(crate) headings: HeadingStyles,
pub(crate) intro_slide: IntroSlideStyle,
pub(crate) footer: FooterStyle,
@ -73,6 +74,7 @@ impl PresentationTheme {
block_quote,
alert,
default_style,
column_layout,
headings,
intro_slide,
footer,
@ -99,6 +101,7 @@ impl PresentationTheme {
block_quote: BlockQuoteStyle::new(block_quote, &palette)?,
alert: AlertStyle::new(alert, &palette)?,
default_style: default_style.clone(),
column_layout: ColumnLayoutStyle::new(column_layout),
headings: HeadingStyles::new(headings, &palette, options)?,
intro_slide: IntroSlideStyle::new(intro_slide, &palette, options)?,
footer: FooterStyle::new(&footer.clone().unwrap_or_default(), &palette, resources)?,
@ -470,6 +473,19 @@ impl DefaultStyle {
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ColumnLayoutStyle {
pub(crate) margin: Margin,
}
impl ColumnLayoutStyle {
fn new(raw: &raw::ColumnLayoutStyle) -> Self {
let raw::ColumnLayoutStyle { margin } = raw;
let margin = margin.unwrap_or_default();
Self { margin }
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum Alignment {
Left { margin: Margin },

View File

@ -63,6 +63,10 @@ pub struct PresentationTheme {
#[serde(rename = "default", default)]
pub(crate) default_style: DefaultStyle,
/// The style for column layouts.
#[serde(default)]
pub(crate) column_layout: ColumnLayoutStyle,
//// The style of all headings.
#[serde(default)]
pub(crate) headings: HeadingStyles,
@ -356,6 +360,14 @@ pub(crate) struct DefaultStyle {
pub(crate) alignment: Option<Alignment>,
}
/// The column layout style.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub(crate) struct ColumnLayoutStyle {
/// The margin in between two columns.
#[serde(default, with = "serde_yaml::with::singleton_map")]
pub(crate) margin: Option<Margin>,
}
/// A simple style.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub(crate) struct BasicStyle {

View File

@ -6,6 +6,10 @@ default:
foreground: palette:text
background: palette:base
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: palette:text
background: palette:base
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: palette:text
background: palette:base
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: palette:text
background: palette:base
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: palette:white
background: "040312"
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: "ebdbb2"
background: "282828"
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: "212529"
background: "f8f9fa"
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: null
background: null
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: null
background: null
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: "3760bf"
background: "e1e2e7"
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: "c8d3f5"
background: "222436"
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: "c0caf5"
background: "1a1b26"
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1

View File

@ -6,6 +6,10 @@ default:
foreground: "c0caf5"
background: "24283b"
column_layout:
margin:
fixed: 4
slide_title:
alignment: center
padding_bottom: 1