use crate::annotations::{Annotation, AnnotationType};
use crate::error::Result;
use crate::forms::{ComboBox, ListBox};
use crate::geometry::Rectangle;
use crate::graphics::Color;
use crate::objects::{Dictionary, Object, Stream};
use crate::text::Font;
use std::fmt::Write;
#[derive(Debug, Clone)]
pub struct ChoiceWidget {
pub rect: Rectangle,
pub border_color: Color,
pub border_width: f64,
pub background_color: Option<Color>,
pub text_color: Color,
pub font: Font,
pub font_size: f64,
pub highlight_color: Option<Color>,
}
impl Default for ChoiceWidget {
fn default() -> Self {
Self {
rect: Rectangle::from_position_and_size(0.0, 0.0, 100.0, 20.0),
border_color: Color::rgb(0.0, 0.0, 0.0),
border_width: 1.0,
background_color: Some(Color::rgb(1.0, 1.0, 1.0)),
text_color: Color::rgb(0.0, 0.0, 0.0),
font: Font::Helvetica,
font_size: 10.0,
highlight_color: Some(Color::rgb(0.8, 0.8, 1.0)),
}
}
}
impl ChoiceWidget {
pub fn new(rect: Rectangle) -> Self {
Self {
rect,
..Default::default()
}
}
pub fn with_border_color(mut self, color: Color) -> Self {
self.border_color = color;
self
}
pub fn with_border_width(mut self, width: f64) -> Self {
self.border_width = width;
self
}
pub fn with_background_color(mut self, color: Option<Color>) -> Self {
self.background_color = color;
self
}
pub fn with_text_color(mut self, color: Color) -> Self {
self.text_color = color;
self
}
pub fn with_font(mut self, font: Font) -> Self {
self.font = font;
self
}
pub fn with_font_size(mut self, size: f64) -> Self {
self.font_size = size;
self
}
pub fn with_highlight_color(mut self, color: Option<Color>) -> Self {
self.highlight_color = color;
self
}
fn create_combobox_appearance(&self, combo: &ComboBox) -> String {
let mut stream = String::new();
writeln!(&mut stream, "q").expect("Writing to string should never fail");
if let Some(bg_color) = &self.background_color {
writeln!(
&mut stream,
"{:.3} {:.3} {:.3} rg",
bg_color.r(),
bg_color.g(),
bg_color.b()
)
.expect("Writing to string should never fail");
writeln!(
&mut stream,
"0 0 {} {} re",
self.rect.width(),
self.rect.height()
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "f").expect("Writing to string should never fail");
}
writeln!(
&mut stream,
"{:.3} {:.3} {:.3} RG",
self.border_color.r(),
self.border_color.g(),
self.border_color.b()
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "{} w", self.border_width)
.expect("Writing to string should never fail");
writeln!(
&mut stream,
"0 0 {} {} re",
self.rect.width(),
self.rect.height()
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "S").expect("Writing to string should never fail");
let arrow_x = self.rect.width() - 15.0;
let arrow_y = self.rect.height() / 2.0;
writeln!(&mut stream, "{:.3} {:.3} {:.3} rg", 0.3, 0.3, 0.3)
.expect("Writing to string should never fail");
writeln!(&mut stream, "{} {} m", arrow_x, arrow_y + 3.0)
.expect("Writing to string should never fail");
writeln!(&mut stream, "{} {} l", arrow_x + 8.0, arrow_y + 3.0)
.expect("Writing to string should never fail");
writeln!(&mut stream, "{} {} l", arrow_x + 4.0, arrow_y - 3.0)
.expect("Writing to string should never fail");
writeln!(&mut stream, "f").expect("Writing to string should never fail");
if let Some(selected_idx) = combo.selected {
if let Some((_, display_text)) = combo.options.get(selected_idx) {
writeln!(&mut stream, "BT").expect("Writing to string should never fail");
writeln!(
&mut stream,
"/{} {} Tf",
self.font.pdf_name(),
self.font_size
)
.expect("Writing to string should never fail");
writeln!(
&mut stream,
"{:.3} {:.3} {:.3} rg",
self.text_color.r(),
self.text_color.g(),
self.text_color.b()
)
.expect("Writing to string should never fail");
writeln!(
&mut stream,
"2 {} Td",
(self.rect.height() - self.font_size) / 2.0
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "({}) Tj", escape_pdf_string(display_text))
.expect("Writing to string should never fail");
writeln!(&mut stream, "ET").expect("Writing to string should never fail");
}
}
writeln!(&mut stream, "Q").expect("Writing to string should never fail");
stream
}
fn create_listbox_appearance(&self, listbox: &ListBox) -> String {
let mut stream = String::new();
writeln!(&mut stream, "q").expect("Writing to string should never fail");
if let Some(bg_color) = &self.background_color {
writeln!(
&mut stream,
"{:.3} {:.3} {:.3} rg",
bg_color.r(),
bg_color.g(),
bg_color.b()
)
.expect("Writing to string should never fail");
writeln!(
&mut stream,
"0 0 {} {} re",
self.rect.width(),
self.rect.height()
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "f").expect("Writing to string should never fail");
}
writeln!(
&mut stream,
"{:.3} {:.3} {:.3} RG",
self.border_color.r(),
self.border_color.g(),
self.border_color.b()
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "{} w", self.border_width)
.expect("Writing to string should never fail");
writeln!(
&mut stream,
"0 0 {} {} re",
self.rect.width(),
self.rect.height()
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "S").expect("Writing to string should never fail");
let item_height = self.font_size + 4.0;
let visible_items = (self.rect.height() / item_height) as usize;
for (idx, (_, display_text)) in listbox.options.iter().enumerate().take(visible_items) {
let y_pos = self.rect.height() - ((idx + 1) as f64 * item_height);
if listbox.selected.contains(&idx) {
if let Some(highlight) = &self.highlight_color {
writeln!(
&mut stream,
"{:.3} {:.3} {:.3} rg",
highlight.r(),
highlight.g(),
highlight.b()
)
.expect("Writing to string should never fail");
writeln!(
&mut stream,
"0 {} {} {} re",
y_pos,
self.rect.width(),
item_height
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "f").expect("Writing to string should never fail");
}
}
writeln!(&mut stream, "BT").expect("Writing to string should never fail");
writeln!(
&mut stream,
"/{} {} Tf",
self.font.pdf_name(),
self.font_size
)
.expect("Writing to string should never fail");
writeln!(
&mut stream,
"{:.3} {:.3} {:.3} rg",
self.text_color.r(),
self.text_color.g(),
self.text_color.b()
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "2 {} Td", y_pos + 2.0)
.expect("Writing to string should never fail");
writeln!(&mut stream, "({}) Tj", escape_pdf_string(display_text))
.expect("Writing to string should never fail");
writeln!(&mut stream, "ET").expect("Writing to string should never fail");
}
if listbox.options.len() > visible_items {
writeln!(&mut stream, "0.7 0.7 0.7 rg").expect("Writing to string should never fail");
let scrollbar_x = self.rect.width() - 10.0;
writeln!(&mut stream, "{} 0 8 {} re", scrollbar_x, self.rect.height())
.expect("Writing to string should never fail");
writeln!(&mut stream, "f").expect("Writing to string should never fail");
writeln!(&mut stream, "0.4 0.4 0.4 rg").expect("Writing to string should never fail");
let thumb_height =
(visible_items as f64 / listbox.options.len() as f64) * self.rect.height();
writeln!(
&mut stream,
"{} {} 8 {} re",
scrollbar_x,
self.rect.height() - thumb_height,
thumb_height
)
.expect("Writing to string should never fail");
writeln!(&mut stream, "f").expect("Writing to string should never fail");
}
writeln!(&mut stream, "Q").expect("Writing to string should never fail");
stream
}
}
pub fn create_combobox_widget(combo: &ComboBox, widget: &ChoiceWidget) -> Result<Annotation> {
let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
let mut field_dict = combo.to_dict();
field_dict.set(
"Rect",
Object::Array(vec![
Object::Real(widget.rect.lower_left.x),
Object::Real(widget.rect.lower_left.y),
Object::Real(widget.rect.upper_right.x),
Object::Real(widget.rect.upper_right.y),
]),
);
let appearance_content = widget.create_combobox_appearance(combo);
let appearance_stream = create_appearance_stream(
appearance_content.as_bytes(),
widget.rect.width(),
widget.rect.height(),
);
let mut ap_dict = Dictionary::new();
let mut n_dict = Dictionary::new();
n_dict.set(
"default",
Object::Stream(
appearance_stream.dictionary().clone(),
appearance_stream.data().to_vec(),
),
);
ap_dict.set("N", Object::Dictionary(n_dict));
field_dict.set("AP", Object::Dictionary(ap_dict));
let da = format!(
"/{} {} Tf {} {} {} rg",
widget.font.pdf_name(),
widget.font_size,
widget.text_color.r(),
widget.text_color.g(),
widget.text_color.b(),
);
field_dict.set("DA", Object::String(da));
annotation.set_field_dict(field_dict);
Ok(annotation)
}
pub fn create_listbox_widget(listbox: &ListBox, widget: &ChoiceWidget) -> Result<Annotation> {
let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
let mut field_dict = listbox.to_dict();
field_dict.set(
"Rect",
Object::Array(vec![
Object::Real(widget.rect.lower_left.x),
Object::Real(widget.rect.lower_left.y),
Object::Real(widget.rect.upper_right.x),
Object::Real(widget.rect.upper_right.y),
]),
);
let appearance_content = widget.create_listbox_appearance(listbox);
let appearance_stream = create_appearance_stream(
appearance_content.as_bytes(),
widget.rect.width(),
widget.rect.height(),
);
let mut ap_dict = Dictionary::new();
let mut n_dict = Dictionary::new();
n_dict.set(
"default",
Object::Stream(
appearance_stream.dictionary().clone(),
appearance_stream.data().to_vec(),
),
);
ap_dict.set("N", Object::Dictionary(n_dict));
field_dict.set("AP", Object::Dictionary(ap_dict));
let da = format!(
"/{} {} Tf {} {} {} rg",
widget.font.pdf_name(),
widget.font_size,
widget.text_color.r(),
widget.text_color.g(),
widget.text_color.b(),
);
field_dict.set("DA", Object::String(da));
annotation.set_field_dict(field_dict);
Ok(annotation)
}
fn escape_pdf_string(s: &str) -> String {
s.chars()
.map(|c| match c {
'(' => "\\(".to_string(),
')' => "\\)".to_string(),
'\\' => "\\\\".to_string(),
_ => c.to_string(),
})
.collect()
}
fn create_appearance_stream(content: &[u8], width: f64, height: f64) -> Stream {
let mut dict = Dictionary::new();
dict.set("Type", Object::Name("XObject".to_string()));
dict.set("Subtype", Object::Name("Form".to_string()));
dict.set(
"BBox",
Object::Array(vec![
Object::Integer(0),
Object::Integer(0),
Object::Real(width),
Object::Real(height),
]),
);
Stream::with_dictionary(dict, content.to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Point;
#[test]
fn test_choice_widget_creation() {
let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
let widget = ChoiceWidget::new(rect.clone());
assert_eq!(widget.rect, rect);
assert_eq!(widget.font_size, 10.0);
}
#[test]
fn test_choice_widget_builder() {
let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 30.0));
let widget = ChoiceWidget::new(rect)
.with_border_color(Color::rgb(1.0, 0.0, 0.0))
.with_font_size(12.0)
.with_font(Font::HelveticaBold);
assert_eq!(widget.border_color, Color::rgb(1.0, 0.0, 0.0));
assert_eq!(widget.font_size, 12.0);
assert_eq!(widget.font, Font::HelveticaBold);
}
#[test]
fn test_combobox_widget_creation() {
let combo = ComboBox::new("country")
.add_option("US", "United States")
.add_option("CA", "Canada")
.with_selected(0);
let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(250.0, 125.0));
let widget = ChoiceWidget::new(rect);
let annotation = create_combobox_widget(&combo, &widget);
assert!(annotation.is_ok());
}
#[test]
fn test_listbox_widget_creation() {
let listbox = ListBox::new("languages")
.add_option("en", "English")
.add_option("es", "Spanish")
.add_option("fr", "French")
.with_selected(vec![0, 2]);
let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 200.0));
let widget = ChoiceWidget::new(rect);
let annotation = create_listbox_widget(&listbox, &widget);
assert!(annotation.is_ok());
}
#[test]
fn test_escape_pdf_string() {
assert_eq!(escape_pdf_string("Hello"), "Hello");
assert_eq!(escape_pdf_string("Hello (World)"), "Hello \\(World\\)");
assert_eq!(escape_pdf_string("Path\\to\\file"), "Path\\\\to\\\\file");
}
}