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 {
111 writeln!(
112 &mut stream,
113 "{:.3} {:.3} {:.3} rg",
114 bg_color.r(),
115 bg_color.g(),
116 bg_color.b()
117 )
118 .expect("Writing to string should never fail");
119 writeln!(
120 &mut stream,
121 "0 0 {} {} re",
122 self.rect.width(),
123 self.rect.height()
124 )
125 .expect("Writing to string should never fail");
126 writeln!(&mut stream, "f").expect("Writing to string should never fail");
127 }
128
129 writeln!(
131 &mut stream,
132 "{:.3} {:.3} {:.3} RG",
133 self.border_color.r(),
134 self.border_color.g(),
135 self.border_color.b()
136 )
137 .expect("Writing to string should never fail");
138 writeln!(&mut stream, "{} w", self.border_width)
139 .expect("Writing to string should never fail");
140 writeln!(
141 &mut stream,
142 "0 0 {} {} re",
143 self.rect.width(),
144 self.rect.height()
145 )
146 .expect("Writing to string should never fail");
147 writeln!(&mut stream, "S").expect("Writing to string should never fail");
148
149 let arrow_x = self.rect.width() - 15.0;
151 let arrow_y = self.rect.height() / 2.0;
152 writeln!(&mut stream, "{:.3} {:.3} {:.3} rg", 0.3, 0.3, 0.3)
153 .expect("Writing to string should never fail");
154 writeln!(&mut stream, "{} {} m", arrow_x, arrow_y + 3.0)
155 .expect("Writing to string should never fail");
156 writeln!(&mut stream, "{} {} l", arrow_x + 8.0, arrow_y + 3.0)
157 .expect("Writing to string should never fail");
158 writeln!(&mut stream, "{} {} l", arrow_x + 4.0, arrow_y - 3.0)
159 .expect("Writing to string should never fail");
160 writeln!(&mut stream, "f").expect("Writing to string should never fail");
161
162 if let Some(selected_idx) = combo.selected {
164 if let Some((_, display_text)) = combo.options.get(selected_idx) {
165 writeln!(&mut stream, "BT").expect("Writing to string should never fail");
166 writeln!(
167 &mut stream,
168 "/{} {} Tf",
169 self.font.pdf_name(),
170 self.font_size
171 )
172 .expect("Writing to string should never fail");
173 writeln!(
174 &mut stream,
175 "{:.3} {:.3} {:.3} rg",
176 self.text_color.r(),
177 self.text_color.g(),
178 self.text_color.b()
179 )
180 .expect("Writing to string should never fail");
181 writeln!(
182 &mut stream,
183 "2 {} Td",
184 (self.rect.height() - self.font_size) / 2.0
185 )
186 .expect("Writing to string should never fail");
187 writeln!(&mut stream, "({}) Tj", escape_pdf_string(display_text))
188 .expect("Writing to string should never fail");
189 writeln!(&mut stream, "ET").expect("Writing to string should never fail");
190 }
191 }
192
193 writeln!(&mut stream, "Q").expect("Writing to string should never fail");
195
196 stream
197 }
198
199 fn create_listbox_appearance(&self, listbox: &ListBox) -> String {
201 let mut stream = String::new();
202
203 writeln!(&mut stream, "q").expect("Writing to string should never fail");
205
206 if let Some(bg_color) = &self.background_color {
208 writeln!(
209 &mut stream,
210 "{:.3} {:.3} {:.3} rg",
211 bg_color.r(),
212 bg_color.g(),
213 bg_color.b()
214 )
215 .expect("Writing to string should never fail");
216 writeln!(
217 &mut stream,
218 "0 0 {} {} re",
219 self.rect.width(),
220 self.rect.height()
221 )
222 .expect("Writing to string should never fail");
223 writeln!(&mut stream, "f").expect("Writing to string should never fail");
224 }
225
226 writeln!(
228 &mut stream,
229 "{:.3} {:.3} {:.3} RG",
230 self.border_color.r(),
231 self.border_color.g(),
232 self.border_color.b()
233 )
234 .expect("Writing to string should never fail");
235 writeln!(&mut stream, "{} w", self.border_width)
236 .expect("Writing to string should never fail");
237 writeln!(
238 &mut stream,
239 "0 0 {} {} re",
240 self.rect.width(),
241 self.rect.height()
242 )
243 .expect("Writing to string should never fail");
244 writeln!(&mut stream, "S").expect("Writing to string should never fail");
245
246 let item_height = self.font_size + 4.0;
248 let visible_items = (self.rect.height() / item_height) as usize;
249
250 for (idx, (_, display_text)) in listbox.options.iter().enumerate().take(visible_items) {
253 let y_pos = self.rect.height() - ((idx + 1) as f64 * item_height);
254
255 if listbox.selected.contains(&idx) {
257 if let Some(highlight) = &self.highlight_color {
258 writeln!(
259 &mut stream,
260 "{:.3} {:.3} {:.3} rg",
261 highlight.r(),
262 highlight.g(),
263 highlight.b()
264 )
265 .expect("Writing to string should never fail");
266 writeln!(
267 &mut stream,
268 "0 {} {} {} re",
269 y_pos,
270 self.rect.width(),
271 item_height
272 )
273 .expect("Writing to string should never fail");
274 writeln!(&mut stream, "f").expect("Writing to string should never fail");
275 }
276 }
277
278 writeln!(&mut stream, "BT").expect("Writing to string should never fail");
280 writeln!(
281 &mut stream,
282 "/{} {} Tf",
283 self.font.pdf_name(),
284 self.font_size
285 )
286 .expect("Writing to string should never fail");
287 writeln!(
288 &mut stream,
289 "{:.3} {:.3} {:.3} rg",
290 self.text_color.r(),
291 self.text_color.g(),
292 self.text_color.b()
293 )
294 .expect("Writing to string should never fail");
295 writeln!(&mut stream, "2 {} Td", y_pos + 2.0)
296 .expect("Writing to string should never fail");
297 writeln!(&mut stream, "({}) Tj", escape_pdf_string(display_text))
298 .expect("Writing to string should never fail");
299 writeln!(&mut stream, "ET").expect("Writing to string should never fail");
300 }
301
302 if listbox.options.len() > visible_items {
304 writeln!(&mut stream, "0.7 0.7 0.7 rg").expect("Writing to string should never fail");
305 let scrollbar_x = self.rect.width() - 10.0;
306 writeln!(&mut stream, "{} 0 8 {} re", scrollbar_x, self.rect.height())
307 .expect("Writing to string should never fail");
308 writeln!(&mut stream, "f").expect("Writing to string should never fail");
309
310 writeln!(&mut stream, "0.4 0.4 0.4 rg").expect("Writing to string should never fail");
312 let thumb_height =
313 (visible_items as f64 / listbox.options.len() as f64) * self.rect.height();
314 writeln!(
315 &mut stream,
316 "{} {} 8 {} re",
317 scrollbar_x,
318 self.rect.height() - thumb_height,
319 thumb_height
320 )
321 .expect("Writing to string should never fail");
322 writeln!(&mut stream, "f").expect("Writing to string should never fail");
323 }
324
325 writeln!(&mut stream, "Q").expect("Writing to string should never fail");
327
328 stream
329 }
330}
331
332pub fn create_combobox_widget(combo: &ComboBox, widget: &ChoiceWidget) -> Result<Annotation> {
334 let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
335
336 let mut field_dict = combo.to_dict();
338
339 field_dict.set(
341 "Rect",
342 Object::Array(vec![
343 Object::Real(widget.rect.lower_left.x),
344 Object::Real(widget.rect.lower_left.y),
345 Object::Real(widget.rect.upper_right.x),
346 Object::Real(widget.rect.upper_right.y),
347 ]),
348 );
349
350 let appearance_content = widget.create_combobox_appearance(combo);
352 let appearance_stream = create_appearance_stream(
353 appearance_content.as_bytes(),
354 widget.rect.width(),
355 widget.rect.height(),
356 );
357
358 let mut ap_dict = Dictionary::new();
360 let mut n_dict = Dictionary::new();
361 n_dict.set(
362 "default",
363 Object::Stream(
364 appearance_stream.dictionary().clone(),
365 appearance_stream.data().to_vec(),
366 ),
367 );
368 ap_dict.set("N", Object::Dictionary(n_dict));
369 field_dict.set("AP", Object::Dictionary(ap_dict));
370
371 let da = format!(
373 "/{} {} Tf {} {} {} rg",
374 widget.font.pdf_name(),
375 widget.font_size,
376 widget.text_color.r(),
377 widget.text_color.g(),
378 widget.text_color.b(),
379 );
380 field_dict.set("DA", Object::String(da));
381
382 annotation.set_field_dict(field_dict);
384
385 Ok(annotation)
386}
387
388pub fn create_listbox_widget(listbox: &ListBox, widget: &ChoiceWidget) -> Result<Annotation> {
390 let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
391
392 let mut field_dict = listbox.to_dict();
394
395 field_dict.set(
397 "Rect",
398 Object::Array(vec![
399 Object::Real(widget.rect.lower_left.x),
400 Object::Real(widget.rect.lower_left.y),
401 Object::Real(widget.rect.upper_right.x),
402 Object::Real(widget.rect.upper_right.y),
403 ]),
404 );
405
406 let appearance_content = widget.create_listbox_appearance(listbox);
408 let appearance_stream = create_appearance_stream(
409 appearance_content.as_bytes(),
410 widget.rect.width(),
411 widget.rect.height(),
412 );
413
414 let mut ap_dict = Dictionary::new();
416 let mut n_dict = Dictionary::new();
417 n_dict.set(
418 "default",
419 Object::Stream(
420 appearance_stream.dictionary().clone(),
421 appearance_stream.data().to_vec(),
422 ),
423 );
424 ap_dict.set("N", Object::Dictionary(n_dict));
425 field_dict.set("AP", Object::Dictionary(ap_dict));
426
427 let da = format!(
429 "/{} {} Tf {} {} {} rg",
430 widget.font.pdf_name(),
431 widget.font_size,
432 widget.text_color.r(),
433 widget.text_color.g(),
434 widget.text_color.b(),
435 );
436 field_dict.set("DA", Object::String(da));
437
438 annotation.set_field_dict(field_dict);
440
441 Ok(annotation)
442}
443
444fn escape_pdf_string(s: &str) -> String {
446 s.chars()
447 .map(|c| match c {
448 '(' => "\\(".to_string(),
449 ')' => "\\)".to_string(),
450 '\\' => "\\\\".to_string(),
451 _ => c.to_string(),
452 })
453 .collect()
454}
455
456fn create_appearance_stream(content: &[u8], width: f64, height: f64) -> Stream {
458 let mut dict = Dictionary::new();
459 dict.set("Type", Object::Name("XObject".to_string()));
460 dict.set("Subtype", Object::Name("Form".to_string()));
461 dict.set(
462 "BBox",
463 Object::Array(vec![
464 Object::Integer(0),
465 Object::Integer(0),
466 Object::Real(width),
467 Object::Real(height),
468 ]),
469 );
470
471 Stream::with_dictionary(dict, content.to_vec())
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use crate::geometry::Point;
478
479 #[test]
480 fn test_choice_widget_creation() {
481 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
482 let widget = ChoiceWidget::new(rect.clone());
483
484 assert_eq!(widget.rect, rect);
485 assert_eq!(widget.font_size, 10.0);
486 }
487
488 #[test]
489 fn test_choice_widget_builder() {
490 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 30.0));
491 let widget = ChoiceWidget::new(rect)
492 .with_border_color(Color::rgb(1.0, 0.0, 0.0))
493 .with_font_size(12.0)
494 .with_font(Font::HelveticaBold);
495
496 assert_eq!(widget.border_color, Color::rgb(1.0, 0.0, 0.0));
497 assert_eq!(widget.font_size, 12.0);
498 assert_eq!(widget.font, Font::HelveticaBold);
499 }
500
501 #[test]
502 fn test_combobox_widget_creation() {
503 let combo = ComboBox::new("country")
504 .add_option("US", "United States")
505 .add_option("CA", "Canada")
506 .with_selected(0);
507
508 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(250.0, 125.0));
509 let widget = ChoiceWidget::new(rect);
510
511 let annotation = create_combobox_widget(&combo, &widget);
512 assert!(annotation.is_ok());
513 }
514
515 #[test]
516 fn test_listbox_widget_creation() {
517 let listbox = ListBox::new("languages")
518 .add_option("en", "English")
519 .add_option("es", "Spanish")
520 .add_option("fr", "French")
521 .with_selected(vec![0, 2]);
522
523 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 200.0));
524 let widget = ChoiceWidget::new(rect);
525
526 let annotation = create_listbox_widget(&listbox, &widget);
527 assert!(annotation.is_ok());
528 }
529
530 #[test]
531 fn test_escape_pdf_string() {
532 assert_eq!(escape_pdf_string("Hello"), "Hello");
533 assert_eq!(escape_pdf_string("Hello (World)"), "Hello \\(World\\)");
534 assert_eq!(escape_pdf_string("Path\\to\\file"), "Path\\\\to\\\\file");
535 }
536}