1use crate::decoration::{DecorationSegment, DecorationStyle, TextDecoration};
8use crate::GlyphPosition;
9
10#[derive(Debug, Clone, Default)]
17pub struct Preedit {
18 pub text: String,
20 pub cursor_range: Option<(usize, usize)>,
23}
24
25impl Preedit {
26 pub fn new(text: impl Into<String>, cursor_range: Option<(usize, usize)>) -> Self {
28 Self {
29 text: text.into(),
30 cursor_range,
31 }
32 }
33
34 pub fn is_empty(&self) -> bool {
36 self.text.is_empty()
37 }
38
39 pub fn underline_segments(
47 &self,
48 x_start: f32,
49 y_baseline: f32,
50 char_width: f32,
51 ) -> Vec<DecorationSegment> {
52 if self.text.is_empty() {
53 return Vec::new();
54 }
55
56 let char_count = self.text.chars().count();
57 let total_width = char_count as f32 * char_width;
58
59 let line_height = char_width * 1.4;
63 let ascent = char_width * 1.2;
64 let glyph = GlyphPosition {
65 byte_offset: 0,
66 x: x_start,
67 y: y_baseline - ascent,
68 width: total_width,
69 height: line_height,
70 };
71 let glyph_slice = [glyph];
72
73 let decoration = TextDecoration {
74 underline: Some(DecorationStyle::Solid),
75 overline: None,
76 strikethrough: None,
77 color: [0, 0, 0, 255],
78 thickness: 1.0,
79 };
80
81 decoration.line_segments(&glyph_slice, y_baseline, line_height)
82 }
83
84 pub fn composition_window_rect(
90 &self,
91 caret_x: f32,
92 caret_y: f32,
93 char_width: f32,
94 line_height: f32,
95 ) -> (f32, f32, f32, f32) {
96 let width = (self.text.chars().count() as f32 * char_width).max(1.0);
97 (caret_x, caret_y, width, line_height)
98 }
99}
100
101#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn preedit_empty_no_segments() {
109 let p = Preedit::new("", None);
110 assert!(p.is_empty());
111 let segs = p.underline_segments(0.0, 12.0, 8.0);
112 assert!(segs.is_empty(), "empty preedit must yield no segments");
113 }
114
115 #[test]
116 fn preedit_underline_segments_non_empty() {
117 let p = Preedit::new("こんにちは", None);
118 assert!(!p.is_empty());
119 let segs = p.underline_segments(0.0, 16.0, 10.0);
120 assert!(
121 !segs.is_empty(),
122 "non-empty preedit must produce at least one underline segment"
123 );
124 }
125
126 #[test]
127 fn preedit_underline_segment_covers_width() {
128 let p = Preedit::new("abc", None);
129 let char_w = 8.0_f32;
130 let segs = p.underline_segments(0.0, 16.0, char_w);
131 assert!(!segs.is_empty());
132 let seg = &segs[0];
133 assert!(
135 (seg.x2 - seg.x1 - 3.0 * char_w).abs() < f32::EPSILON,
136 "underline segment width should equal char_count * char_width, got {}",
137 seg.x2 - seg.x1
138 );
139 }
140
141 #[test]
142 fn preedit_composition_window_rect_positive_size() {
143 let p = Preedit::new("hello", None);
144 let (x, y, w, h) = p.composition_window_rect(10.0, 20.0, 8.0, 16.0);
145 assert!((x - 10.0).abs() < f32::EPSILON);
146 assert!((y - 20.0).abs() < f32::EPSILON);
147 assert!(w > 0.0, "width must be positive");
148 assert!((h - 16.0).abs() < f32::EPSILON);
149 }
150
151 #[test]
152 fn preedit_composition_window_rect_empty_has_min_width() {
153 let p = Preedit::new("", None);
154 let (_, _, w, _) = p.composition_window_rect(0.0, 0.0, 8.0, 16.0);
155 assert!(
156 w >= 1.0,
157 "even empty preedit window must be at least 1px wide"
158 );
159 }
160
161 #[test]
162 fn preedit_cursor_range_stored() {
163 let p = Preedit::new("abc", Some((1, 2)));
164 assert_eq!(p.cursor_range, Some((1, 2)));
165 }
166}