1use smallvec::SmallVec;
2
3use super::layout::{Track, distribute};
4use crate::{
5 component::{Cached, Component, IntoChildren, PaintCtx, Slot, next_slot},
6 context::UiContext,
7 frame::Rect,
8 markup::{Align, Dim, Justify, VAlign},
9 props::{Prop, PropValue, Props},
10};
11
12pub struct Row {
18 props: Props,
19 slot: Slot,
20 children: Vec<Cached>,
21}
22
23impl Row {
24 pub fn new() -> Self {
26 Self { props: Props::new(), slot: next_slot(), children: Vec::new() }
27 }
28
29 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
31 self.props.set(prop, value);
32 self
33 }
34
35 pub fn with_str(mut self, prop: Prop, value: &str) -> Self {
37 self.props.set(prop, value);
38 self
39 }
40
41 pub fn child(mut self, children: impl IntoChildren) -> Self {
43 let first = self.children.len();
44 children.extend_children(&mut self.children);
45 for child in &mut self.children[first..] {
46 child.comp_mut().props_mut().set(Prop::Vertical, true);
47 child.invalidate();
48 }
49 self
50 }
51
52 fn visible(&self) -> SmallVec<usize, 8> {
53 self
54 .children
55 .iter()
56 .enumerate()
57 .filter_map(|(index, child)| child.visible.then_some(index))
58 .collect()
59 }
60
61 fn pack_width(&mut self, ctx: &UiContext, index: usize, width: u16) -> u16 {
65 let (measured_min, measured_natural) = self.children[index].measure(ctx);
66 let request = self.children[index].w(ctx);
67 let props = self.children[index].comp().props();
68 let minimum = measured_min.max(props.min().unwrap_or(0));
69 let cap = props.max().unwrap_or(u16::MAX);
70 let base = match request {
71 Some(Dim::Pct(percent)) => ((u32::from(width) * u32::from(percent)) / 100).max(1) as u16,
72 Some(Dim::Cells(cells)) => cells,
73 None if props.grow().is_some() => minimum,
74 None => measured_natural.min(width),
75 };
76 base.min(cap).max(minimum)
77 }
78
79 fn wrap_lines(
82 &mut self,
83 ctx: &UiContext,
84 visible: &[usize],
85 width: u16,
86 gap: u16,
87 ) -> SmallVec<usize, 8> {
88 let mut ends: SmallVec<usize, 8> = SmallVec::new();
89 let mut used = 0_u16;
90 let mut count = 0_usize;
91 for (position, &index) in visible.iter().enumerate() {
92 let pack = self.pack_width(ctx, index, width);
93 let extended = used.saturating_add(gap).saturating_add(pack);
94 if count > 0 && extended > width {
95 ends.push(position);
96 used = pack;
97 count = 1;
98 } else {
99 used = if count == 0 { pack } else { extended };
100 count += 1;
101 }
102 }
103 if count > 0 {
104 ends.push(visible.len());
105 }
106 ends
107 }
108
109 fn line_height(&mut self, ctx: &UiContext, line: &[usize], width: u16, gap: u16) -> u16 {
111 let widths = self.solve_row(ctx, line, width, gap);
112 line
113 .iter()
114 .zip(widths)
115 .map(|(&index, child_width)| self.children[index].height(ctx, child_width))
116 .max()
117 .unwrap_or(0)
118 }
119
120 fn solve_row(
121 &mut self,
122 ctx: &UiContext,
123 visible: &[usize],
124 available: u16,
125 gap: u16,
126 ) -> SmallVec<u16, 8> {
127 let count = visible.len();
128 let room = available.saturating_sub(
129 gap.saturating_mul(u16::try_from(count.saturating_sub(1)).unwrap_or(u16::MAX)),
130 );
131 let mut tracks: SmallVec<Track, 8> = SmallVec::new();
132 for &index in visible {
133 let (measured_min, measured_natural) = self.children[index].measure(ctx);
134 let width_request = self.children[index].w(ctx);
135 let props = self.children[index].comp().props();
136 let mut track = Track {
137 base: 0,
138 min: measured_min.max(props.min().unwrap_or(0)),
139 cap: props.max().unwrap_or(u16::MAX),
140 grow: None,
141 flexible: false,
142 };
143 track.base = match width_request {
144 Some(Dim::Pct(percent)) => {
145 track.flexible = true;
146 (u32::from(room) * u32::from(percent) / 100).max(1) as u16
147 },
148 Some(Dim::Cells(cells)) => cells,
149 None => {
150 if let Some(weight) = props.grow() {
151 track.grow = Some(weight);
152 track.min
153 } else {
154 track.flexible = true;
155 measured_natural.min(room)
156 }
157 },
158 };
159 track.base = track.base.min(track.cap).max(track.min);
160 tracks.push(track);
161 }
162 distribute(&mut tracks, room);
163 tracks.iter().map(|track| track.base).collect()
164 }
165
166 fn align_cross_axis(
167 &mut self,
168 ctx: &UiContext,
169 visible: &[usize],
170 row: Option<VAlign>,
171 top: u16,
172 tallest: u16,
173 ) {
174 for &index in visible {
175 let mode = if self.children[index].comp().stretch_in_row() {
176 VAlign::Stretch
177 } else {
178 row.unwrap_or(VAlign::Stretch)
179 };
180 let mut rect = self.children[index].rect;
181 let slack = tallest.saturating_sub(rect.height);
182 if slack == 0 {
183 continue;
184 }
185 match mode {
186 VAlign::Start => {},
187 VAlign::Center => {
188 rect.y = top.saturating_add(slack / 2);
189 self.children[index].place(ctx, rect);
190 },
191 VAlign::End => {
192 rect.y = top.saturating_add(slack);
193 self.children[index].place(ctx, rect);
194 },
195 VAlign::Stretch => {
196 rect.height = tallest;
197 self.children[index].place(ctx, rect);
198 },
199 }
200 }
201 }
202
203 fn place_line(
206 &mut self,
207 ctx: &UiContext,
208 line: &[usize],
209 x: u16,
210 y: u16,
211 width: u16,
212 gap: u16,
213 ) -> u16 {
214 let widths = self.solve_row(ctx, line, width, gap);
215 let used = widths
216 .iter()
217 .copied()
218 .fold(0_u16, u16::saturating_add)
219 .saturating_add(
220 gap.saturating_mul(u16::try_from(line.len().saturating_sub(1)).unwrap_or(0)),
221 );
222 let slack = width.saturating_sub(used);
223 let justify = match self.props.get(Prop::Justify) {
224 Some(PropValue::Justify(value)) => *value,
225 _ => Justify::Start,
226 };
227 let mut cursor = x.saturating_add(match justify {
228 Justify::Between => 0,
229 Justify::Center => slack / 2,
230 Justify::End => slack,
231 Justify::Start => match self.props.align() {
232 Align::Start => 0,
233 Align::Center => slack / 2,
234 Align::End => slack,
235 },
236 });
237 let between = u16::try_from(line.len().saturating_sub(1)).unwrap_or(0);
238 let (gap_extra, gap_remainder) = if justify == Justify::Between && between > 0 {
239 (slack / between, slack % between)
240 } else {
241 (0, 0)
242 };
243 let mut tallest = 0_u16;
244 for (position, (&index, child_width)) in line.iter().zip(widths).enumerate() {
245 let height = self.children[index].height(ctx, child_width);
246 self.children[index].place(ctx, Rect::new(cursor, y, child_width, height));
247 tallest = tallest.max(height);
248 let remainder = u16::from(u16::try_from(position).unwrap_or(u16::MAX) < gap_remainder);
249 cursor = cursor
250 .saturating_add(child_width)
251 .saturating_add(gap)
252 .saturating_add(gap_extra)
253 .saturating_add(remainder);
254 }
255 self.align_cross_axis(ctx, line, self.props.valign(), y, tallest);
256 tallest
257 }
258}
259
260impl Default for Row {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266impl Component for Row {
267 fn props(&self) -> &Props {
268 &self.props
269 }
270
271 fn props_mut(&mut self) -> &mut Props {
272 &mut self.props
273 }
274
275 fn slot(&self) -> Slot {
276 self.slot
277 }
278
279 fn children(&self) -> &[Cached] {
280 &self.children
281 }
282
283 fn children_mut(&mut self) -> &mut [Cached] {
284 &mut self.children
285 }
286
287 fn measure(&mut self, ctx: &UiContext) -> (u16, u16) {
288 let visible = self.visible();
289 let gaps = self
290 .props
291 .gap()
292 .saturating_mul(u16::try_from(visible.len().saturating_sub(1)).unwrap_or(u16::MAX));
293 let wraps = self.props.flag(Prop::Wrap);
294 let mut minimum = if wraps { 0 } else { gaps };
295 let mut natural = gaps;
296 for index in visible {
297 let (child_minimum, child_natural) = self.children[index].measure(ctx);
298 let child_minimum =
299 child_minimum.max(self.children[index].comp().props().min().unwrap_or(0));
300 if wraps {
301 minimum = minimum.max(child_minimum);
302 } else {
303 minimum = minimum.saturating_add(child_minimum);
304 }
305 natural = natural.saturating_add(child_natural);
306 }
307 (minimum, natural)
308 }
309
310 fn height(&mut self, ctx: &UiContext, width: u16) -> u16 {
311 let visible = self.visible();
312 let gap = self.props.gap();
313 if self.props.flag(Prop::Wrap) {
314 let ends = self.wrap_lines(ctx, &visible, width, gap);
315 let mut total = 0_u16;
316 let mut start = 0_usize;
317 for &end in &ends {
318 total = total.saturating_add(self.line_height(ctx, &visible[start..end], width, gap));
319 start = end;
320 }
321 return total;
322 }
323 self.line_height(ctx, &visible, width, gap)
324 }
325
326 fn place(&mut self, ctx: &UiContext, content: Rect) {
327 let visible = self.visible();
328 let gap = self.props.gap();
329 if self.props.flag(Prop::Wrap) {
330 let ends = self.wrap_lines(ctx, &visible, content.width, gap);
331 let mut top = content.y;
332 let mut start = 0_usize;
333 for &end in &ends {
334 let tallest =
335 self.place_line(ctx, &visible[start..end], content.x, top, content.width, gap);
336 top = top.saturating_add(tallest);
337 start = end;
338 }
339 return;
340 }
341 self.place_line(ctx, &visible, content.x, content.y, content.width, gap);
342 }
343
344 fn paint(&mut self, pc: &mut PaintCtx<'_>, _rect: Rect) {
345 for child in self.children.iter_mut().filter(|child| child.visible) {
346 child.paint(pc);
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::Row;
354 use crate::{
355 component::Component, components::TextLeaf, context::UiContext, frame::Rect, markup::Dim,
356 props::Prop,
357 };
358
359 #[test]
360 fn solves_percent_and_grow_widths_without_heap_scratch_for_small_rows() {
361 let ctx = UiContext::default();
362 let mut row = Row::new()
363 .child(TextLeaf::new().text("pct").with(Prop::W, Dim::Pct(50)))
364 .child(TextLeaf::new().text("grow").with(Prop::Grow, 1.0_f32));
365 assert_eq!(row.measure(&ctx), (7, 7));
366 row.place(&ctx, Rect::new(0, 0, 20, 1));
367 assert_eq!(row.children()[0].rect, Rect::new(0, 0, 10, 1));
368 assert_eq!(row.children()[1].rect, Rect::new(10, 0, 10, 1));
369 }
370}