Skip to main content

rpdfium_doc/variable_text/
variable_text.rs

1// Derived from PDFium's cpvt_variabletext.cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! Variable text layout engine (`CPVT_VariableText`).
7//!
8//! Ports PDFium's `CPVT_VariableText` class which performs text layout
9//! (line breaking, alignment, auto font sizing) for interactive form fields.
10
11use crate::error::{DocError, DocResult};
12
13pub use super::vt_font_map::VtFontProvider;
14pub use super::vt_line::Line;
15pub use super::vt_section::Section;
16pub use super::vt_word_info::WordInfo;
17pub use super::vt_word_place::WordPlace;
18pub use super::vt_word_range::WordRange;
19
20/// Type alias for the stateful word iterator — backward-compatible public name.
21///
22/// Corresponds to upstream `CPVT_VariableText::Iterator`.
23pub type VtIterator<'a, P> = VtWordIterator<'a, P>;
24
25/// Text alignment mode.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum Alignment {
28    /// Left-aligned (default).
29    #[default]
30    Left,
31    /// Center-aligned.
32    Center,
33    /// Right-aligned.
34    Right,
35}
36
37impl Alignment {
38    /// Parse from an integer value (`/Q` key): 0=left, 1=center, 2=right.
39    pub fn from_value(v: i32) -> Self {
40        match v {
41            1 => Self::Center,
42            2 => Self::Right,
43            _ => Self::Left,
44        }
45    }
46}
47
48/// Font size steps used by auto-font-size binary search.
49const AUTO_FONT_SIZES: &[f32] = &[
50    4.0, 6.0, 8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0,
51    48.0, 72.0, 144.0,
52];
53
54/// Variable text layout engine.
55///
56/// Performs text layout with line breaking, alignment, character array mode,
57/// password masking, and auto font sizing for form field appearance streams.
58pub struct VariableText<P: VtFontProvider> {
59    provider: P,
60    sections: Vec<Section>,
61    plate_rect: [f32; 4],
62    font_size: f32,
63    alignment: Alignment,
64    multi_line: bool,
65    auto_return: bool,
66    auto_font_size: bool,
67    char_array: Option<usize>,
68    password_char: Option<char>,
69    limit_char: Option<usize>,
70    content_rect: [f32; 4],
71}
72
73impl<P: VtFontProvider> VariableText<P> {
74    /// Create a new layout engine with the given font provider.
75    pub fn new(provider: P) -> Self {
76        Self {
77            provider,
78            sections: Vec::new(),
79            plate_rect: [0.0; 4],
80            font_size: 12.0,
81            alignment: Alignment::Left,
82            multi_line: false,
83            auto_return: true,
84            auto_font_size: false,
85            char_array: None,
86            password_char: None,
87            limit_char: None,
88            content_rect: [0.0; 4],
89        }
90    }
91
92    /// Set the bounding plate rectangle [left, bottom, right, top].
93    pub fn set_plate_rect(&mut self, rect: [f32; 4]) {
94        self.plate_rect = rect;
95    }
96
97    /// Return the bounding plate rectangle [left, bottom, right, top].
98    ///
99    /// Corresponds to `CPDF_VariableText::GetPlateRect()` in PDFium.
100    pub fn plate_rect(&self) -> [f32; 4] {
101        self.plate_rect
102    }
103
104    /// ADR-019 alias for [`plate_rect()`](Self::plate_rect).
105    ///
106    /// Corresponds to `CPDF_VariableText::GetPlateRect()` in PDFium.
107    #[inline]
108    pub fn get_plate_rect(&self) -> [f32; 4] {
109        self.plate_rect()
110    }
111
112    /// Set the font size.
113    pub fn set_font_size(&mut self, size: f32) {
114        self.font_size = size;
115    }
116
117    /// Set text alignment.
118    pub fn set_alignment(&mut self, alignment: Alignment) {
119        self.alignment = alignment;
120    }
121
122    /// Enable or disable multi-line mode.
123    pub fn set_multi_line(&mut self, multi_line: bool) {
124        self.multi_line = multi_line;
125    }
126
127    /// Enable or disable automatic line return.
128    pub fn set_auto_return(&mut self, auto_return: bool) {
129        self.auto_return = auto_return;
130    }
131
132    /// Enable or disable automatic font sizing to fit the plate rect.
133    pub fn set_auto_font_size(&mut self, auto: bool) {
134        self.auto_font_size = auto;
135    }
136
137    /// Set character array mode (fixed grid with N cells).
138    pub fn set_char_array(&mut self, count: Option<usize>) {
139        self.char_array = count;
140    }
141
142    /// Set the password masking character.
143    pub fn set_password_char(&mut self, ch: Option<char>) {
144        self.password_char = ch;
145    }
146
147    /// Set the maximum character limit.
148    pub fn set_limit_char(&mut self, limit: Option<usize>) {
149        self.limit_char = limit;
150    }
151
152    /// Parse text into sections and words.
153    ///
154    /// Sections are separated by line breaks (`\r\n`, `\r`, or `\n`).
155    /// Each character becomes a word in the current section.
156    pub fn set_text(&mut self, text: &str) {
157        self.sections.clear();
158
159        let effective_text: String = if let Some(limit) = self.limit_char {
160            text.chars().take(limit).collect()
161        } else {
162            text.to_string()
163        };
164
165        let display_text = if let Some(pw) = self.password_char {
166            pw.to_string().repeat(effective_text.chars().count())
167        } else {
168            effective_text
169        };
170
171        let mut current_section = Section {
172            words: Vec::new(),
173            lines: Vec::new(),
174            rect: [0.0; 4],
175        };
176
177        let mut chars = display_text.chars().peekable();
178        while let Some(ch) = chars.next() {
179            if ch == '\r' {
180                // \r\n or bare \r — both start a new section
181                if chars.peek() == Some(&'\n') {
182                    chars.next(); // consume the \n
183                }
184                self.sections.push(current_section);
185                current_section = Section {
186                    words: Vec::new(),
187                    lines: Vec::new(),
188                    rect: [0.0; 4],
189                };
190            } else if ch == '\n' {
191                self.sections.push(current_section);
192                current_section = Section {
193                    words: Vec::new(),
194                    lines: Vec::new(),
195                    rect: [0.0; 4],
196                };
197            } else {
198                current_section.words.push(WordInfo {
199                    character: ch,
200                    font_index: 0,
201                    position_x: 0.0,
202                    width: 0.0,
203                });
204            }
205        }
206        self.sections.push(current_section);
207    }
208
209    /// Reflow all sections into lines.
210    ///
211    /// Must be called after `set_text()` to compute the layout.
212    pub fn rearrange(&mut self) {
213        if self.auto_font_size {
214            self.font_size = self.find_auto_font_size();
215        }
216
217        let plate_width = self.plate_rect[2] - self.plate_rect[0];
218
219        let mut total_y = self.plate_rect[3]; // start from top
220
221        for section in &mut self.sections {
222            // Measure character widths
223            for word in &mut section.words {
224                word.width = self.provider.char_width(
225                    word.font_index,
226                    word.character as u16,
227                    self.font_size,
228                );
229            }
230
231            section.lines.clear();
232
233            if let Some(cell_count) = self.char_array {
234                // Character array mode: fixed grid
235                let cell_width = if cell_count > 0 {
236                    plate_width / cell_count as f32
237                } else {
238                    plate_width
239                };
240                let ascent = self.provider.ascent(0, self.font_size);
241                let descent = self.provider.descent(0, self.font_size);
242
243                let mut x_pos = 0.0;
244                for (i, word) in section.words.iter_mut().enumerate() {
245                    if i >= cell_count {
246                        break;
247                    }
248                    // Center character in cell
249                    word.position_x = x_pos + (cell_width - word.width) / 2.0;
250                    x_pos += cell_width;
251                }
252
253                let line_width = section
254                    .words
255                    .iter()
256                    .take(cell_count)
257                    .next_back()
258                    .map(|w| w.position_x + w.width)
259                    .unwrap_or(0.0);
260
261                let word_count = section.words.len().min(cell_count);
262                total_y -= ascent;
263                section.lines.push(Line {
264                    word_start: 0,
265                    word_count,
266                    x: 0.0,
267                    y: total_y,
268                    width: line_width,
269                    ascent,
270                    descent,
271                });
272                total_y += descent;
273            } else {
274                // Normal line-breaking layout
275                let ascent = self.provider.ascent(0, self.font_size);
276                let descent = self.provider.descent(0, self.font_size);
277
278                let mut line_start = 0;
279                let mut line_width = 0.0_f32;
280                let mut x_pos = 0.0_f32;
281
282                for (i, word) in section.words.iter_mut().enumerate() {
283                    let char_width = word.width;
284
285                    // Check for line break
286                    let should_break = self.multi_line
287                        && self.auto_return
288                        && line_width + char_width > plate_width
289                        && i > line_start;
290
291                    if should_break {
292                        // Finalize current line
293                        let word_count = i - line_start;
294                        total_y -= ascent;
295                        section.lines.push(Line {
296                            word_start: line_start,
297                            word_count,
298                            x: 0.0,
299                            y: total_y,
300                            width: line_width,
301                            ascent,
302                            descent,
303                        });
304                        total_y += descent;
305
306                        // Start new line
307                        line_start = i;
308                        line_width = 0.0;
309                        x_pos = 0.0;
310                    }
311
312                    word.position_x = x_pos;
313                    x_pos += char_width;
314                    line_width += char_width;
315                }
316
317                // Final line
318                let word_count = section.words.len() - line_start;
319                if word_count > 0 || section.lines.is_empty() {
320                    total_y -= ascent;
321                    section.lines.push(Line {
322                        word_start: line_start,
323                        word_count,
324                        x: 0.0,
325                        y: total_y,
326                        width: line_width,
327                        ascent,
328                        descent,
329                    });
330                    total_y += descent;
331                }
332            }
333
334            // Apply alignment
335            for line in &mut section.lines {
336                line.x = match self.alignment {
337                    Alignment::Left => 0.0,
338                    Alignment::Center => (plate_width - line.width) / 2.0,
339                    Alignment::Right => plate_width - line.width,
340                };
341            }
342
343            // Compute section rect
344            if let (Some(first), Some(last)) = (section.lines.first(), section.lines.last()) {
345                section.rect = [
346                    self.plate_rect[0],
347                    last.y + last.descent,
348                    self.plate_rect[2],
349                    first.y + first.ascent,
350                ];
351            }
352        }
353
354        // Compute content rect
355        if let (Some(first_sec), Some(last_sec)) = (self.sections.first(), self.sections.last()) {
356            self.content_rect = [
357                self.plate_rect[0],
358                last_sec.rect[1],
359                self.plate_rect[2],
360                first_sec.rect[3],
361            ];
362        }
363    }
364
365    /// Return the bounding box of the laid-out text.
366    pub fn content_rect(&self) -> [f32; 4] {
367        self.content_rect
368    }
369
370    /// ADR-019 alias for [`content_rect()`](Self::content_rect).
371    ///
372    /// Corresponds to `CPDF_VariableText::GetContentRect()` in PDFium.
373    #[inline]
374    pub fn get_content_rect(&self) -> [f32; 4] {
375        self.content_rect()
376    }
377
378    /// Return the sections after layout.
379    pub fn sections(&self) -> &[Section] {
380        &self.sections
381    }
382
383    /// Return the current font size (may differ from set value if auto-sizing).
384    pub fn font_size(&self) -> f32 {
385        self.font_size
386    }
387
388    /// Map a point (x, y) to the nearest WordPlace.
389    pub fn hit_test(&self, x: f32, y: f32) -> Option<WordPlace> {
390        let rel_x = x - self.plate_rect[0];
391        let rel_y = y;
392
393        for (si, section) in self.sections.iter().enumerate() {
394            for (li, line) in section.lines.iter().enumerate() {
395                let line_top = line.y + line.ascent;
396                let line_bottom = line.y + line.descent;
397
398                if rel_y <= line_top && rel_y >= line_bottom {
399                    // Find the nearest word on this line
400                    let line_x = rel_x - line.x;
401                    let end = line.word_start + line.word_count;
402                    for wi in line.word_start..end {
403                        let word = &section.words[wi];
404                        if line_x <= word.position_x + word.width / 2.0 {
405                            return Some(WordPlace {
406                                section: si,
407                                line: li,
408                                word: wi - line.word_start,
409                            });
410                        }
411                    }
412                    // Past end of line
413                    return Some(WordPlace {
414                        section: si,
415                        line: li,
416                        word: line.word_count.saturating_sub(1),
417                    });
418                }
419            }
420        }
421        // Below all lines — return last position
422        if let Some(last_sec) = self.sections.last() {
423            if let Some(last_line) = last_sec.lines.last() {
424                return Some(WordPlace {
425                    section: self.sections.len() - 1,
426                    line: last_sec.lines.len() - 1,
427                    word: last_line.word_count.saturating_sub(1),
428                });
429            }
430        }
431        None
432    }
433
434    /// Convert a WordPlace to a flat character index.
435    pub fn word_place_to_index(&self, place: &WordPlace) -> usize {
436        let mut index = 0;
437        for (si, section) in self.sections.iter().enumerate() {
438            if si < place.section {
439                index += section.words.len();
440                index += 1; // count the \n separator
441            } else {
442                // Find the offset within this section
443                if let Some(line) = section.lines.get(place.line) {
444                    index += line.word_start + place.word;
445                }
446                break;
447            }
448        }
449        index
450    }
451
452    /// Insert text at a given position.
453    ///
454    /// Characters are inserted at the section/line/word position described by
455    /// `place`. The layout is automatically re-computed via `rearrange()`.
456    pub fn insert(&mut self, place: &WordPlace, text: &str) -> DocResult<()> {
457        let si = place.section;
458        if si >= self.sections.len() {
459            return Err(DocError::InvalidIndex(si));
460        }
461
462        // Compute absolute word index within the section.
463        let abs_idx = {
464            let section = &self.sections[si];
465            if section.lines.is_empty() {
466                0
467            } else {
468                let li = place.line.min(section.lines.len() - 1);
469                let line = &section.lines[li];
470                let wi = place.word.min(line.word_count);
471                line.word_start + wi
472            }
473        };
474
475        // Inherit font_index from the neighbour at abs_idx (if any).
476        let font_index = self.sections[si]
477            .words
478            .get(abs_idx)
479            .or_else(|| self.sections[si].words.get(abs_idx.saturating_sub(1)))
480            .map(|w| w.font_index)
481            .unwrap_or(0);
482
483        // Insert one WordInfo per character.
484        for (offset, ch) in text.chars().enumerate() {
485            self.sections[si].words.insert(
486                abs_idx + offset,
487                WordInfo {
488                    character: ch,
489                    font_index,
490                    position_x: 0.0,
491                    width: 0.0,
492                },
493            );
494        }
495
496        self.rearrange();
497        Ok(())
498    }
499
500    /// Delete text within a range.
501    ///
502    /// `range.begin` and `range.end` must refer to the same section.
503    /// Characters from `begin` (inclusive) through `end` (exclusive) are
504    /// removed. The layout is automatically re-computed via `rearrange()`.
505    pub fn delete(&mut self, range: &WordRange) -> DocResult<()> {
506        let si = range.begin.section;
507        if si >= self.sections.len() {
508            return Err(DocError::InvalidIndex(si));
509        }
510        if range.end.section != si {
511            return Err(DocError::TypeMismatch {
512                expected: "same-section range".into(),
513                got: "cross-section range".into(),
514            });
515        }
516
517        // Convert WordPlace to absolute section word indices.
518        let begin_abs = {
519            let section = &self.sections[si];
520            if section.lines.is_empty() {
521                return Ok(());
522            }
523            let li = range.begin.line.min(section.lines.len() - 1);
524            let line = &section.lines[li];
525            line.word_start + range.begin.word.min(line.word_count)
526        };
527        let end_abs = {
528            let section = &self.sections[si];
529            let li = range.end.line.min(section.lines.len() - 1);
530            let line = &section.lines[li];
531            (line.word_start + range.end.word.min(line.word_count)).min(section.words.len())
532        };
533
534        if begin_abs < end_abs {
535            self.sections[si].words.drain(begin_abs..end_abs);
536            self.rearrange();
537        }
538        Ok(())
539    }
540
541    /// Iterate over all lines across all sections.
542    pub fn iter_lines(&self) -> impl Iterator<Item = (usize, &Line)> {
543        self.sections
544            .iter()
545            .flat_map(|section| section.lines.iter())
546            .enumerate()
547    }
548
549    /// Iterate over all words across all sections.
550    pub fn iter_words(&self) -> impl Iterator<Item = &WordInfo> {
551        self.sections
552            .iter()
553            .flat_map(|section| section.words.iter())
554    }
555
556    /// Return the total number of lines across all sections.
557    pub fn total_lines(&self) -> usize {
558        self.sections.iter().map(|s| s.lines.len()).sum()
559    }
560
561    /// Return the total number of characters across all sections.
562    pub fn total_words(&self) -> usize {
563        self.sections.iter().map(|s| s.words.len()).sum()
564    }
565
566    /// Total word count as `i32` — upstream `CPVT_VariableText::GetTotalWords()` signature.
567    ///
568    /// Returns the same value as [`total_words()`](Self::total_words) cast to `i32`
569    /// to match the upstream C++ return type.
570    ///
571    /// Corresponds to `CPVT_VariableText::GetTotalWords()` in PDFium.
572    pub fn get_total_words(&self) -> i32 {
573        self.total_words() as i32
574    }
575
576    /// Beginning of all content — section 0, line 0, word 0.
577    pub fn begin_word_place(&self) -> WordPlace {
578        WordPlace::default()
579    }
580
581    /// ADR-019 alias for `begin_word_place()`.
582    #[inline]
583    pub fn get_begin_word_place(&self) -> WordPlace {
584        self.begin_word_place()
585    }
586
587    /// End of all content — last section, last line, last word.
588    pub fn end_word_place(&self) -> WordPlace {
589        if self.sections.is_empty() {
590            return WordPlace::default();
591        }
592        let si = self.sections.len() - 1;
593        let section = &self.sections[si];
594        if section.lines.is_empty() {
595            return WordPlace {
596                section: si,
597                line: 0,
598                word: 0,
599            };
600        }
601        let li = section.lines.len() - 1;
602        let word_count = section.lines[li].word_count;
603        WordPlace {
604            section: si,
605            line: li,
606            word: word_count.saturating_sub(1),
607        }
608    }
609
610    /// ADR-019 alias for `end_word_place()`.
611    #[inline]
612    pub fn get_end_word_place(&self) -> WordPlace {
613        self.end_word_place()
614    }
615
616    /// Previous word position (move backward by one character).
617    ///
618    /// Returns the same place if already at the beginning.
619    pub fn prev_word_place(&self, place: &WordPlace) -> WordPlace {
620        if self.sections.is_empty() {
621            return *place;
622        }
623        let mut si = place.section.min(self.sections.len() - 1);
624        let section = &self.sections[si];
625        if section.lines.is_empty() {
626            if si == 0 {
627                return WordPlace::default();
628            }
629            si -= 1;
630            return self.section_end_place_for(si);
631        }
632        let mut li = place.line.min(section.lines.len() - 1);
633        let mut wi = place.word;
634
635        if wi > 0 {
636            return WordPlace {
637                section: si,
638                line: li,
639                word: wi - 1,
640            };
641        }
642        if li > 0 {
643            li -= 1;
644            wi = self.sections[si].lines[li].word_count.saturating_sub(1);
645            return WordPlace {
646                section: si,
647                line: li,
648                word: wi,
649            };
650        }
651        if si > 0 {
652            si -= 1;
653            return self.section_end_place_for(si);
654        }
655        WordPlace::default()
656    }
657
658    /// ADR-019 alias for `prev_word_place()`.
659    #[inline]
660    pub fn get_prev_word_place(&self, place: &WordPlace) -> WordPlace {
661        self.prev_word_place(place)
662    }
663
664    /// Next word position (move forward by one character).
665    ///
666    /// Returns the same place if already at the end.
667    pub fn next_word_place(&self, place: &WordPlace) -> WordPlace {
668        if self.sections.is_empty() {
669            return *place;
670        }
671        let si = place.section.min(self.sections.len() - 1);
672        let section = &self.sections[si];
673        if section.lines.is_empty() {
674            if si + 1 < self.sections.len() {
675                return WordPlace {
676                    section: si + 1,
677                    line: 0,
678                    word: 0,
679                };
680            }
681            return *place;
682        }
683        let li = place.line.min(section.lines.len() - 1);
684        let line = &section.lines[li];
685        let wi = place.word;
686
687        if wi + 1 < line.word_count {
688            return WordPlace {
689                section: si,
690                line: li,
691                word: wi + 1,
692            };
693        }
694        if li + 1 < section.lines.len() {
695            return WordPlace {
696                section: si,
697                line: li + 1,
698                word: 0,
699            };
700        }
701        if si + 1 < self.sections.len() {
702            return WordPlace {
703                section: si + 1,
704                line: 0,
705                word: 0,
706            };
707        }
708        *place
709    }
710
711    /// ADR-019 alias for `next_word_place()`.
712    #[inline]
713    pub fn get_next_word_place(&self, place: &WordPlace) -> WordPlace {
714        self.next_word_place(place)
715    }
716
717    /// Hit-test: find the word place closest to an (x, y) plate coordinate.
718    ///
719    /// Equivalent to upstream `SearchWordPlace(point)`.
720    pub fn search_word_place(&self, point: (f32, f32)) -> WordPlace {
721        self.hit_test(point.0, point.1)
722            .unwrap_or_else(|| self.end_word_place())
723    }
724
725    /// Move up one line from `place`, keeping the x-coordinate from `point`.
726    ///
727    /// Equivalent to upstream `GetUpWordPlace(place, point)`.
728    pub fn up_word_place(&self, place: &WordPlace, point: (f32, f32)) -> WordPlace {
729        if self.sections.is_empty() {
730            return *place;
731        }
732        let si = place.section.min(self.sections.len() - 1);
733        let section = &self.sections[si];
734        if section.lines.is_empty() {
735            return *place;
736        }
737        let li = place.line.min(section.lines.len() - 1);
738
739        let (target_si, target_li) = if li > 0 {
740            (si, li - 1)
741        } else if si > 0 {
742            let prev_si = si - 1;
743            let prev_lines = self.sections[prev_si].lines.len();
744            if prev_lines == 0 {
745                return *place;
746            }
747            (prev_si, prev_lines - 1)
748        } else {
749            return *place;
750        };
751
752        self.closest_word_on_line(target_si, target_li, point.0)
753    }
754
755    /// ADR-019 alias for `up_word_place()`.
756    #[inline]
757    pub fn get_up_word_place(&self, place: &WordPlace, point: (f32, f32)) -> WordPlace {
758        self.up_word_place(place, point)
759    }
760
761    /// Move down one line from `place`, keeping the x-coordinate from `point`.
762    ///
763    /// Equivalent to upstream `GetDownWordPlace(place, point)`.
764    pub fn down_word_place(&self, place: &WordPlace, point: (f32, f32)) -> WordPlace {
765        if self.sections.is_empty() {
766            return *place;
767        }
768        let si = place.section.min(self.sections.len() - 1);
769        let section = &self.sections[si];
770        if section.lines.is_empty() {
771            return *place;
772        }
773        let li = place.line.min(section.lines.len() - 1);
774
775        let (target_si, target_li) = if li + 1 < section.lines.len() {
776            (si, li + 1)
777        } else if si + 1 < self.sections.len() {
778            let next_si = si + 1;
779            if self.sections[next_si].lines.is_empty() {
780                return *place;
781            }
782            (next_si, 0)
783        } else {
784            return *place;
785        };
786
787        self.closest_word_on_line(target_si, target_li, point.0)
788    }
789
790    /// ADR-019 alias for `down_word_place()`.
791    #[inline]
792    pub fn get_down_word_place(&self, place: &WordPlace, point: (f32, f32)) -> WordPlace {
793        self.down_word_place(place, point)
794    }
795
796    /// First word of the line containing `place`.
797    ///
798    /// Equivalent to upstream `GetLineBeginPlace(place)`.
799    pub fn line_begin_place(&self, place: &WordPlace) -> WordPlace {
800        if self.sections.is_empty() {
801            return *place;
802        }
803        let si = place.section.min(self.sections.len() - 1);
804        let section = &self.sections[si];
805        if section.lines.is_empty() {
806            return WordPlace {
807                section: si,
808                line: 0,
809                word: 0,
810            };
811        }
812        let li = place.line.min(section.lines.len() - 1);
813        WordPlace {
814            section: si,
815            line: li,
816            word: 0,
817        }
818    }
819
820    /// ADR-019 alias for `line_begin_place()`.
821    #[inline]
822    pub fn get_line_begin_place(&self, place: &WordPlace) -> WordPlace {
823        self.line_begin_place(place)
824    }
825
826    /// Last word of the line containing `place`.
827    ///
828    /// Equivalent to upstream `GetLineEndPlace(place)`.
829    pub fn line_end_place(&self, place: &WordPlace) -> WordPlace {
830        if self.sections.is_empty() {
831            return *place;
832        }
833        let si = place.section.min(self.sections.len() - 1);
834        let section = &self.sections[si];
835        if section.lines.is_empty() {
836            return WordPlace {
837                section: si,
838                line: 0,
839                word: 0,
840            };
841        }
842        let li = place.line.min(section.lines.len() - 1);
843        let word_count = section.lines[li].word_count;
844        WordPlace {
845            section: si,
846            line: li,
847            word: word_count.saturating_sub(1),
848        }
849    }
850
851    /// ADR-019 alias for `line_end_place()`.
852    #[inline]
853    pub fn get_line_end_place(&self, place: &WordPlace) -> WordPlace {
854        self.line_end_place(place)
855    }
856
857    /// First word of the section containing `place`.
858    ///
859    /// Equivalent to upstream `GetSectionBeginPlace(place)`.
860    pub fn section_begin_place(&self, place: &WordPlace) -> WordPlace {
861        let si = if self.sections.is_empty() {
862            0
863        } else {
864            place.section.min(self.sections.len() - 1)
865        };
866        WordPlace {
867            section: si,
868            line: 0,
869            word: 0,
870        }
871    }
872
873    /// ADR-019 alias for `section_begin_place()`.
874    #[inline]
875    pub fn get_section_begin_place(&self, place: &WordPlace) -> WordPlace {
876        self.section_begin_place(place)
877    }
878
879    /// Last word of the section containing `place`.
880    ///
881    /// Equivalent to upstream `GetSectionEndPlace(place)`.
882    pub fn section_end_place(&self, place: &WordPlace) -> WordPlace {
883        if self.sections.is_empty() {
884            return WordPlace::default();
885        }
886        let si = place.section.min(self.sections.len() - 1);
887        self.section_end_place_for(si)
888    }
889
890    /// ADR-019 alias for `section_end_place()`.
891    #[inline]
892    pub fn get_section_end_place(&self, place: &WordPlace) -> WordPlace {
893        self.section_end_place(place)
894    }
895
896    /// Clamp `place` to the nearest valid position.
897    ///
898    /// Equivalent to upstream `UpdateWordPlace(place)`.
899    pub fn update_word_place(&self, place: &mut WordPlace) {
900        if self.sections.is_empty() {
901            *place = WordPlace::default();
902            return;
903        }
904        place.section = place.section.min(self.sections.len() - 1);
905        let section = &self.sections[place.section];
906        if section.lines.is_empty() {
907            place.line = 0;
908            place.word = 0;
909            return;
910        }
911        place.line = place.line.min(section.lines.len() - 1);
912        let word_count = section.lines[place.line].word_count;
913        if word_count == 0 {
914            place.word = 0;
915        } else {
916            place.word = place.word.min(word_count - 1);
917        }
918    }
919
920    /// Convert a `WordPlace` to a flat word index across all sections.
921    ///
922    /// This counts the newline separators between sections.
923    /// Equivalent to upstream `WordPlaceToWordIndex(place)`.
924    #[inline]
925    pub fn word_place_to_word_index(&self, place: &WordPlace) -> usize {
926        self.word_place_to_index(place)
927    }
928
929    /// Convert a flat word index to a `WordPlace`.
930    ///
931    /// Equivalent to upstream `WordIndexToWordPlace(index)`.
932    pub fn word_index_to_word_place(&self, index: usize) -> WordPlace {
933        let mut remaining = index;
934        for (si, section) in self.sections.iter().enumerate() {
935            let words_in_section = section.words.len();
936            if remaining < words_in_section {
937                for (li, line) in section.lines.iter().enumerate() {
938                    if remaining < line.word_start + line.word_count {
939                        let word = remaining.saturating_sub(line.word_start);
940                        return WordPlace {
941                            section: si,
942                            line: li,
943                            word,
944                        };
945                    }
946                }
947                return self.section_end_place_for(si);
948            }
949            remaining -= words_in_section;
950            if remaining == 0 && si + 1 < self.sections.len() {
951                return WordPlace {
952                    section: si + 1,
953                    line: 0,
954                    word: 0,
955                };
956            }
957            if si + 1 < self.sections.len() {
958                remaining = remaining.saturating_sub(1);
959            }
960        }
961        self.end_word_place()
962    }
963
964    // -----------------------------------------------------------------------
965    // Plate dimensions and coordinate conversion
966    // -----------------------------------------------------------------------
967
968    /// Width of the plate rectangle.
969    ///
970    /// Equivalent to upstream `GetPlateWidth()`.
971    pub fn plate_width(&self) -> f32 {
972        self.plate_rect[2] - self.plate_rect[0]
973    }
974
975    /// ADR-019 alias for `plate_width()`.
976    #[inline]
977    pub fn get_plate_width(&self) -> f32 {
978        self.plate_width()
979    }
980
981    /// Height of the plate rectangle.
982    ///
983    /// Equivalent to upstream `GetPlateHeight()`.
984    pub fn plate_height(&self) -> f32 {
985        self.plate_rect[3] - self.plate_rect[1]
986    }
987
988    /// ADR-019 alias for `plate_height()`.
989    #[inline]
990    pub fn get_plate_height(&self) -> f32 {
991        self.plate_height()
992    }
993
994    /// Begin text point — top-left corner of the content area in plate space.
995    ///
996    /// Equivalent to upstream `GetBTPoint()`.
997    pub fn begin_text_point(&self) -> (f32, f32) {
998        (self.plate_rect[0], self.plate_rect[3])
999    }
1000
1001    /// ADR-019 alias for `begin_text_point()`.
1002    #[inline]
1003    pub fn get_bt_point(&self) -> (f32, f32) {
1004        self.begin_text_point()
1005    }
1006
1007    /// End text point — bottom-right corner of the content area in plate space.
1008    ///
1009    /// Equivalent to upstream `GetETPoint()`.
1010    pub fn end_text_point(&self) -> (f32, f32) {
1011        (self.plate_rect[2], self.plate_rect[1])
1012    }
1013
1014    /// ADR-019 alias for `end_text_point()`.
1015    #[inline]
1016    pub fn get_et_point(&self) -> (f32, f32) {
1017        self.end_text_point()
1018    }
1019
1020    /// Convert internal (layout) coordinates to plate/output coordinates.
1021    ///
1022    /// Equivalent to upstream `InToOut(point)`.
1023    pub fn in_to_out(&self, point: (f32, f32)) -> (f32, f32) {
1024        (point.0 + self.plate_rect[0], self.plate_rect[3] - point.1)
1025    }
1026
1027    /// Convert plate coordinates to internal layout coordinates.
1028    ///
1029    /// Equivalent to upstream `OutToIn(point)`.
1030    pub fn out_to_in(&self, point: (f32, f32)) -> (f32, f32) {
1031        (point.0 - self.plate_rect[0], self.plate_rect[3] - point.1)
1032    }
1033
1034    // -----------------------------------------------------------------------
1035    // Configuration getters
1036    // -----------------------------------------------------------------------
1037
1038    /// Return the password substitution character (upstream `GetPasswordChar()`).
1039    pub fn password_char(&self) -> Option<char> {
1040        self.password_char
1041    }
1042
1043    /// ADR-019 alias for `password_char()`.
1044    #[inline]
1045    pub fn get_password_char(&self) -> Option<char> {
1046        self.password_char()
1047    }
1048
1049    /// Return the password substitution character (upstream `GetSubWord()` name).
1050    ///
1051    /// Deprecated: neither the idiomatic Rust primary nor the exact upstream alias.
1052    /// Use [`password_char()`](Self::password_char) (primary) or
1053    /// [`get_sub_word()`](Self::get_sub_word) (upstream-aligned alias) instead.
1054    #[deprecated(
1055        since = "0.1.0",
1056        note = "use password_char() or get_sub_word() instead"
1057    )]
1058    #[inline]
1059    pub fn sub_word(&self) -> Option<char> {
1060        self.password_char()
1061    }
1062
1063    /// ADR-019 alias for [`password_char()`](Self::password_char).
1064    ///
1065    /// Corresponds to `CPVT_VariableText::GetSubWord()` in PDFium.
1066    #[inline]
1067    pub fn get_sub_word(&self) -> Option<char> {
1068        self.password_char()
1069    }
1070
1071    /// Return the configured font size.
1072    #[inline]
1073    pub fn get_font_size(&self) -> f32 {
1074        self.font_size()
1075    }
1076
1077    /// Return the text alignment.
1078    pub fn alignment(&self) -> Alignment {
1079        self.alignment
1080    }
1081
1082    /// ADR-019 alias for `alignment()`.
1083    #[inline]
1084    pub fn get_alignment(&self) -> Alignment {
1085        self.alignment()
1086    }
1087
1088    /// Return whether multi-line mode is enabled.
1089    pub fn is_multi_line(&self) -> bool {
1090        self.multi_line
1091    }
1092
1093    /// Return whether automatic line return is enabled.
1094    pub fn is_auto_return(&self) -> bool {
1095        self.auto_return
1096    }
1097
1098    /// Return the character array cell count, if set.
1099    ///
1100    /// When `Some(n)`, the field is laid out as a fixed grid of `n` cells.
1101    /// Corresponds to upstream `CPDF_VariableText::GetCharArray()`.
1102    pub fn char_array(&self) -> Option<usize> {
1103        self.char_array
1104    }
1105
1106    /// Upstream-aligned alias for [`char_array()`](Self::char_array).
1107    ///
1108    /// Corresponds to `CPDF_VariableText::GetCharArray()` in PDFium.
1109    #[inline]
1110    pub fn get_char_array(&self) -> Option<usize> {
1111        self.char_array()
1112    }
1113
1114    /// Return the maximum character limit, if set.
1115    ///
1116    /// When `Some(n)`, input is capped at `n` characters.
1117    /// Corresponds to upstream `CPDF_VariableText::GetLimitChar()`.
1118    pub fn limit_char(&self) -> Option<usize> {
1119        self.limit_char
1120    }
1121
1122    /// Upstream-aligned alias for [`limit_char()`](Self::limit_char).
1123    ///
1124    /// Corresponds to `CPDF_VariableText::GetLimitChar()` in PDFium.
1125    #[inline]
1126    pub fn get_limit_char(&self) -> Option<usize> {
1127        self.limit_char()
1128    }
1129
1130    // -----------------------------------------------------------------------
1131    // Metrics helpers
1132    // -----------------------------------------------------------------------
1133
1134    /// Approximate line leading (line-to-line distance).
1135    ///
1136    /// Computed as `ascent - descent` using font index 0.
1137    /// Equivalent to upstream `GetLineLeading()`.
1138    pub fn line_leading(&self) -> f32 {
1139        let asc = self.provider.ascent(0, self.font_size);
1140        let desc = self.provider.descent(0, self.font_size);
1141        asc - desc
1142    }
1143
1144    /// ADR-019 alias for `line_leading()`.
1145    #[inline]
1146    pub fn get_line_leading(&self) -> f32 {
1147        self.line_leading()
1148    }
1149
1150    /// Font ascent at the current font size for font index 0.
1151    ///
1152    /// Equivalent to upstream `GetLineAscent()`.
1153    pub fn line_ascent(&self) -> f32 {
1154        self.provider.ascent(0, self.font_size)
1155    }
1156
1157    /// Font descent at the current font size for font index 0.
1158    ///
1159    /// Equivalent to upstream `GetLineDescent()`.
1160    pub fn line_descent(&self) -> f32 {
1161        self.provider.descent(0, self.font_size)
1162    }
1163
1164    /// Create a stateful iterator over this `VariableText`.
1165    ///
1166    /// Returns a `VtWordIterator` positioned at the beginning of the text.
1167    pub fn word_iterator(&self) -> VtWordIterator<'_, P> {
1168        VtWordIterator::new(self)
1169    }
1170
1171    // -----------------------------------------------------------------------
1172    // Private helpers
1173    // -----------------------------------------------------------------------
1174
1175    /// Return the `WordPlace` for the last character of section `si`.
1176    fn section_end_place_for(&self, si: usize) -> WordPlace {
1177        if si >= self.sections.len() {
1178            return self.end_word_place();
1179        }
1180        let section = &self.sections[si];
1181        if section.lines.is_empty() {
1182            return WordPlace {
1183                section: si,
1184                line: 0,
1185                word: 0,
1186            };
1187        }
1188        let li = section.lines.len() - 1;
1189        let word_count = section.lines[li].word_count;
1190        WordPlace {
1191            section: si,
1192            line: li,
1193            word: word_count.saturating_sub(1),
1194        }
1195    }
1196
1197    /// Find the closest word to x-coordinate `x` on line `li` of section `si`.
1198    fn closest_word_on_line(&self, si: usize, li: usize, x: f32) -> WordPlace {
1199        let section = &self.sections[si];
1200        let line = &section.lines[li];
1201        if line.word_count == 0 {
1202            return WordPlace {
1203                section: si,
1204                line: li,
1205                word: 0,
1206            };
1207        }
1208        let rel_x = x - self.plate_rect[0] - line.x;
1209        let end = line.word_start + line.word_count;
1210        for wi in line.word_start..end {
1211            let word = &section.words[wi];
1212            if rel_x <= word.position_x + word.width / 2.0 {
1213                return WordPlace {
1214                    section: si,
1215                    line: li,
1216                    word: wi - line.word_start,
1217                };
1218            }
1219        }
1220        WordPlace {
1221            section: si,
1222            line: li,
1223            word: line.word_count - 1,
1224        }
1225    }
1226
1227    /// Find the best font size that fits all text within the plate rect.
1228    fn find_auto_font_size(&self) -> f32 {
1229        let plate_width = self.plate_rect[2] - self.plate_rect[0];
1230        let plate_height = self.plate_rect[3] - self.plate_rect[1];
1231
1232        if plate_width <= 0.0 || plate_height <= 0.0 {
1233            return 1.0;
1234        }
1235
1236        let total_chars: usize = self.sections.iter().map(|s| s.words.len()).sum();
1237        if total_chars == 0 {
1238            return self.font_size;
1239        }
1240
1241        // Linear scan through ascending sizes — keep the largest that fits
1242        let mut best = AUTO_FONT_SIZES[0];
1243
1244        for &size in AUTO_FONT_SIZES {
1245            if self.text_fits_at_size(size, plate_width, plate_height) {
1246                best = size;
1247            } else {
1248                break;
1249            }
1250        }
1251
1252        best
1253    }
1254
1255    /// Check if the current text fits in the plate at a given font size.
1256    fn text_fits_at_size(&self, size: f32, plate_width: f32, plate_height: f32) -> bool {
1257        let ascent = self.provider.ascent(0, size);
1258        let descent = self.provider.descent(0, size);
1259        let line_height = ascent - descent;
1260
1261        if line_height <= 0.0 {
1262            return false;
1263        }
1264
1265        let mut total_lines = 0_usize;
1266
1267        for section in &self.sections {
1268            if section.words.is_empty() {
1269                total_lines += 1;
1270                continue;
1271            }
1272
1273            let mut line_width = 0.0_f32;
1274            let mut lines_in_section = 1_usize;
1275
1276            for word in &section.words {
1277                let w = self
1278                    .provider
1279                    .char_width(word.font_index, word.character as u16, size);
1280                if self.multi_line
1281                    && self.auto_return
1282                    && line_width + w > plate_width
1283                    && line_width > 0.0
1284                {
1285                    lines_in_section += 1;
1286                    line_width = w;
1287                } else {
1288                    line_width += w;
1289                }
1290
1291                // Single-line: check total width
1292                if !self.multi_line && line_width > plate_width {
1293                    return false;
1294                }
1295            }
1296
1297            total_lines += lines_in_section;
1298        }
1299
1300        let total_height = total_lines as f32 * line_height;
1301        total_height <= plate_height
1302    }
1303}
1304
1305// ---------------------------------------------------------------------------
1306// VtWordIterator — stateful position-based iterator
1307// ---------------------------------------------------------------------------
1308
1309/// Stateful iterator over character positions in a `VariableText`.
1310///
1311/// Corresponds to the inner `Iterator` class in upstream `CPVT_VariableText`.
1312/// Unlike `VtIterator` (which is a Rust `Iterator` over `WordInfo` values),
1313/// `VtWordIterator` mirrors the upstream class with explicit `next_word()` /
1314/// `next_line()` step methods and a `word_place()` position query.
1315pub struct VtWordIterator<'a, P: VtFontProvider> {
1316    vt: &'a VariableText<P>,
1317    current: WordPlace,
1318}
1319
1320impl<'a, P: VtFontProvider> VtWordIterator<'a, P> {
1321    /// Create a new iterator positioned at the beginning.
1322    pub fn new(vt: &'a VariableText<P>) -> Self {
1323        Self {
1324            vt,
1325            current: vt.begin_word_place(),
1326        }
1327    }
1328
1329    /// Move the iterator to the position described by a flat word index.
1330    ///
1331    /// Equivalent to upstream `Iterator::SetAt(int32_t index)`.
1332    pub fn set_at_index(&mut self, word_index: i32) {
1333        let idx = if word_index < 0 {
1334            0
1335        } else {
1336            word_index as usize
1337        };
1338        self.current = self.vt.word_index_to_word_place(idx);
1339    }
1340
1341    /// Move the iterator to an explicit place.
1342    ///
1343    /// Equivalent to upstream `Iterator::SetAt(const CPVT_WordPlace& place)`.
1344    pub fn set_at_place(&mut self, place: WordPlace) {
1345        self.current = place;
1346    }
1347
1348    /// Advance one character forward.
1349    ///
1350    /// Returns `true` if the iterator moved; `false` if already at the end.
1351    /// Equivalent to upstream `Iterator::NextWord()`.
1352    pub fn next_word(&mut self) -> bool {
1353        let next = self.vt.next_word_place(&self.current);
1354        if next == self.current {
1355            return false;
1356        }
1357        self.current = next;
1358        true
1359    }
1360
1361    /// Advance to the start of the next line.
1362    ///
1363    /// Returns `true` if the iterator moved; `false` if already on the last line.
1364    /// Equivalent to upstream `Iterator::NextLine()`.
1365    pub fn next_line(&mut self) -> bool {
1366        if self.vt.sections.is_empty() {
1367            return false;
1368        }
1369        let si = self.current.section.min(self.vt.sections.len() - 1);
1370        let section = &self.vt.sections[si];
1371        if section.lines.is_empty() {
1372            return false;
1373        }
1374        let li = self.current.line.min(section.lines.len() - 1);
1375        if li + 1 < section.lines.len() {
1376            self.current = WordPlace {
1377                section: si,
1378                line: li + 1,
1379                word: 0,
1380            };
1381            return true;
1382        }
1383        if si + 1 < self.vt.sections.len() {
1384            self.current = WordPlace {
1385                section: si + 1,
1386                line: 0,
1387                word: 0,
1388            };
1389            return true;
1390        }
1391        false
1392    }
1393
1394    /// Return the current iterator position.
1395    ///
1396    /// Equivalent to upstream `Iterator::GetWordPlace()`.
1397    pub fn word_place(&self) -> &WordPlace {
1398        &self.current
1399    }
1400
1401    /// ADR-019 alias for `word_place()`.
1402    #[inline]
1403    pub fn get_word_place(&self) -> &WordPlace {
1404        self.word_place()
1405    }
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410    use super::*;
1411
1412    /// Simple fixed-width font provider for testing.
1413    struct FixedWidthProvider {
1414        char_width: f32,
1415        ascent: f32,
1416        descent: f32,
1417    }
1418
1419    impl FixedWidthProvider {
1420        fn new(char_width: f32) -> Self {
1421            Self {
1422                char_width,
1423                ascent: 10.0,
1424                descent: -3.0,
1425            }
1426        }
1427    }
1428
1429    impl VtFontProvider for FixedWidthProvider {
1430        fn char_width(&self, _font_index: usize, _word: u16, font_size: f32) -> f32 {
1431            self.char_width * (font_size / 12.0)
1432        }
1433        fn ascent(&self, _font_index: usize, font_size: f32) -> f32 {
1434            self.ascent * (font_size / 12.0)
1435        }
1436        fn descent(&self, _font_index: usize, font_size: f32) -> f32 {
1437            self.descent * (font_size / 12.0)
1438        }
1439    }
1440
1441    fn make_vt(provider: FixedWidthProvider) -> VariableText<FixedWidthProvider> {
1442        let mut vt = VariableText::new(provider);
1443        vt.set_plate_rect([0.0, 0.0, 200.0, 100.0]);
1444        vt.set_font_size(12.0);
1445        vt
1446    }
1447
1448    #[test]
1449    fn test_empty_text() {
1450        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1451        vt.set_text("");
1452        vt.rearrange();
1453        assert_eq!(vt.sections().len(), 1);
1454        assert_eq!(vt.sections()[0].words.len(), 0);
1455        assert_eq!(vt.sections()[0].lines.len(), 1);
1456    }
1457
1458    #[test]
1459    fn test_single_line_text() {
1460        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1461        vt.set_text("Hello");
1462        vt.rearrange();
1463        assert_eq!(vt.sections().len(), 1);
1464        assert_eq!(vt.sections()[0].words.len(), 5);
1465        assert_eq!(vt.sections()[0].lines.len(), 1);
1466        assert_eq!(vt.sections()[0].lines[0].word_count, 5);
1467    }
1468
1469    #[test]
1470    fn test_multi_section_text() {
1471        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1472        vt.set_text("Line1\nLine2\nLine3");
1473        vt.rearrange();
1474        assert_eq!(vt.sections().len(), 3);
1475        assert_eq!(vt.sections()[0].words.len(), 5);
1476        assert_eq!(vt.sections()[1].words.len(), 5);
1477        assert_eq!(vt.sections()[2].words.len(), 5);
1478    }
1479
1480    #[test]
1481    fn test_line_breaking() {
1482        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1483        vt.set_multi_line(true);
1484        vt.set_auto_return(true);
1485        // Plate width = 200, char width = 6, so ~33 chars per line
1486        // 50 chars should break into 2 lines
1487        let text: String = "A".repeat(50);
1488        vt.set_text(&text);
1489        vt.rearrange();
1490        assert_eq!(vt.sections()[0].lines.len(), 2);
1491    }
1492
1493    #[test]
1494    fn test_no_line_breaking_single_line_mode() {
1495        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1496        vt.set_multi_line(false);
1497        let text: String = "A".repeat(50);
1498        vt.set_text(&text);
1499        vt.rearrange();
1500        assert_eq!(vt.sections()[0].lines.len(), 1);
1501        assert_eq!(vt.sections()[0].lines[0].word_count, 50);
1502    }
1503
1504    #[test]
1505    fn test_alignment_left() {
1506        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1507        vt.set_alignment(Alignment::Left);
1508        vt.set_text("Hi");
1509        vt.rearrange();
1510        assert!((vt.sections()[0].lines[0].x - 0.0).abs() < 0.01);
1511    }
1512
1513    #[test]
1514    fn test_alignment_center() {
1515        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1516        vt.set_alignment(Alignment::Center);
1517        vt.set_text("Hi");
1518        vt.rearrange();
1519        let line = &vt.sections()[0].lines[0];
1520        // 2 chars * 6.0 = 12.0, plate_width = 200.0, offset = (200-12)/2 = 94.0
1521        assert!((line.x - 94.0).abs() < 0.01);
1522    }
1523
1524    #[test]
1525    fn test_alignment_right() {
1526        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1527        vt.set_alignment(Alignment::Right);
1528        vt.set_text("Hi");
1529        vt.rearrange();
1530        let line = &vt.sections()[0].lines[0];
1531        // offset = 200 - 12 = 188
1532        assert!((line.x - 188.0).abs() < 0.01);
1533    }
1534
1535    #[test]
1536    fn test_alignment_from_value() {
1537        assert_eq!(Alignment::from_value(0), Alignment::Left);
1538        assert_eq!(Alignment::from_value(1), Alignment::Center);
1539        assert_eq!(Alignment::from_value(2), Alignment::Right);
1540        assert_eq!(Alignment::from_value(99), Alignment::Left);
1541    }
1542
1543    #[test]
1544    fn test_char_array_mode() {
1545        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1546        vt.set_char_array(Some(10));
1547        vt.set_text("ABC");
1548        vt.rearrange();
1549        let section = &vt.sections()[0];
1550        assert_eq!(section.lines.len(), 1);
1551        assert_eq!(section.lines[0].word_count, 3);
1552        // Cell width = 200/10 = 20, first char centered in cell
1553        let first_word = &section.words[0];
1554        let expected_x = (20.0 - 6.0) / 2.0; // centered in 20pt cell
1555        assert!((first_word.position_x - expected_x).abs() < 0.01);
1556    }
1557
1558    #[test]
1559    fn test_char_array_limits_to_count() {
1560        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1561        vt.set_char_array(Some(3));
1562        vt.set_text("ABCDE");
1563        vt.rearrange();
1564        assert_eq!(vt.sections()[0].lines[0].word_count, 3);
1565    }
1566
1567    #[test]
1568    fn test_password_masking() {
1569        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1570        vt.set_password_char(Some('*'));
1571        vt.set_text("secret");
1572        vt.rearrange();
1573        let section = &vt.sections()[0];
1574        assert_eq!(section.words.len(), 6);
1575        for word in &section.words {
1576            assert_eq!(word.character, '*');
1577        }
1578    }
1579
1580    #[test]
1581    fn test_limit_char() {
1582        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1583        vt.set_limit_char(Some(5));
1584        vt.set_text("Hello World");
1585        vt.rearrange();
1586        assert_eq!(vt.sections()[0].words.len(), 5);
1587    }
1588
1589    #[test]
1590    fn test_auto_font_size() {
1591        let mut vt = VariableText::new(FixedWidthProvider::new(6.0));
1592        vt.set_plate_rect([0.0, 0.0, 100.0, 20.0]);
1593        vt.set_auto_font_size(true);
1594        vt.set_text("Hello World This Is Long");
1595        vt.rearrange();
1596        // Auto-size should have found a size that fits
1597        let content_height = vt.content_rect()[3] - vt.content_rect()[1];
1598        assert!(content_height <= 20.0 + 1.0); // small tolerance
1599    }
1600
1601    #[test]
1602    fn test_auto_font_size_empty() {
1603        let mut vt = VariableText::new(FixedWidthProvider::new(6.0));
1604        vt.set_plate_rect([0.0, 0.0, 100.0, 20.0]);
1605        vt.set_font_size(12.0);
1606        vt.set_auto_font_size(true);
1607        vt.set_text("");
1608        vt.rearrange();
1609        assert_eq!(vt.font_size(), 12.0);
1610    }
1611
1612    #[test]
1613    fn test_hit_test_first_char() {
1614        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1615        vt.set_text("Hello");
1616        vt.rearrange();
1617        let line = &vt.sections()[0].lines[0];
1618        let y = line.y + line.ascent / 2.0;
1619        let place = vt.hit_test(1.0, y);
1620        assert!(place.is_some());
1621        assert_eq!(place.unwrap().word, 0);
1622    }
1623
1624    #[test]
1625    fn test_hit_test_past_end() {
1626        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1627        vt.set_text("Hi");
1628        vt.rearrange();
1629        let line = &vt.sections()[0].lines[0];
1630        let y = line.y + line.ascent / 2.0;
1631        let place = vt.hit_test(100.0, y);
1632        assert!(place.is_some());
1633        assert_eq!(place.unwrap().word, 1); // last char
1634    }
1635
1636    #[test]
1637    fn test_word_place_to_index_first() {
1638        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1639        vt.set_text("Hello");
1640        vt.rearrange();
1641        let idx = vt.word_place_to_index(&WordPlace {
1642            section: 0,
1643            line: 0,
1644            word: 0,
1645        });
1646        assert_eq!(idx, 0);
1647    }
1648
1649    #[test]
1650    fn test_word_place_to_index_second_section() {
1651        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1652        vt.set_text("AB\nCD");
1653        vt.rearrange();
1654        let idx = vt.word_place_to_index(&WordPlace {
1655            section: 1,
1656            line: 0,
1657            word: 0,
1658        });
1659        assert_eq!(idx, 3); // 2 chars + 1 newline separator
1660    }
1661
1662    #[test]
1663    fn test_word_place_to_index_mid() {
1664        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1665        vt.set_text("Hello");
1666        vt.rearrange();
1667        let idx = vt.word_place_to_index(&WordPlace {
1668            section: 0,
1669            line: 0,
1670            word: 3,
1671        });
1672        assert_eq!(idx, 3);
1673    }
1674
1675    #[test]
1676    fn test_content_rect_non_empty() {
1677        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1678        vt.set_text("Hello");
1679        vt.rearrange();
1680        let cr = vt.content_rect();
1681        assert!(cr[3] > cr[1]); // top > bottom
1682    }
1683
1684    #[test]
1685    fn test_multi_line_many_breaks() {
1686        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1687        vt.set_multi_line(true);
1688        vt.set_auto_return(true);
1689        // Plate is 200 wide, char is 6, so 33 chars per line
1690        // 100 chars → ceil(100/33) = 4 lines
1691        let text: String = "X".repeat(100);
1692        vt.set_text(&text);
1693        vt.rearrange();
1694        let line_count = vt.sections()[0].lines.len();
1695        assert!(line_count >= 3);
1696        assert!(line_count <= 4);
1697    }
1698
1699    #[test]
1700    fn test_word_info_positions_increase() {
1701        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1702        vt.set_text("ABCDE");
1703        vt.rearrange();
1704        let words = &vt.sections()[0].words;
1705        for i in 1..words.len() {
1706            assert!(words[i].position_x > words[i - 1].position_x);
1707        }
1708    }
1709
1710    #[test]
1711    fn test_cjk_characters_layout() {
1712        // CJK characters are wider — use a wider char width
1713        let mut vt = VariableText::new(FixedWidthProvider::new(12.0));
1714        vt.set_plate_rect([0.0, 0.0, 100.0, 100.0]);
1715        vt.set_font_size(12.0);
1716        vt.set_multi_line(true);
1717        vt.set_auto_return(true);
1718        // 100 / 12 = ~8 chars per line, 15 chars → 2 lines
1719        let text: String = "\u{4E00}".repeat(15);
1720        vt.set_text(&text);
1721        vt.rearrange();
1722        assert!(vt.sections()[0].lines.len() >= 2);
1723    }
1724
1725    #[test]
1726    fn test_multi_line_auto_font_size() {
1727        let mut vt = VariableText::new(FixedWidthProvider::new(6.0));
1728        vt.set_plate_rect([0.0, 0.0, 50.0, 30.0]);
1729        vt.set_multi_line(true);
1730        vt.set_auto_return(true);
1731        vt.set_auto_font_size(true);
1732        vt.set_text("This is a fairly long text that needs to fit");
1733        vt.rearrange();
1734        assert!(vt.font_size() < 12.0); // should have reduced
1735        assert!(vt.font_size() >= 4.0); // but not below minimum
1736    }
1737
1738    #[test]
1739    fn test_line_y_positions_decrease() {
1740        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1741        vt.set_text("Line1\nLine2\nLine3");
1742        vt.rearrange();
1743        let y0 = vt.sections()[0].lines[0].y;
1744        let y1 = vt.sections()[1].lines[0].y;
1745        let y2 = vt.sections()[2].lines[0].y;
1746        assert!(y0 > y1);
1747        assert!(y1 > y2);
1748    }
1749
1750    #[test]
1751    fn test_hit_test_below_all_lines() {
1752        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1753        vt.set_text("Hello");
1754        vt.rearrange();
1755        // Very low y value
1756        let place = vt.hit_test(5.0, -100.0);
1757        assert!(place.is_some());
1758    }
1759
1760    #[test]
1761    fn test_word_range_creation() {
1762        let range = WordRange {
1763            begin: WordPlace {
1764                section: 0,
1765                line: 0,
1766                word: 0,
1767            },
1768            end: WordPlace {
1769                section: 0,
1770                line: 0,
1771                word: 5,
1772            },
1773        };
1774        assert_eq!(range.begin.word, 0);
1775        assert_eq!(range.end.word, 5);
1776    }
1777
1778    #[test]
1779    fn test_cr_line_endings() {
1780        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1781        vt.set_text("Line1\rLine2\rLine3");
1782        vt.rearrange();
1783        assert_eq!(vt.sections().len(), 3);
1784        assert_eq!(vt.sections()[0].words.len(), 5);
1785        assert_eq!(vt.sections()[1].words.len(), 5);
1786        assert_eq!(vt.sections()[2].words.len(), 5);
1787    }
1788
1789    #[test]
1790    fn test_crlf_line_endings() {
1791        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1792        vt.set_text("Line1\r\nLine2\r\nLine3");
1793        vt.rearrange();
1794        assert_eq!(vt.sections().len(), 3);
1795        assert_eq!(vt.sections()[0].words.len(), 5);
1796        assert_eq!(vt.sections()[1].words.len(), 5);
1797        assert_eq!(vt.sections()[2].words.len(), 5);
1798    }
1799
1800    #[test]
1801    fn test_mixed_line_endings() {
1802        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1803        vt.set_text("A\r\nB\rC\nD");
1804        vt.rearrange();
1805        assert_eq!(vt.sections().len(), 4);
1806        assert_eq!(vt.sections()[0].words.len(), 1); // A
1807        assert_eq!(vt.sections()[1].words.len(), 1); // B
1808        assert_eq!(vt.sections()[2].words.len(), 1); // C
1809        assert_eq!(vt.sections()[3].words.len(), 1); // D
1810    }
1811
1812    #[test]
1813    fn test_insert_adds_chars_and_rearranges() {
1814        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1815        vt.set_text("Hello");
1816        vt.rearrange();
1817        let place = WordPlace {
1818            section: 0,
1819            line: 0,
1820            word: 2,
1821        };
1822        vt.insert(&place, "XY").unwrap();
1823        // "He" + "XY" + "llo" = 7 chars
1824        assert_eq!(vt.sections()[0].words.len(), 7);
1825        assert_eq!(vt.sections()[0].words[2].character, 'X');
1826        assert_eq!(vt.sections()[0].words[3].character, 'Y');
1827    }
1828
1829    #[test]
1830    fn test_delete_removes_chars_and_rearranges() {
1831        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1832        vt.set_text("Hello");
1833        vt.rearrange();
1834        let range = WordRange {
1835            begin: WordPlace {
1836                section: 0,
1837                line: 0,
1838                word: 0,
1839            },
1840            end: WordPlace {
1841                section: 0,
1842                line: 0,
1843                word: 3,
1844            },
1845        };
1846        vt.delete(&range).unwrap();
1847        // "Hello" with indices 0..3 removed → "lo"
1848        assert_eq!(vt.sections()[0].words.len(), 2);
1849        assert_eq!(vt.sections()[0].words[0].character, 'l');
1850        assert_eq!(vt.sections()[0].words[1].character, 'o');
1851    }
1852
1853    #[test]
1854    fn test_iter_lines_count() {
1855        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1856        vt.set_text("Line1\nLine2\nLine3");
1857        vt.rearrange();
1858        assert_eq!(vt.iter_lines().count(), 3);
1859        assert_eq!(vt.total_lines(), 3);
1860    }
1861
1862    #[test]
1863    fn test_iter_words_count() {
1864        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1865        vt.set_text("Hello");
1866        vt.rearrange();
1867        assert_eq!(vt.iter_words().count(), 5);
1868        assert_eq!(vt.total_words(), 5);
1869    }
1870
1871    #[test]
1872    fn test_iter_words_across_sections() {
1873        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1874        vt.set_text("AB\nCD");
1875        vt.rearrange();
1876        assert_eq!(vt.iter_words().count(), 4);
1877        assert_eq!(vt.total_words(), 4);
1878    }
1879
1880    // -----------------------------------------------------------------------
1881    // Navigation / query method tests
1882    // -----------------------------------------------------------------------
1883
1884    #[test]
1885    fn test_total_words_empty() {
1886        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1887        vt.set_text("");
1888        vt.rearrange();
1889        assert_eq!(vt.total_words(), 0);
1890        assert_eq!(vt.get_total_words(), 0);
1891    }
1892
1893    #[test]
1894    fn test_total_words_single() {
1895        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1896        vt.set_text("A");
1897        vt.rearrange();
1898        assert_eq!(vt.total_words(), 1);
1899    }
1900
1901    #[test]
1902    fn test_total_words_multi() {
1903        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1904        vt.set_text("Hello\nWorld");
1905        vt.rearrange();
1906        assert_eq!(vt.total_words(), 10);
1907    }
1908
1909    #[test]
1910    fn test_begin_and_end_word_place_empty() {
1911        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1912        vt.set_text("");
1913        vt.rearrange();
1914        let begin = vt.begin_word_place();
1915        assert_eq!(begin, WordPlace::default());
1916        let end = vt.end_word_place();
1917        assert_eq!(end.section, 0);
1918    }
1919
1920    #[test]
1921    fn test_begin_and_end_word_place_non_empty() {
1922        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1923        vt.set_text("Hello");
1924        vt.rearrange();
1925        let begin = vt.begin_word_place();
1926        assert_eq!(
1927            begin,
1928            WordPlace {
1929                section: 0,
1930                line: 0,
1931                word: 0
1932            }
1933        );
1934        let end = vt.end_word_place();
1935        assert_eq!(end.section, 0);
1936        assert_eq!(end.word, 4);
1937    }
1938
1939    #[test]
1940    fn test_word_place_to_word_index_round_trip() {
1941        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1942        vt.set_text("ABCDE");
1943        vt.rearrange();
1944        for i in 0..5 {
1945            let place = WordPlace {
1946                section: 0,
1947                line: 0,
1948                word: i,
1949            };
1950            let idx = vt.word_place_to_word_index(&place);
1951            assert_eq!(idx, i, "index mismatch at word {i}");
1952            let back = vt.word_index_to_word_place(idx);
1953            assert_eq!(back, place, "round-trip mismatch at word {i}");
1954        }
1955    }
1956
1957    #[test]
1958    fn test_word_index_to_place_across_sections() {
1959        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1960        vt.set_text("AB\nCD");
1961        vt.rearrange();
1962        let p0 = vt.word_index_to_word_place(0);
1963        assert_eq!(
1964            p0,
1965            WordPlace {
1966                section: 0,
1967                line: 0,
1968                word: 0
1969            }
1970        );
1971        let p1 = vt.word_index_to_word_place(1);
1972        assert_eq!(
1973            p1,
1974            WordPlace {
1975                section: 0,
1976                line: 0,
1977                word: 1
1978            }
1979        );
1980        let p3 = vt.word_index_to_word_place(3);
1981        assert_eq!(p3.section, 1);
1982        assert_eq!(p3.word, 0);
1983    }
1984
1985    #[test]
1986    fn test_prev_next_word_place_navigation() {
1987        let mut vt = make_vt(FixedWidthProvider::new(6.0));
1988        vt.set_text("ABCDE");
1989        vt.rearrange();
1990
1991        let mut place = vt.begin_word_place();
1992        let mut count = 0;
1993        loop {
1994            count += 1;
1995            let next = vt.next_word_place(&place);
1996            if next == place {
1997                break;
1998            }
1999            place = next;
2000        }
2001        assert_eq!(count, 5);
2002
2003        place = vt.end_word_place();
2004        count = 0;
2005        loop {
2006            count += 1;
2007            let prev = vt.prev_word_place(&place);
2008            if prev == place {
2009                break;
2010            }
2011            place = prev;
2012        }
2013        assert_eq!(count, 5);
2014    }
2015
2016    #[test]
2017    fn test_line_begin_and_end_place() {
2018        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2019        vt.set_text("Hello");
2020        vt.rearrange();
2021        let mid = WordPlace {
2022            section: 0,
2023            line: 0,
2024            word: 2,
2025        };
2026        let begin = vt.line_begin_place(&mid);
2027        assert_eq!(begin.word, 0);
2028        let end = vt.line_end_place(&mid);
2029        assert_eq!(end.word, 4);
2030    }
2031
2032    #[test]
2033    fn test_section_begin_and_end_place() {
2034        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2035        vt.set_text("Hello\nWorld");
2036        vt.rearrange();
2037        let mid_sec1 = WordPlace {
2038            section: 1,
2039            line: 0,
2040            word: 2,
2041        };
2042        let sec_begin = vt.section_begin_place(&mid_sec1);
2043        assert_eq!(
2044            sec_begin,
2045            WordPlace {
2046                section: 1,
2047                line: 0,
2048                word: 0
2049            }
2050        );
2051        let sec_end = vt.section_end_place(&mid_sec1);
2052        assert_eq!(sec_end.section, 1);
2053        assert_eq!(sec_end.word, 4);
2054    }
2055
2056    #[test]
2057    fn test_plate_width_and_height() {
2058        let vt = make_vt(FixedWidthProvider::new(6.0));
2059        assert!((vt.plate_width() - 200.0).abs() < 0.01);
2060        assert!((vt.plate_height() - 100.0).abs() < 0.01);
2061        assert!((vt.get_plate_width() - 200.0).abs() < 0.01);
2062        assert!((vt.get_plate_height() - 100.0).abs() < 0.01);
2063    }
2064
2065    #[test]
2066    fn test_in_to_out_and_out_to_in_inverse() {
2067        let vt = make_vt(FixedWidthProvider::new(6.0));
2068        let original = (50.0_f32, 30.0_f32);
2069        let out = vt.in_to_out(original);
2070        let back = vt.out_to_in(out);
2071        assert!((back.0 - original.0).abs() < 0.001, "x mismatch");
2072        assert!((back.1 - original.1).abs() < 0.001, "y mismatch");
2073    }
2074
2075    #[test]
2076    fn test_vt_word_iterator_next_word_advances() {
2077        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2078        vt.set_text("ABCDE");
2079        vt.rearrange();
2080        let mut it = vt.word_iterator();
2081        let mut count = 0;
2082        while it.next_word() {
2083            count += 1;
2084        }
2085        assert_eq!(count, 4);
2086    }
2087
2088    #[test]
2089    fn test_vt_word_iterator_set_at_index() {
2090        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2091        vt.set_text("ABCDE");
2092        vt.rearrange();
2093        let mut it = vt.word_iterator();
2094        it.set_at_index(3);
2095        assert_eq!(it.word_place().word, 3);
2096    }
2097
2098    #[test]
2099    fn test_vt_word_iterator_set_at_place() {
2100        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2101        vt.set_text("Hello");
2102        vt.rearrange();
2103        let target = WordPlace {
2104            section: 0,
2105            line: 0,
2106            word: 2,
2107        };
2108        let mut it = vt.word_iterator();
2109        it.set_at_place(target);
2110        assert_eq!(*it.get_word_place(), target);
2111    }
2112
2113    #[test]
2114    fn test_config_getters() {
2115        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2116        vt.set_text("Test");
2117        vt.rearrange();
2118        assert!((vt.font_size() - 12.0).abs() < 0.01);
2119        assert!((vt.get_font_size() - 12.0).abs() < 0.01);
2120        assert_eq!(vt.alignment(), Alignment::Left);
2121        assert_eq!(vt.get_alignment(), Alignment::Left);
2122        assert!(!vt.is_multi_line());
2123        assert!(vt.is_auto_return());
2124    }
2125
2126    #[test]
2127    fn test_password_char_getter() {
2128        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2129        assert_eq!(vt.password_char(), None);
2130        assert_eq!(vt.get_password_char(), None);
2131        assert_eq!(vt.get_sub_word(), None);
2132        vt.set_password_char(Some('*'));
2133        assert_eq!(vt.password_char(), Some('*'));
2134        assert_eq!(vt.get_sub_word(), Some('*'));
2135        assert_eq!(vt.get_password_char(), Some('*'));
2136    }
2137
2138    #[test]
2139    fn test_line_leading_positive() {
2140        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2141        vt.set_text("Hi");
2142        vt.rearrange();
2143        // ascent=10, descent=-3, leading=13
2144        assert!((vt.line_leading() - 13.0).abs() < 0.01);
2145        assert!((vt.get_line_leading() - 13.0).abs() < 0.01);
2146    }
2147
2148    #[test]
2149    fn test_update_word_place_clamping() {
2150        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2151        vt.set_text("Hi");
2152        vt.rearrange();
2153        let mut out_of_bounds = WordPlace {
2154            section: 99,
2155            line: 99,
2156            word: 99,
2157        };
2158        vt.update_word_place(&mut out_of_bounds);
2159        assert_eq!(out_of_bounds.section, 0);
2160        assert_eq!(out_of_bounds.line, 0);
2161        assert!(out_of_bounds.word <= 1);
2162    }
2163
2164    #[test]
2165    fn test_search_word_place_returns_valid() {
2166        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2167        vt.set_text("Hello");
2168        vt.rearrange();
2169        let line = &vt.sections()[0].lines[0];
2170        let y = line.y + line.ascent / 2.0;
2171        let place = vt.search_word_place((10.0, y));
2172        assert_eq!(place.section, 0);
2173    }
2174
2175    #[test]
2176    fn test_vt_word_iterator_next_line_across_sections() {
2177        let mut vt = make_vt(FixedWidthProvider::new(6.0));
2178        vt.set_text("Hello\nWorld");
2179        vt.rearrange();
2180        let mut it = vt.word_iterator();
2181        assert_eq!(it.word_place().section, 0);
2182        assert!(it.next_line());
2183        assert_eq!(it.word_place().section, 1);
2184        assert_eq!(it.word_place().line, 0);
2185        assert!(!it.next_line());
2186    }
2187}