1use crate::annotations::{Annotation, AnnotationType};
7use crate::error::Result;
8use crate::forms::{ComboBox, ListBox};
9use crate::geometry::Rectangle;
10use crate::graphics::Color;
11use crate::objects::{Dictionary, Object, Stream};
12use crate::text::Font;
13use std::fmt::Write;
14
15#[derive(Debug, Clone)]
17pub struct ChoiceWidget {
18 pub rect: Rectangle,
20 pub border_color: Color,
22 pub border_width: f64,
24 pub background_color: Option<Color>,
26 pub text_color: Color,
28 pub font: Font,
30 pub font_size: f64,
32 pub highlight_color: Option<Color>,
34}
35
36impl Default for ChoiceWidget {
37 fn default() -> Self {
38 Self {
39 rect: Rectangle::from_position_and_size(0.0, 0.0, 100.0, 20.0),
40 border_color: Color::rgb(0.0, 0.0, 0.0),
41 border_width: 1.0,
42 background_color: Some(Color::rgb(1.0, 1.0, 1.0)),
43 text_color: Color::rgb(0.0, 0.0, 0.0),
44 font: Font::Helvetica,
45 font_size: 10.0,
46 highlight_color: Some(Color::rgb(0.8, 0.8, 1.0)),
47 }
48 }
49}
50
51impl ChoiceWidget {
52 pub fn new(rect: Rectangle) -> Self {
54 Self {
55 rect,
56 ..Default::default()
57 }
58 }
59
60 pub fn with_border_color(mut self, color: Color) -> Self {
62 self.border_color = color;
63 self
64 }
65
66 pub fn with_border_width(mut self, width: f64) -> Self {
68 self.border_width = width;
69 self
70 }
71
72 pub fn with_background_color(mut self, color: Option<Color>) -> Self {
74 self.background_color = color;
75 self
76 }
77
78 pub fn with_text_color(mut self, color: Color) -> Self {
80 self.text_color = color;
81 self
82 }
83
84 pub fn with_font(mut self, font: Font) -> Self {
86 self.font = font;
87 self
88 }
89
90 pub fn with_font_size(mut self, size: f64) -> Self {
92 self.font_size = size;
93 self
94 }
95
96 pub fn with_highlight_color(mut self, color: Option<Color>) -> Self {
98 self.highlight_color = color;
99 self
100 }
101
102 fn create_combobox_appearance(&self, combo: &ComboBox) -> String {
104 let mut stream = String::new();
105
106 writeln!(&mut stream, "q").expect("Writing to string should never fail");
108
109 if let Some(bg_color) = &self.background_color {
115 crate::graphics::color::write_fill_color(&mut stream, *bg_color);
116 writeln!(
117 &mut stream,
118 "0 0 {} {} re",
119 self.rect.width(),
120 self.rect.height()
121 )
122 .expect("Writing to string should never fail");
123 writeln!(&mut stream, "f").expect("Writing to string should never fail");
124 }
125
126 crate::graphics::color::write_stroke_color(&mut stream, self.border_color);
128 writeln!(&mut stream, "{} w", self.border_width)
129 .expect("Writing to string should never fail");
130 writeln!(
131 &mut stream,
132 "0 0 {} {} re",
133 self.rect.width(),
134 self.rect.height()
135 )
136 .expect("Writing to string should never fail");
137 writeln!(&mut stream, "S").expect("Writing to string should never fail");
138
139 let arrow_x = self.rect.width() - 15.0;
141 let arrow_y = self.rect.height() / 2.0;
142 crate::graphics::color::write_fill_color(
143 &mut stream,
144 crate::graphics::Color::Rgb(0.3, 0.3, 0.3),
145 );
146 writeln!(&mut stream, "{} {} m", arrow_x, arrow_y + 3.0)
147 .expect("Writing to string should never fail");
148 writeln!(&mut stream, "{} {} l", arrow_x + 8.0, arrow_y + 3.0)
149 .expect("Writing to string should never fail");
150 writeln!(&mut stream, "{} {} l", arrow_x + 4.0, arrow_y - 3.0)
151 .expect("Writing to string should never fail");
152 writeln!(&mut stream, "f").expect("Writing to string should never fail");
153
154 if let Some(selected_idx) = combo.selected {
156 if let Some((_, display_text)) = combo.options.get(selected_idx) {
157 writeln!(&mut stream, "BT").expect("Writing to string should never fail");
158 writeln!(
159 &mut stream,
160 "/{} {} Tf",
161 self.font.pdf_name(),
162 self.font_size
163 )
164 .expect("Writing to string should never fail");
165 crate::graphics::color::write_fill_color(&mut stream, self.text_color);
166 writeln!(
167 &mut stream,
168 "2 {} Td",
169 (self.rect.height() - self.font_size) / 2.0
170 )
171 .expect("Writing to string should never fail");
172 writeln!(&mut stream, "({}) Tj", escape_pdf_string(display_text))
173 .expect("Writing to string should never fail");
174 writeln!(&mut stream, "ET").expect("Writing to string should never fail");
175 }
176 }
177
178 writeln!(&mut stream, "Q").expect("Writing to string should never fail");
180
181 stream
182 }
183
184 fn create_listbox_appearance(&self, listbox: &ListBox) -> String {
186 let mut stream = String::new();
187
188 writeln!(&mut stream, "q").expect("Writing to string should never fail");
190
191 if let Some(bg_color) = &self.background_color {
193 crate::graphics::color::write_fill_color(&mut stream, *bg_color);
194 writeln!(
195 &mut stream,
196 "0 0 {} {} re",
197 self.rect.width(),
198 self.rect.height()
199 )
200 .expect("Writing to string should never fail");
201 writeln!(&mut stream, "f").expect("Writing to string should never fail");
202 }
203
204 crate::graphics::color::write_stroke_color(&mut stream, self.border_color);
206 writeln!(&mut stream, "{} w", self.border_width)
207 .expect("Writing to string should never fail");
208 writeln!(
209 &mut stream,
210 "0 0 {} {} re",
211 self.rect.width(),
212 self.rect.height()
213 )
214 .expect("Writing to string should never fail");
215 writeln!(&mut stream, "S").expect("Writing to string should never fail");
216
217 let item_height = self.font_size + 4.0;
219 let visible_items = (self.rect.height() / item_height) as usize;
220
221 for (idx, (_, display_text)) in listbox.options.iter().enumerate().take(visible_items) {
224 let y_pos = self.rect.height() - ((idx + 1) as f64 * item_height);
225
226 if listbox.selected.contains(&idx) {
228 if let Some(highlight) = &self.highlight_color {
229 crate::graphics::color::write_fill_color(&mut stream, *highlight);
230 writeln!(
231 &mut stream,
232 "0 {} {} {} re",
233 y_pos,
234 self.rect.width(),
235 item_height
236 )
237 .expect("Writing to string should never fail");
238 writeln!(&mut stream, "f").expect("Writing to string should never fail");
239 }
240 }
241
242 writeln!(&mut stream, "BT").expect("Writing to string should never fail");
244 writeln!(
245 &mut stream,
246 "/{} {} Tf",
247 self.font.pdf_name(),
248 self.font_size
249 )
250 .expect("Writing to string should never fail");
251 crate::graphics::color::write_fill_color(&mut stream, self.text_color);
252 writeln!(&mut stream, "2 {} Td", y_pos + 2.0)
253 .expect("Writing to string should never fail");
254 writeln!(&mut stream, "({}) Tj", escape_pdf_string(display_text))
255 .expect("Writing to string should never fail");
256 writeln!(&mut stream, "ET").expect("Writing to string should never fail");
257 }
258
259 if listbox.options.len() > visible_items {
261 crate::graphics::color::write_fill_color(&mut stream, Color::gray(0.7));
262 let scrollbar_x = self.rect.width() - 10.0;
263 writeln!(&mut stream, "{} 0 8 {} re", scrollbar_x, self.rect.height())
264 .expect("Writing to string should never fail");
265 writeln!(&mut stream, "f").expect("Writing to string should never fail");
266
267 crate::graphics::color::write_fill_color(&mut stream, Color::gray(0.4));
269 let thumb_height =
270 (visible_items as f64 / listbox.options.len() as f64) * self.rect.height();
271 writeln!(
272 &mut stream,
273 "{} {} 8 {} re",
274 scrollbar_x,
275 self.rect.height() - thumb_height,
276 thumb_height
277 )
278 .expect("Writing to string should never fail");
279 writeln!(&mut stream, "f").expect("Writing to string should never fail");
280 }
281
282 writeln!(&mut stream, "Q").expect("Writing to string should never fail");
284
285 stream
286 }
287}
288
289pub fn create_combobox_widget(combo: &ComboBox, widget: &ChoiceWidget) -> Result<Annotation> {
291 let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
292
293 let mut field_dict = combo.to_dict();
295
296 field_dict.set(
298 "Rect",
299 Object::Array(vec![
300 Object::Real(widget.rect.lower_left.x),
301 Object::Real(widget.rect.lower_left.y),
302 Object::Real(widget.rect.upper_right.x),
303 Object::Real(widget.rect.upper_right.y),
304 ]),
305 );
306
307 let appearance_content = widget.create_combobox_appearance(combo);
309 let appearance_stream = create_appearance_stream(
310 appearance_content.as_bytes(),
311 widget.rect.width(),
312 widget.rect.height(),
313 );
314
315 let mut ap_dict = Dictionary::new();
317 let mut n_dict = Dictionary::new();
318 n_dict.set(
319 "default",
320 Object::Stream(
321 appearance_stream.dictionary().clone(),
322 appearance_stream.data().to_vec(),
323 ),
324 );
325 ap_dict.set("N", Object::Dictionary(n_dict));
326 field_dict.set("AP", Object::Dictionary(ap_dict));
327
328 let da = format!(
331 "/{} {} Tf {}",
332 widget.font.pdf_name(),
333 widget.font_size,
334 crate::graphics::color::fill_color_op(widget.text_color),
335 );
336 field_dict.set("DA", Object::String(da));
337
338 annotation.set_field_dict(field_dict);
340
341 Ok(annotation)
342}
343
344pub fn create_listbox_widget(listbox: &ListBox, widget: &ChoiceWidget) -> Result<Annotation> {
346 let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
347
348 let mut field_dict = listbox.to_dict();
350
351 field_dict.set(
353 "Rect",
354 Object::Array(vec![
355 Object::Real(widget.rect.lower_left.x),
356 Object::Real(widget.rect.lower_left.y),
357 Object::Real(widget.rect.upper_right.x),
358 Object::Real(widget.rect.upper_right.y),
359 ]),
360 );
361
362 let appearance_content = widget.create_listbox_appearance(listbox);
364 let appearance_stream = create_appearance_stream(
365 appearance_content.as_bytes(),
366 widget.rect.width(),
367 widget.rect.height(),
368 );
369
370 let mut ap_dict = Dictionary::new();
372 let mut n_dict = Dictionary::new();
373 n_dict.set(
374 "default",
375 Object::Stream(
376 appearance_stream.dictionary().clone(),
377 appearance_stream.data().to_vec(),
378 ),
379 );
380 ap_dict.set("N", Object::Dictionary(n_dict));
381 field_dict.set("AP", Object::Dictionary(ap_dict));
382
383 let da = format!(
386 "/{} {} Tf {}",
387 widget.font.pdf_name(),
388 widget.font_size,
389 crate::graphics::color::fill_color_op(widget.text_color),
390 );
391 field_dict.set("DA", Object::String(da));
392
393 annotation.set_field_dict(field_dict);
395
396 Ok(annotation)
397}
398
399fn escape_pdf_string(s: &str) -> String {
401 s.chars()
402 .map(|c| match c {
403 '(' => "\\(".to_string(),
404 ')' => "\\)".to_string(),
405 '\\' => "\\\\".to_string(),
406 _ => c.to_string(),
407 })
408 .collect()
409}
410
411fn create_appearance_stream(content: &[u8], width: f64, height: f64) -> Stream {
413 let mut dict = Dictionary::new();
414 dict.set("Type", Object::Name("XObject".to_string()));
415 dict.set("Subtype", Object::Name("Form".to_string()));
416 dict.set(
417 "BBox",
418 Object::Array(vec![
419 Object::Integer(0),
420 Object::Integer(0),
421 Object::Real(width),
422 Object::Real(height),
423 ]),
424 );
425
426 Stream::with_dictionary(dict, content.to_vec())
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use crate::geometry::Point;
433
434 #[test]
435 fn test_choice_widget_creation() {
436 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
437 let widget = ChoiceWidget::new(rect.clone());
438
439 assert_eq!(widget.rect, rect);
440 assert_eq!(widget.font_size, 10.0);
441 }
442
443 #[test]
444 fn test_choice_widget_builder() {
445 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 30.0));
446 let widget = ChoiceWidget::new(rect)
447 .with_border_color(Color::rgb(1.0, 0.0, 0.0))
448 .with_font_size(12.0)
449 .with_font(Font::HelveticaBold);
450
451 assert_eq!(widget.border_color, Color::rgb(1.0, 0.0, 0.0));
452 assert_eq!(widget.font_size, 12.0);
453 assert_eq!(widget.font, Font::HelveticaBold);
454 }
455
456 #[test]
457 fn test_combobox_widget_creation() {
458 let combo = ComboBox::new("country")
459 .add_option("US", "United States")
460 .add_option("CA", "Canada")
461 .with_selected(0);
462
463 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(250.0, 125.0));
464 let widget = ChoiceWidget::new(rect);
465
466 let annotation = create_combobox_widget(&combo, &widget);
467 assert!(annotation.is_ok());
468 }
469
470 #[test]
471 fn test_listbox_widget_creation() {
472 let listbox = ListBox::new("languages")
473 .add_option("en", "English")
474 .add_option("es", "Spanish")
475 .add_option("fr", "French")
476 .with_selected(vec![0, 2]);
477
478 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 200.0));
479 let widget = ChoiceWidget::new(rect);
480
481 let annotation = create_listbox_widget(&listbox, &widget);
482 assert!(annotation.is_ok());
483 }
484
485 #[test]
486 fn test_escape_pdf_string() {
487 assert_eq!(escape_pdf_string("Hello"), "Hello");
488 assert_eq!(escape_pdf_string("Hello (World)"), "Hello \\(World\\)");
489 assert_eq!(escape_pdf_string("Path\\to\\file"), "Path\\\\to\\\\file");
490 }
491}