use super::{ChoiceFieldFlags, FormFieldEntry, FormFieldWidget};
use crate::geometry::Rect;
use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ComboBoxWidget {
name: String,
rect: Rect,
options: Vec<ChoiceOption>,
value: Option<String>,
default_value: Option<String>,
flags: ChoiceFieldFlags,
font_name: String,
font_size: f32,
text_color: (f32, f32, f32),
border_color: Option<(f32, f32, f32)>,
background_color: Option<(f32, f32, f32)>,
border_width: f32,
tooltip: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ListBoxWidget {
name: String,
rect: Rect,
options: Vec<ChoiceOption>,
values: Vec<String>,
default_values: Vec<String>,
flags: ChoiceFieldFlags,
font_name: String,
font_size: f32,
text_color: (f32, f32, f32),
border_color: Option<(f32, f32, f32)>,
background_color: Option<(f32, f32, f32)>,
border_width: f32,
tooltip: Option<String>,
top_index: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct ChoiceOption {
pub display: String,
pub export: String,
}
impl ChoiceOption {
pub fn new(value: impl Into<String>) -> Self {
let v = value.into();
Self {
display: v.clone(),
export: v,
}
}
pub fn new_with_export(display: impl Into<String>, export: impl Into<String>) -> Self {
Self {
display: display.into(),
export: export.into(),
}
}
}
impl ComboBoxWidget {
pub fn new(name: impl Into<String>, rect: Rect) -> Self {
Self {
name: name.into(),
rect,
options: Vec::new(),
value: None,
default_value: None,
flags: ChoiceFieldFlags::COMBO, font_name: "Helv".to_string(),
font_size: 12.0,
text_color: (0.0, 0.0, 0.0),
border_color: Some((0.0, 0.0, 0.0)),
background_color: Some((1.0, 1.0, 1.0)),
border_width: 1.0,
tooltip: None,
}
}
pub fn with_options(mut self, options: Vec<impl Into<String>>) -> Self {
self.options = options.into_iter().map(|s| ChoiceOption::new(s)).collect();
self
}
pub fn with_choice_options(mut self, options: Vec<ChoiceOption>) -> Self {
self.options = options;
self
}
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn with_default_value(mut self, value: impl Into<String>) -> Self {
self.default_value = Some(value.into());
self
}
pub fn editable(mut self) -> Self {
self.flags |= ChoiceFieldFlags::EDIT;
self
}
pub fn sorted(mut self) -> Self {
self.flags |= ChoiceFieldFlags::SORT;
self
}
pub fn read_only(mut self) -> Self {
self.flags |= ChoiceFieldFlags::READ_ONLY;
self
}
pub fn required(mut self) -> Self {
self.flags |= ChoiceFieldFlags::REQUIRED;
self
}
pub fn commit_on_change(mut self) -> Self {
self.flags |= ChoiceFieldFlags::COMMIT_ON_SEL_CHANGE;
self
}
pub fn with_font(mut self, name: impl Into<String>, size: f32) -> Self {
self.font_name = name.into();
self.font_size = size;
self
}
pub fn with_text_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.text_color = (r, g, b);
self
}
pub fn with_border_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.border_color = Some((r, g, b));
self
}
pub fn with_background_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.background_color = Some((r, g, b));
self
}
pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
fn build_default_appearance(&self) -> String {
let (r, g, b) = self.text_color;
format!("/{} {} Tf {} {} {} rg", self.font_name, self.font_size, r, g, b)
}
pub fn build_entry(&self, page_ref: ObjectRef) -> FormFieldEntry {
FormFieldEntry {
widget_dict: self.build_widget_dict(page_ref),
field_dict: self.build_field_dict(),
name: self.name.clone(),
rect: self.rect,
field_type: "Ch".to_string(),
}
}
}
impl FormFieldWidget for ComboBoxWidget {
fn field_name(&self) -> &str {
&self.name
}
fn rect(&self) -> Rect {
self.rect
}
fn field_type(&self) -> &'static str {
"Ch"
}
fn field_flags(&self) -> u32 {
self.flags.bits()
}
fn build_field_dict(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("FT".to_string(), Object::Name("Ch".to_string()));
dict.insert("T".to_string(), Object::String(self.name.as_bytes().to_vec()));
let opt_array: Vec<Object> = self
.options
.iter()
.map(|opt| {
if opt.display == opt.export {
Object::String(opt.display.as_bytes().to_vec())
} else {
Object::Array(vec![
Object::String(opt.export.as_bytes().to_vec()),
Object::String(opt.display.as_bytes().to_vec()),
])
}
})
.collect();
dict.insert("Opt".to_string(), Object::Array(opt_array));
if let Some(ref value) = self.value {
dict.insert("V".to_string(), Object::String(value.as_bytes().to_vec()));
}
if let Some(ref dv) = self.default_value {
dict.insert("DV".to_string(), Object::String(dv.as_bytes().to_vec()));
}
dict.insert("Ff".to_string(), Object::Integer(self.flags.bits() as i64));
dict.insert("DA".to_string(), Object::String(self.build_default_appearance().into_bytes()));
dict
}
fn build_widget_dict(&self, page_ref: ObjectRef) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("Annot".to_string()));
dict.insert("Subtype".to_string(), Object::Name("Widget".to_string()));
dict.insert(
"Rect".to_string(),
Object::Array(vec![
Object::Real(self.rect.x as f64),
Object::Real(self.rect.y as f64),
Object::Real((self.rect.x + self.rect.width) as f64),
Object::Real((self.rect.y + self.rect.height) as f64),
]),
);
dict.insert("P".to_string(), Object::Reference(page_ref));
dict.insert("F".to_string(), Object::Integer(4));
if let Some(ref tip) = self.tooltip {
dict.insert("TU".to_string(), Object::String(tip.as_bytes().to_vec()));
}
if self.border_width > 0.0 {
let mut bs = HashMap::new();
bs.insert("W".to_string(), Object::Real(self.border_width as f64));
bs.insert("S".to_string(), Object::Name("S".to_string()));
dict.insert("BS".to_string(), Object::Dictionary(bs));
}
let mut mk = HashMap::new();
if let Some((r, g, b)) = self.border_color {
mk.insert(
"BC".to_string(),
Object::Array(vec![
Object::Real(r as f64),
Object::Real(g as f64),
Object::Real(b as f64),
]),
);
}
if let Some((r, g, b)) = self.background_color {
mk.insert(
"BG".to_string(),
Object::Array(vec![
Object::Real(r as f64),
Object::Real(g as f64),
Object::Real(b as f64),
]),
);
}
if !mk.is_empty() {
dict.insert("MK".to_string(), Object::Dictionary(mk));
}
dict
}
}
impl ListBoxWidget {
pub fn new(name: impl Into<String>, rect: Rect) -> Self {
Self {
name: name.into(),
rect,
options: Vec::new(),
values: Vec::new(),
default_values: Vec::new(),
flags: ChoiceFieldFlags::empty(), font_name: "Helv".to_string(),
font_size: 12.0,
text_color: (0.0, 0.0, 0.0),
border_color: Some((0.0, 0.0, 0.0)),
background_color: Some((1.0, 1.0, 1.0)),
border_width: 1.0,
tooltip: None,
top_index: None,
}
}
pub fn with_options(mut self, options: Vec<impl Into<String>>) -> Self {
self.options = options.into_iter().map(|s| ChoiceOption::new(s)).collect();
self
}
pub fn with_choice_options(mut self, options: Vec<ChoiceOption>) -> Self {
self.options = options;
self
}
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.values = vec![value.into()];
self
}
pub fn with_values(mut self, values: Vec<impl Into<String>>) -> Self {
self.values = values.into_iter().map(|v| v.into()).collect();
self
}
pub fn with_default_value(mut self, value: impl Into<String>) -> Self {
self.default_values = vec![value.into()];
self
}
pub fn multi_select(mut self) -> Self {
self.flags |= ChoiceFieldFlags::MULTI_SELECT;
self
}
pub fn sorted(mut self) -> Self {
self.flags |= ChoiceFieldFlags::SORT;
self
}
pub fn read_only(mut self) -> Self {
self.flags |= ChoiceFieldFlags::READ_ONLY;
self
}
pub fn required(mut self) -> Self {
self.flags |= ChoiceFieldFlags::REQUIRED;
self
}
pub fn commit_on_change(mut self) -> Self {
self.flags |= ChoiceFieldFlags::COMMIT_ON_SEL_CHANGE;
self
}
pub fn with_top_index(mut self, index: u32) -> Self {
self.top_index = Some(index);
self
}
pub fn with_font(mut self, name: impl Into<String>, size: f32) -> Self {
self.font_name = name.into();
self.font_size = size;
self
}
pub fn with_text_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.text_color = (r, g, b);
self
}
pub fn with_border_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.border_color = Some((r, g, b));
self
}
pub fn with_background_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.background_color = Some((r, g, b));
self
}
pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
fn build_default_appearance(&self) -> String {
let (r, g, b) = self.text_color;
format!("/{} {} Tf {} {} {} rg", self.font_name, self.font_size, r, g, b)
}
pub fn build_entry(&self, page_ref: ObjectRef) -> FormFieldEntry {
FormFieldEntry {
widget_dict: self.build_widget_dict(page_ref),
field_dict: self.build_field_dict(),
name: self.name.clone(),
rect: self.rect,
field_type: "Ch".to_string(),
}
}
}
impl FormFieldWidget for ListBoxWidget {
fn field_name(&self) -> &str {
&self.name
}
fn rect(&self) -> Rect {
self.rect
}
fn field_type(&self) -> &'static str {
"Ch"
}
fn field_flags(&self) -> u32 {
self.flags.bits()
}
fn build_field_dict(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("FT".to_string(), Object::Name("Ch".to_string()));
dict.insert("T".to_string(), Object::String(self.name.as_bytes().to_vec()));
let opt_array: Vec<Object> = self
.options
.iter()
.map(|opt| {
if opt.display == opt.export {
Object::String(opt.display.as_bytes().to_vec())
} else {
Object::Array(vec![
Object::String(opt.export.as_bytes().to_vec()),
Object::String(opt.display.as_bytes().to_vec()),
])
}
})
.collect();
dict.insert("Opt".to_string(), Object::Array(opt_array));
if !self.values.is_empty() {
if self.values.len() == 1 {
dict.insert("V".to_string(), Object::String(self.values[0].as_bytes().to_vec()));
} else {
let v_array: Vec<Object> = self
.values
.iter()
.map(|v| Object::String(v.as_bytes().to_vec()))
.collect();
dict.insert("V".to_string(), Object::Array(v_array));
}
}
if !self.default_values.is_empty() {
if self.default_values.len() == 1 {
dict.insert(
"DV".to_string(),
Object::String(self.default_values[0].as_bytes().to_vec()),
);
} else {
let dv_array: Vec<Object> = self
.default_values
.iter()
.map(|v| Object::String(v.as_bytes().to_vec()))
.collect();
dict.insert("DV".to_string(), Object::Array(dv_array));
}
}
if self.flags.bits() != 0 {
dict.insert("Ff".to_string(), Object::Integer(self.flags.bits() as i64));
}
dict.insert("DA".to_string(), Object::String(self.build_default_appearance().into_bytes()));
if let Some(ti) = self.top_index {
dict.insert("TI".to_string(), Object::Integer(ti as i64));
}
dict
}
fn build_widget_dict(&self, page_ref: ObjectRef) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("Annot".to_string()));
dict.insert("Subtype".to_string(), Object::Name("Widget".to_string()));
dict.insert(
"Rect".to_string(),
Object::Array(vec![
Object::Real(self.rect.x as f64),
Object::Real(self.rect.y as f64),
Object::Real((self.rect.x + self.rect.width) as f64),
Object::Real((self.rect.y + self.rect.height) as f64),
]),
);
dict.insert("P".to_string(), Object::Reference(page_ref));
dict.insert("F".to_string(), Object::Integer(4));
if let Some(ref tip) = self.tooltip {
dict.insert("TU".to_string(), Object::String(tip.as_bytes().to_vec()));
}
if self.border_width > 0.0 {
let mut bs = HashMap::new();
bs.insert("W".to_string(), Object::Real(self.border_width as f64));
bs.insert("S".to_string(), Object::Name("S".to_string()));
dict.insert("BS".to_string(), Object::Dictionary(bs));
}
let mut mk = HashMap::new();
if let Some((r, g, b)) = self.border_color {
mk.insert(
"BC".to_string(),
Object::Array(vec![
Object::Real(r as f64),
Object::Real(g as f64),
Object::Real(b as f64),
]),
);
}
if let Some((r, g, b)) = self.background_color {
mk.insert(
"BG".to_string(),
Object::Array(vec![
Object::Real(r as f64),
Object::Real(g as f64),
Object::Real(b as f64),
]),
);
}
if !mk.is_empty() {
dict.insert("MK".to_string(), Object::Dictionary(mk));
}
dict
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_choice_option() {
let opt = ChoiceOption::new("USA");
assert_eq!(opt.display, "USA");
assert_eq!(opt.export, "USA");
let opt2 = ChoiceOption::new_with_export("United States", "USA");
assert_eq!(opt2.display, "United States");
assert_eq!(opt2.export, "USA");
}
#[test]
fn test_combo_box_new() {
let combo = ComboBoxWidget::new("country", Rect::new(72.0, 700.0, 150.0, 20.0));
assert_eq!(combo.name, "country");
assert!(combo.flags.contains(ChoiceFieldFlags::COMBO));
}
#[test]
fn test_combo_box_with_options() {
let combo = ComboBoxWidget::new("country", Rect::new(72.0, 700.0, 150.0, 20.0))
.with_options(vec!["USA", "Canada", "UK"])
.with_value("USA");
assert_eq!(combo.options.len(), 3);
assert_eq!(combo.value, Some("USA".to_string()));
}
#[test]
fn test_combo_box_editable() {
let combo = ComboBoxWidget::new("country", Rect::new(72.0, 700.0, 150.0, 20.0)).editable();
assert!(combo.flags.contains(ChoiceFieldFlags::EDIT));
}
#[test]
fn test_combo_box_build_field_dict() {
let combo = ComboBoxWidget::new("country", Rect::new(72.0, 700.0, 150.0, 20.0))
.with_options(vec!["USA", "Canada"])
.with_value("USA")
.required();
let dict = combo.build_field_dict();
assert_eq!(dict.get("FT"), Some(&Object::Name("Ch".to_string())));
assert!(dict.contains_key("Opt"));
assert!(dict.contains_key("V"));
assert!(dict.contains_key("Ff"));
assert!(dict.contains_key("DA"));
}
#[test]
fn test_list_box_new() {
let list = ListBoxWidget::new("interests", Rect::new(72.0, 600.0, 150.0, 80.0));
assert_eq!(list.name, "interests");
assert!(!list.flags.contains(ChoiceFieldFlags::COMBO));
}
#[test]
fn test_list_box_multi_select() {
let list = ListBoxWidget::new("interests", Rect::new(72.0, 600.0, 150.0, 80.0))
.with_options(vec!["Sports", "Music", "Art"])
.multi_select()
.with_values(vec!["Sports", "Music"]);
assert!(list.flags.contains(ChoiceFieldFlags::MULTI_SELECT));
assert_eq!(list.values.len(), 2);
}
#[test]
fn test_list_box_build_field_dict() {
let list = ListBoxWidget::new("items", Rect::new(72.0, 600.0, 150.0, 80.0))
.with_options(vec!["A", "B", "C"])
.with_value("A")
.with_top_index(0);
let dict = list.build_field_dict();
assert_eq!(dict.get("FT"), Some(&Object::Name("Ch".to_string())));
assert!(dict.contains_key("Opt"));
assert!(dict.contains_key("V"));
assert!(dict.contains_key("TI"));
}
#[test]
fn test_list_box_multi_value_dict() {
let list = ListBoxWidget::new("items", Rect::new(72.0, 600.0, 150.0, 80.0))
.with_options(vec!["A", "B", "C"])
.multi_select()
.with_values(vec!["A", "B"]);
let dict = list.build_field_dict();
if let Some(Object::Array(arr)) = dict.get("V") {
assert_eq!(arr.len(), 2);
} else {
panic!("Expected V to be an array for multi-select");
}
}
#[test]
fn test_combo_widget_dict() {
let combo = ComboBoxWidget::new("test", Rect::new(72.0, 700.0, 150.0, 20.0))
.with_tooltip("Select a value")
.with_border_color(0.0, 0.0, 1.0);
let page_ref = ObjectRef::new(10, 0);
let dict = combo.build_widget_dict(page_ref);
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Widget".to_string())));
assert!(dict.contains_key("TU"));
assert!(dict.contains_key("MK"));
}
#[test]
fn test_choice_option_pairs() {
let combo = ComboBoxWidget::new("status", Rect::new(72.0, 700.0, 150.0, 20.0))
.with_choice_options(vec![
ChoiceOption::new_with_export("Active", "1"),
ChoiceOption::new_with_export("Inactive", "0"),
]);
let dict = combo.build_field_dict();
if let Some(Object::Array(opts)) = dict.get("Opt") {
if let Object::Array(first) = &opts[0] {
assert_eq!(first.len(), 2);
} else {
panic!("Expected option to be array for export/display pair");
}
}
}
}