1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3pub mod bidi;
17pub mod engine;
18pub mod hyphenation;
19pub mod knuth_plass;
20pub mod linebreak;
21pub mod options;
22pub mod reorder;
23pub mod ruby;
24pub mod styled;
25pub mod tate_chu_yoko;
26pub mod vertical;
27
28pub use engine::{
29 BreakingStrategy, LayoutEngine, LayoutResult, Line, LineMetrics, ParagraphMetrics,
30};
31pub use hyphenation::soft_hyphen_breaks;
32pub use options::{LayoutOptions, LayoutOptionsBuilder, TabStops, TruncationMode};
33pub use oxitext_core::{
34 DecorationRect, InlineObject, PositionedInlineObject, TextDecoration, VerticalPosition,
35};
36pub use reorder::needs_bidi;
37pub use ruby::{layout_ruby, RubyAnnotation, RubyLayout, RubyPosition};
38pub use styled::StyledRun;
39pub use tate_chu_yoko::{detect_runs, tcy_combined_advance, GlyphEntry, TateChuYokoRun};
40pub use vertical::vmtx_advance_for_glyph;
41
42use oxitext_core::{FlowDirection, LayoutConstraints, OxiTextError, PositionedGlyph, ShapedRun};
43use std::sync::Arc;
44
45pub struct SimpleLayouter {
56 pub flow_direction: FlowDirection,
58}
59
60impl SimpleLayouter {
61 pub fn new() -> Self {
63 Self {
64 flow_direction: FlowDirection::Horizontal,
65 }
66 }
67
68 pub fn with_flow_direction(mut self, dir: FlowDirection) -> Self {
70 self.flow_direction = dir;
71 self
72 }
73
74 pub fn layout(
82 &self,
83 runs: &[ShapedRun],
84 constraints: &LayoutConstraints,
85 ) -> Result<Vec<PositionedGlyph>, OxiTextError> {
86 match self.flow_direction {
87 FlowDirection::Horizontal => self.layout_horizontal(runs, constraints),
88 FlowDirection::Vertical => self.layout_vertical(runs, constraints),
89 }
90 }
91
92 fn layout_horizontal(
96 &self,
97 runs: &[ShapedRun],
98 constraints: &LayoutConstraints,
99 ) -> Result<Vec<PositionedGlyph>, OxiTextError> {
100 let mut positioned = Vec::new();
101 let mut cursor_x: f32 = 0.0;
102 let line_height = constraints.font_size * 1.4;
104 let mut cursor_y: f32 = constraints.font_size * 1.2;
105
106 for run in runs {
107 let font_data = Arc::clone(&run.font_data);
108 for glyph in &run.glyphs {
109 if constraints.max_width > 0.0 && cursor_x + glyph.x_advance > constraints.max_width
111 {
112 cursor_x = 0.0;
113 cursor_y += line_height;
114 }
115 positioned.push(PositionedGlyph {
116 gid: glyph.gid,
117 font_data: Arc::clone(&font_data),
118 pos: (cursor_x + glyph.x_offset, cursor_y + glyph.y_offset),
119 font_size: constraints.font_size,
120 advance_x: glyph.x_advance,
121 cluster: glyph.cluster,
122 });
123 cursor_x += glyph.x_advance;
124 }
125 }
126
127 Ok(positioned)
128 }
129
130 fn layout_vertical(
140 &self,
141 runs: &[ShapedRun],
142 constraints: &LayoutConstraints,
143 ) -> Result<Vec<PositionedGlyph>, OxiTextError> {
144 let mut positioned = Vec::new();
145 let column_width = constraints.font_size * 1.2;
146 let mut column_x: f32 = 0.0;
147 let mut cursor_y: f32 = 0.0;
148 let max_col_h = constraints.max_width; for run in runs {
151 let font_data = Arc::clone(&run.font_data);
152 for glyph in &run.glyphs {
153 let v_adv = if glyph.y_advance > 0.0 {
155 glyph.y_advance
156 } else {
157 glyph.x_advance
158 };
159
160 if max_col_h > 0.0 && cursor_y + v_adv > max_col_h && cursor_y > 0.0 {
162 column_x += column_width;
163 cursor_y = 0.0;
164 }
165
166 positioned.push(PositionedGlyph {
167 gid: glyph.gid,
168 font_data: Arc::clone(&font_data),
169 pos: (column_x + glyph.x_offset, cursor_y + glyph.y_offset),
170 font_size: constraints.font_size,
171 advance_x: glyph.x_advance,
172 cluster: glyph.cluster,
173 });
174 cursor_y += v_adv;
175 }
176 }
177
178 Ok(positioned)
179 }
180}
181
182impl Default for SimpleLayouter {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use oxitext_core::{LayoutConstraints, ShapedGlyph, ShapedRun};
192 use std::sync::Arc;
193
194 fn make_run(advances: &[f32]) -> ShapedRun {
195 let glyphs = advances
196 .iter()
197 .enumerate()
198 .map(|(i, &adv)| ShapedGlyph {
199 gid: (i + 1) as u16,
200 x_advance: adv,
201 cluster: i as u32,
202 ..Default::default()
203 })
204 .collect();
205 ShapedRun {
206 glyphs,
207 font_data: Arc::from(&[][..]),
208 }
209 }
210
211 #[test]
212 fn layout_positions_are_monotonically_increasing_x() {
213 let run = make_run(&[10.0, 10.0, 10.0, 10.0, 10.0]);
214 let constraints = LayoutConstraints {
215 max_width: 800.0,
216 font_size: 16.0,
217 };
218 let layouter = SimpleLayouter::new();
219 let positioned = layouter
220 .layout(&[run], &constraints)
221 .expect("layout failed");
222 assert_eq!(positioned.len(), 5);
223 for window in positioned.windows(2) {
225 assert!(
226 window[1].pos.0 > window[0].pos.0,
227 "x should increase: {} <= {}",
228 window[1].pos.0,
229 window[0].pos.0
230 );
231 }
232 }
233
234 #[test]
235 fn layout_wraps_when_max_width_exceeded() {
236 let run = make_run(&[200.0, 200.0, 200.0, 200.0, 200.0]);
238 let constraints = LayoutConstraints {
239 max_width: 800.0,
240 font_size: 16.0,
241 };
242 let layouter = SimpleLayouter::new();
243 let positioned = layouter
244 .layout(&[run], &constraints)
245 .expect("layout failed");
246 assert_eq!(positioned.len(), 5);
247 let y_first = positioned[0].pos.1;
251 let y_wrap = positioned[4].pos.1;
252 assert!(
253 y_wrap > y_first,
254 "wrapped glyph should be on a lower line: y_first={y_first}, y_wrap={y_wrap}"
255 );
256 }
257}