oxitext_layout/
options.rs1use oxitext_core::{FlowDirection, TextAlignment, TextDecoration};
8
9#[derive(Debug, Clone)]
15pub struct TruncationMode {
16 pub max_width: f32,
18 pub ellipsis_advance: f32,
22 pub ellipsis_glyph_id: u16,
25}
26
27#[derive(Debug, Clone)]
32pub struct TabStops {
33 pub positions: Vec<f32>,
35 pub default_interval: f32,
38}
39
40impl TabStops {
41 pub fn with_interval(interval: f32) -> Self {
44 Self {
45 positions: Vec::new(),
46 default_interval: interval,
47 }
48 }
49
50 pub fn next_stop(&self, cursor_x: f32) -> f32 {
55 for &pos in &self.positions {
56 if pos > cursor_x + 0.5 {
57 return pos;
58 }
59 }
60 let next = ((cursor_x / self.default_interval).floor() + 1.0) * self.default_interval;
62 next.max(cursor_x + 1.0)
63 }
64}
65
66impl Default for TabStops {
67 fn default() -> Self {
68 Self {
69 positions: Vec::new(),
70 default_interval: 80.0,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
80pub struct LayoutOptions {
81 pub alignment: TextAlignment,
83 pub flow_direction: FlowDirection,
85 pub truncation: Option<TruncationMode>,
87 pub tab_stops: TabStops,
89 pub paragraph_spacing: f32,
92 pub hanging_punctuation: bool,
98 pub decoration: Option<TextDecoration>,
104 pub inline_objects: Vec<oxitext_core::InlineObject>,
109}
110
111impl Default for LayoutOptions {
112 fn default() -> Self {
113 Self {
114 alignment: TextAlignment::Left,
115 flow_direction: FlowDirection::Horizontal,
116 truncation: None,
117 tab_stops: TabStops::default(),
118 paragraph_spacing: 0.0,
119 hanging_punctuation: false,
120 decoration: None,
121 inline_objects: Vec::new(),
122 }
123 }
124}
125
126impl LayoutOptions {
127 pub fn builder() -> LayoutOptionsBuilder {
129 LayoutOptionsBuilder(Self::default())
130 }
131}
132
133pub struct LayoutOptionsBuilder(LayoutOptions);
135
136impl LayoutOptionsBuilder {
137 pub fn alignment(mut self, a: TextAlignment) -> Self {
139 self.0.alignment = a;
140 self
141 }
142
143 pub fn flow_direction(mut self, d: FlowDirection) -> Self {
145 self.0.flow_direction = d;
146 self
147 }
148
149 pub fn truncation(mut self, t: TruncationMode) -> Self {
151 self.0.truncation = Some(t);
152 self
153 }
154
155 pub fn tab_stops(mut self, ts: TabStops) -> Self {
157 self.0.tab_stops = ts;
158 self
159 }
160
161 pub fn paragraph_spacing(mut self, s: f32) -> Self {
163 self.0.paragraph_spacing = s;
164 self
165 }
166
167 pub fn hanging_punctuation(mut self, hp: bool) -> Self {
172 self.0.hanging_punctuation = hp;
173 self
174 }
175
176 pub fn decoration(mut self, d: TextDecoration) -> Self {
182 self.0.decoration = Some(d);
183 self
184 }
185
186 pub fn inline_objects(mut self, objects: Vec<oxitext_core::InlineObject>) -> Self {
191 self.0.inline_objects = objects;
192 self
193 }
194
195 pub fn build(self) -> LayoutOptions {
197 self.0
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn tab_stops_with_interval_default() {
207 let ts = TabStops::with_interval(80.0);
208 assert!(ts.positions.is_empty());
209 assert_eq!(ts.default_interval, 80.0);
210 }
211
212 #[test]
213 fn tab_stops_next_stop_interval() {
214 let ts = TabStops::with_interval(80.0);
215 let stop = ts.next_stop(10.0);
217 assert!((stop - 80.0).abs() < 1.0, "expected ~80.0, got {stop}");
218 let stop2 = ts.next_stop(80.0);
220 assert!((stop2 - 160.0).abs() < 1.0, "expected ~160.0, got {stop2}");
221 }
222
223 #[test]
224 fn tab_stops_explicit_positions() {
225 let ts = TabStops {
226 positions: vec![50.0, 120.0, 200.0],
227 default_interval: 80.0,
228 };
229 assert!((ts.next_stop(0.0) - 50.0).abs() < 1.0);
231 assert!((ts.next_stop(50.5) - 120.0).abs() < 1.0);
233 let stop = ts.next_stop(210.0);
236 assert!((stop - 240.0).abs() < 1.0, "expected ~240.0, got {stop}");
237 }
238
239 #[test]
240 fn tab_stops_default_impl() {
241 let ts = TabStops::default();
242 assert_eq!(ts.default_interval, 80.0);
243 }
244
245 #[test]
246 fn layout_options_default() {
247 let opts = LayoutOptions::default();
248 assert_eq!(opts.alignment, TextAlignment::Left);
249 assert_eq!(opts.flow_direction, FlowDirection::Horizontal);
250 assert!(opts.truncation.is_none());
251 assert_eq!(opts.paragraph_spacing, 0.0);
252 }
253
254 #[test]
255 fn layout_options_builder_sets_fields() {
256 let opts = LayoutOptions::builder()
257 .alignment(TextAlignment::Center)
258 .flow_direction(FlowDirection::Vertical)
259 .paragraph_spacing(12.0)
260 .build();
261 assert_eq!(opts.alignment, TextAlignment::Center);
262 assert_eq!(opts.flow_direction, FlowDirection::Vertical);
263 assert_eq!(opts.paragraph_spacing, 12.0);
264 }
265
266 #[test]
267 fn layout_options_builder_with_truncation() {
268 let trunc = TruncationMode {
269 max_width: 100.0,
270 ellipsis_advance: 10.0,
271 ellipsis_glyph_id: 42,
272 };
273 let opts = LayoutOptions::builder().truncation(trunc).build();
274 let t = opts.truncation.as_ref().expect("truncation should be set");
275 assert_eq!(t.max_width, 100.0);
276 assert_eq!(t.ellipsis_glyph_id, 42);
277 }
278
279 #[test]
280 fn layout_options_builder_with_tab_stops() {
281 let ts = TabStops::with_interval(40.0);
282 let opts = LayoutOptions::builder().tab_stops(ts).build();
283 assert_eq!(opts.tab_stops.default_interval, 40.0);
284 }
285
286 #[test]
287 fn layout_options_with_decoration() {
288 let opts = LayoutOptions::builder()
289 .decoration(TextDecoration::Underline {
290 color: oxitext_core::Rgba8 {
291 r: 0,
292 g: 0,
293 b: 0,
294 a: 255,
295 },
296 thickness: 1.0,
297 offset: 2.0,
298 })
299 .build();
300 assert!(opts.decoration.is_some());
301 match opts.decoration {
302 Some(TextDecoration::Underline {
303 thickness, offset, ..
304 }) => {
305 assert_eq!(thickness, 1.0);
306 assert_eq!(offset, 2.0);
307 }
308 _ => panic!("expected Underline decoration"),
309 }
310 }
311
312 #[test]
313 fn layout_options_decoration_none_by_default() {
314 let opts = LayoutOptions::default();
315 assert!(opts.decoration.is_none());
316 }
317
318 #[test]
319 fn test_layout_options_with_inline_objects() {
320 use oxitext_core::InlineObject;
321 let obj = InlineObject {
322 id: 1,
323 width: 20.0,
324 height: 20.0,
325 baseline_offset: 0.0,
326 advance: 20.0,
327 };
328 let opts = LayoutOptions::builder().inline_objects(vec![obj]).build();
329 assert_eq!(opts.inline_objects.len(), 1);
330 }
331
332 #[test]
333 fn test_styled_run_vertical_position_default() {
334 use oxitext_core::VerticalPosition;
335 let vp = VerticalPosition::Normal;
337 assert_eq!(vp.effective_size(16.0), 16.0);
338 }
339}