feat: allow using images on right in footer (#554)
Some checks failed
Deploy docs / build-and-deploy (push) Has been cancelled
Merge checks / Checks (push) Has been cancelled
Merge checks / Validate nix flake (push) Has been cancelled
Merge checks / Validate bat assets (push) Has been cancelled
Merge checks / Validate JSON schemas (push) Has been cancelled

This adds support for footer images on the right, just like allowed for
left/center. Now that there's tests for this and after being
restructured fairly recently it's much easier to get _right_.

The following presentation now renders like the following:

```markdown
---
theme:
    override:
        footer:
            height: 5
            style: template
            left:
                image: ../examples/doge.png
            center:
                image: ../examples/doge.png
            right:
                image: ../examples/doge.png
---

Footer images
===
```


![image](https://github.com/user-attachments/assets/2dfeb096-eba6-43a2-973c-b3d187c14086)

Closes #549
This commit is contained in:
Matias Fontanini 2025-04-17 16:04:45 -07:00 committed by GitHub
commit 31f7c6c1e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 88 additions and 35 deletions

View File

@ -1,4 +1,6 @@
use super::{RenderError, RenderResult, layout::Layout, properties::CursorPosition, text::TextDrawer};
use super::{
RenderError, RenderResult, layout::Layout, operation::ImagePosition, properties::CursorPosition, text::TextDrawer,
};
use crate::{
config::{MaxColumnsAlignment, MaxRowsAlignment},
markdown::{text::WeightedLine, text_style::Colors},
@ -274,9 +276,10 @@ where
(image_scale.columns, image_scale.rows)
}
};
let cursor = match properties.center {
true => Self::center_cursor(columns, &rect.dimensions, &starting_cursor),
false => starting_cursor.clone(),
let cursor = match &properties.position {
ImagePosition::Cursor => starting_cursor.clone(),
ImagePosition::Center => Self::center_cursor(columns, &rect.dimensions, &starting_cursor),
ImagePosition::Right => Self::align_cursor_right(columns, &rect.dimensions, &starting_cursor),
};
self.terminal.execute(&TerminalCommand::MoveToColumn(cursor.column))?;
@ -303,6 +306,11 @@ where
CursorPosition { row: cursor.row, column: start_column }
}
fn align_cursor_right(columns: u16, window: &WindowSize, cursor: &CursorPosition) -> CursorPosition {
let start_column = window.columns.saturating_sub(columns).saturating_add(cursor.column);
CursorPosition { row: cursor.row, column: start_column }
}
fn render_block_line(&mut self, operation: &BlockLine) -> RenderResult {
let BlockLine {
text,
@ -806,8 +814,13 @@ mod tests {
fn image(#[case] size: ImageSize) {
let image = DynamicImage::new(2, 2, ColorType::Rgba8);
let image = Image::new(TerminalImage::Ascii(image.into()), ImageSource::Generated);
let properties =
ImageRenderProperties { z_index: 0, size, restore_cursor: false, background_color: None, center: false };
let properties = ImageRenderProperties {
z_index: 0,
size,
restore_cursor: false,
background_color: None,
position: ImagePosition::Cursor,
};
let ops = render_with_max_size(&[RenderOperation::RenderImage(image, properties)]);
let expected = [
// centered 20x10, the image is 2x2 so we stand one away from center
@ -835,8 +848,13 @@ mod tests {
fn centered_image(#[case] size: ImageSize) {
let image = DynamicImage::new(2, 2, ColorType::Rgba8);
let image = Image::new(TerminalImage::Ascii(image.into()), ImageSource::Generated);
let properties =
ImageRenderProperties { z_index: 0, size, restore_cursor: false, background_color: None, center: true };
let properties = ImageRenderProperties {
z_index: 0,
size,
restore_cursor: false,
background_color: None,
position: ImagePosition::Center,
};
let ops = render_with_max_size(&[RenderOperation::RenderImage(image, properties)]);
let expected = [
// centered 20x10, the image is 2x2 so we stand one away from center
@ -856,6 +874,40 @@ mod tests {
assert_eq!(ops, expected);
}
// same as the above but use right alignment
#[rstest]
#[case::shrink(ImageSize::ShrinkIfNeeded)]
#[case::specific(ImageSize::Specific(2, 2))]
#[case::width_scaled(ImageSize::WidthScaled { ratio: 1.0 })]
fn right_aligned_image(#[case] size: ImageSize) {
let image = DynamicImage::new(2, 2, ColorType::Rgba8);
let image = Image::new(TerminalImage::Ascii(image.into()), ImageSource::Generated);
let properties = ImageRenderProperties {
z_index: 0,
size,
restore_cursor: false,
background_color: None,
position: ImagePosition::Right,
};
let ops = render_with_max_size(&[RenderOperation::RenderImage(image, properties)]);
let expected = [
// right aligned 20x10, the image is 2x2 so we stand one away from the right
Instruction::MoveTo(40, 45),
Instruction::MoveToColumn(58),
Instruction::PrintImage(PrintOptions {
columns: 2,
rows: 2,
z_index: 0,
background_color: None,
column_width: 2,
row_height: 2,
}),
// place cursor after the image
Instruction::MoveToRow(47),
];
assert_eq!(ops, expected);
}
// same as the above but center it
#[rstest]
fn restore_cursor_after_image() {
@ -866,7 +918,7 @@ mod tests {
size: ImageSize::ShrinkIfNeeded,
restore_cursor: true,
background_color: None,
center: true,
position: ImagePosition::Center,
};
let ops = render_with_max_size(&[RenderOperation::RenderImage(image, properties)]);
let expected = [

View File

@ -101,7 +101,7 @@ pub(crate) struct ImageRenderProperties {
pub(crate) size: ImageSize,
pub(crate) restore_cursor: bool,
pub(crate) background_color: Option<Color>,
pub(crate) center: bool,
pub(crate) position: ImagePosition,
}
impl Default for ImageRenderProperties {
@ -111,11 +111,18 @@ impl Default for ImageRenderProperties {
size: Default::default(),
restore_cursor: false,
background_color: None,
center: true,
position: ImagePosition::Cursor,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum ImagePosition {
Cursor,
Center,
Right,
}
/// The size used when printing an image.
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) enum ImageSize {

View File

@ -474,7 +474,7 @@ pub(crate) enum FooterStyle {
Template {
left: Option<FooterContent>,
center: Option<FooterContent>,
right: Option<FooterTemplate>,
right: Option<FooterContent>,
style: TextStyle,
height: u16,
},
@ -496,7 +496,7 @@ impl FooterStyle {
raw::FooterStyle::Template { left, center, right, colors, height } => {
let left = left.as_ref().map(|t| FooterContent::new(t, resources)).transpose()?;
let center = center.as_ref().map(|t| FooterContent::new(t, resources)).transpose()?;
let right = right.clone();
let right = right.as_ref().map(|t| FooterContent::new(t, resources)).transpose()?;
let style = TextStyle::colored(colors.resolve(palette)?);
let height = height.unwrap_or(DEFAULT_FOOTER_HEIGHT);
Ok(Self::Template { left, center, right, style, height })

View File

@ -412,7 +412,7 @@ pub(super) enum FooterStyle {
center: Option<FooterContent>,
/// The content to be put on the right.
right: Option<FooterTemplate>,
right: Option<FooterContent>,
/// The colors to be used.
#[serde(default)]

View File

@ -5,7 +5,7 @@ use crate::{
text_style::{TextStyle, UndefinedPaletteColorError},
},
render::{
operation::{AsRenderOperations, ImageRenderProperties, MarginProperties, RenderOperation},
operation::{AsRenderOperations, ImagePosition, ImageRenderProperties, MarginProperties, RenderOperation},
properties::WindowSize,
},
terminal::image::Image,
@ -53,14 +53,8 @@ impl FooterGenerator {
]);
}
fn push_image(
&self,
image: &Image,
alignment: Alignment,
dimensions: &WindowSize,
operations: &mut Vec<RenderOperation>,
) {
let mut properties = ImageRenderProperties { center: false, ..Default::default() };
fn push_image(&self, image: &Image, alignment: Alignment, operations: &mut Vec<RenderOperation>) {
let mut properties = ImageRenderProperties::default();
operations.push(RenderOperation::ApplyMargin(MarginProperties {
horizontal: Margin::Fixed(0),
@ -70,11 +64,12 @@ impl FooterGenerator {
match alignment {
Alignment::Left { .. } => {
operations.push(RenderOperation::JumpToColumn { index: 0 });
properties.position = ImagePosition::Cursor;
}
Alignment::Right { .. } => {
operations.push(RenderOperation::JumpToColumn { index: dimensions.columns.saturating_sub(1) });
properties.position = ImagePosition::Right;
}
Alignment::Center { .. } => properties.center = true,
Alignment::Center { .. } => properties.position = ImagePosition::Center,
};
operations.extend([
// Start printing the image at the top of the footer rect
@ -101,23 +96,20 @@ impl AsRenderOperations for FooterGenerator {
let alignments = [
Alignment::Left { margin: Default::default() },
Alignment::Center { minimum_size: 0, minimum_margin: Default::default() },
Alignment::Right { margin: Default::default() },
];
for (content, alignment) in [left, center].iter().zip(alignments) {
for (content, alignment) in [left, center, right].iter().zip(alignments) {
if let Some(content) = content {
match content {
RenderedFooterContent::Line(line) => {
Self::render_line(line, alignment, *height, &mut operations);
}
RenderedFooterContent::Image(image) => {
self.push_image(image, alignment, dimensions, &mut operations);
self.push_image(image, alignment, &mut operations);
}
};
}
}
// We don't support images on the right so treat this differently
if let Some(line) = right {
Self::render_line(line, Alignment::Right { margin: Default::default() }, *height, &mut operations);
}
operations.push(RenderOperation::PopMargin);
operations
}
@ -146,7 +138,7 @@ enum RenderedFooterStyle {
Template {
left: Option<RenderedFooterContent>,
center: Option<RenderedFooterContent>,
right: Option<FooterLine>,
right: Option<RenderedFooterContent>,
height: u16,
},
ProgressBar {
@ -166,7 +158,7 @@ impl RenderedFooterStyle {
FooterStyle::Template { left, center, right, style, height } => {
let left = left.map(|c| RenderedFooterContent::new(c, &style, vars, palette)).transpose()?;
let center = center.map(|c| RenderedFooterContent::new(c, &style, vars, palette)).transpose()?;
let right = right.map(|c| FooterLine::new(c, &style, vars, palette)).transpose()?;
let right = right.map(|c| RenderedFooterContent::new(c, &style, vars, palette)).transpose()?;
Ok(Self::Template { left, center, right, height })
}
FooterStyle::ProgressBar { character, style } => Ok(Self::ProgressBar { character, style }),

View File

@ -9,7 +9,9 @@ use crate::{
},
presentation::PresentationState,
render::{
operation::{AsRenderOperations, ImageRenderProperties, ImageSize, MarginProperties, RenderOperation},
operation::{
AsRenderOperations, ImagePosition, ImageRenderProperties, ImageSize, MarginProperties, RenderOperation,
},
properties::WindowSize,
},
terminal::image::Image,
@ -307,7 +309,7 @@ impl AsRenderOperations for CenterModalContent {
size: ImageSize::Specific(self.content_width, content_height),
restore_cursor: true,
background_color: None,
center: true,
position: ImagePosition::Center,
};
operations.push(RenderOperation::RenderImage(image.clone(), properties));
}