Skip to main content

omp_tui/components/
row.rs

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
12/// A horizontal child stack backing the `<row>` markup tag.
13///
14/// With the `wrap` flag set the row flows children into as many lines as
15/// the width allows; each line is solved and justified independently, so a
16/// wrapping row of fixed-width children behaves as a responsive grid.
17pub struct Row {
18	props:    Props,
19	slot:     Slot,
20	children: Vec<Cached>,
21}
22
23impl Row {
24	/// Creates an empty row.
25	pub fn new() -> Self {
26		Self { props: Props::new(), slot: next_slot(), children: Vec::new() }
27	}
28
29	/// Sets one row property.
30	pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
31		self.props.set(prop, value);
32		self
33	}
34
35	/// Sets one row property from a string.
36	pub fn with_str(mut self, prop: Prop, value: &str) -> Self {
37		self.props.set(prop, value);
38		self
39	}
40
41	/// Appends child components to the row.
42	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	/// Width a child occupies when packing wrap lines: the requested or
62	/// natural width clamped to `min`/`max`. Grow children pack at their
63	/// minimum and expand once their line is solved.
64	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	/// Greedily packs visible children into flow-wrap lines fitting `width`,
80	/// returning each line's exclusive end position within `visible`.
81	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	/// Height of one solved line: its tallest child at that child's width.
110	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	/// Solves, justifies, places, and cross-axis-aligns one line of children,
204	/// returning the line's height.
205	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}