mod button;
pub mod choice;
pub mod text;
pub mod toggle;
use std::num::NonZeroU32;
use pdfrum_doc::form::{FieldFlags, FieldKind};
pub use button::ButtonState;
pub use toggle::{ToggleKind, ToggleState, activate};
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TextConfig {
pub multi_line: bool,
pub password: bool,
pub comb: bool,
pub max_len: Option<NonZeroU32>,
pub read_only: bool,
pub undo_enabled: bool,
pub auto_scroll: bool,
}
impl TextConfig {
#[must_use]
pub fn read(flags: FieldFlags, max_len: Option<u32>) -> TextConfig {
TextConfig {
multi_line: flags.is_multiline(),
password: flags.is_password(),
comb: flags.is_comb(),
max_len: max_len.and_then(NonZeroU32::new),
read_only: flags.is_read_only(),
undo_enabled: true,
auto_scroll: flags.scrolls(),
}
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ChoiceConfig {
pub combo: bool,
pub editable: bool,
pub multi_select: bool,
pub read_only: bool,
}
impl ChoiceConfig {
#[must_use]
pub fn read(flags: FieldFlags) -> ChoiceConfig {
ChoiceConfig {
combo: flags.is_combo(),
editable: flags.is_combo() && flags.is_editable_combo(),
multi_select: !flags.is_combo() && flags.is_multi_select(),
read_only: flags.is_read_only(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldState {
Text(TextState),
Choice(ChoiceState),
Toggle(ToggleState),
Button(ButtonState),
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextState {
pub edit: crate::edit::TextEdit,
pub config: TextConfig,
}
impl TextState {
#[must_use]
pub fn text(&self) -> &str {
&self.edit.text
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ChoiceOption {
pub label: String,
pub value: String,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ChoiceState {
pub options: Vec<ChoiceOption>,
pub selected: std::collections::BTreeSet<usize>,
pub caret_index: Option<usize>,
pub anchor: Option<usize>,
pub top_visible: usize,
pub popup_open: bool,
pub hovered: Option<usize>,
pub config: ChoiceConfig,
pub edit_text: String,
pub edit: Option<Box<crate::edit::TextEdit>>,
}
impl ChoiceState {
#[must_use]
pub fn new(options: Vec<ChoiceOption>, config: ChoiceConfig) -> ChoiceState {
ChoiceState {
options,
config,
..ChoiceState::default()
}
}
#[must_use]
pub fn focused_text(&self) -> String {
if self.config.editable {
return self.edit_text.clone();
}
let index = self
.caret_index
.or_else(|| self.selected.iter().next().copied());
index
.and_then(|i| self.options.get(i))
.map(|o| o.label.clone())
.unwrap_or_default()
}
}
#[must_use]
pub fn family_of(kind: FieldKind) -> Option<Family> {
match kind {
FieldKind::Text => Some(Family::Text),
FieldKind::Combo | FieldKind::List => Some(Family::Choice),
FieldKind::Check | FieldKind::Radio => Some(Family::Toggle),
FieldKind::Button => Some(Family::Button),
FieldKind::Signature => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
Text,
Choice,
Toggle,
Button,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_signature_widget_gets_no_interaction_state() {
assert_eq!(family_of(FieldKind::Signature), None);
assert_eq!(family_of(FieldKind::Text), Some(Family::Text));
assert_eq!(family_of(FieldKind::Combo), Some(Family::Choice));
assert_eq!(family_of(FieldKind::List), Some(Family::Choice));
assert_eq!(family_of(FieldKind::Check), Some(Family::Toggle));
assert_eq!(family_of(FieldKind::Radio), Some(Family::Toggle));
assert_eq!(family_of(FieldKind::Button), Some(Family::Button));
}
fn ff(bits: i64) -> FieldFlags {
FieldFlags::from_bits(bits)
}
#[test]
fn a_zero_length_limit_is_no_limit() {
assert_eq!(TextConfig::read(ff(0), Some(0)).max_len, None);
assert_eq!(TextConfig::read(ff(0), None).max_len, None);
assert_eq!(
TextConfig::read(ff(0), Some(10)).max_len,
NonZeroU32::new(10)
);
}
#[test]
fn a_text_field_always_records_undo() {
assert!(TextConfig::read(ff(0), None).undo_enabled);
assert!(TextConfig::read(ff(1), None).undo_enabled);
}
#[test]
fn a_field_scrolls_unless_it_is_told_not_to() {
assert!(TextConfig::read(ff(0), None).auto_scroll);
assert!(TextConfig::read(ff(1 << 12), None).auto_scroll);
assert!(!TextConfig::read(ff(1 << 23), None).auto_scroll);
}
#[test]
fn text_flags_read_the_documented_bits() {
let config = TextConfig::read(ff((1 << 12) | (1 << 13) | (1 << 24) | 1), None);
assert!(config.multi_line);
assert!(config.password);
assert!(config.comb);
assert!(config.read_only);
}
#[test]
fn only_a_combo_box_can_be_editable() {
let combo = ChoiceConfig::read(ff((1 << 17) | (1 << 18)));
assert!(combo.combo);
assert!(combo.editable);
let list = ChoiceConfig::read(ff(1 << 18));
assert!(!list.combo);
assert!(!list.editable);
}
#[test]
fn only_a_list_box_can_be_multi_select() {
let list = ChoiceConfig::read(ff(1 << 21));
assert!(list.multi_select);
let combo = ChoiceConfig::read(ff((1 << 17) | (1 << 21)));
assert!(!combo.multi_select);
}
fn options(labels: &[&str]) -> Vec<ChoiceOption> {
labels
.iter()
.map(|l| ChoiceOption {
label: (*l).to_string(),
value: (*l).to_string(),
})
.collect()
}
#[test]
fn focused_text_is_the_row_last_acted_upon() {
let mut state = ChoiceState::new(
options(&["Apple", "Banana", "Cherry", "Date"]),
ChoiceConfig::default(),
);
state.selected.insert(0);
state.selected.insert(2);
assert_eq!(state.focused_text(), "Apple");
state.caret_index = Some(3);
assert_eq!(state.focused_text(), "Date");
}
#[test]
fn an_empty_choice_field_reports_no_text() {
let state = ChoiceState::new(Vec::new(), ChoiceConfig::default());
assert_eq!(state.focused_text(), "");
}
}