1use std::sync::Arc;
8
9use parking_lot::Mutex;
10use tessera_ui::{
11 AxisConstraint, Color, ComputedData, Constraint, Dp, LayoutModifierNode, LayoutResult,
12 MeasurementError, Modifier, Px, PxPosition,
13 accesskit::Role,
14 layout::{MeasureScope, PlacementResult, PlacementScope, layout},
15 remember, remember_with_key, tessera, use_context,
16};
17use unicode_linebreak::linebreaks;
18
19use crate::{
20 alignment::Alignment,
21 boxed::boxed,
22 modifier::{ModifierExt as _, Padding, SemanticsArgs},
23 pipelines::text::{command::TextConstraint, pipeline::TextData},
24 spacer::spacer,
25 text::text,
26 theme::{MaterialTheme, TextStyle},
27};
28
29const MAX_GRID_POSITIONS: usize = 1_048_576;
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
33pub enum TextTableHeading {
34 #[default]
36 None,
37 Column,
39 Row,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
47pub struct TextTableCell {
48 pub text: String,
50 pub row: usize,
52 pub column: usize,
54 pub row_span: usize,
56 pub column_span: usize,
58 pub heading: TextTableHeading,
60}
61
62impl Default for TextTableCell {
63 fn default() -> Self {
64 Self {
65 text: String::new(),
66 row: 0,
67 column: 0,
68 row_span: 1,
69 column_span: 1,
70 heading: TextTableHeading::None,
71 }
72 }
73}
74
75#[derive(Clone, Copy, PartialEq, Eq, Hash)]
76struct ShapingStyleKey {
77 font_size: u64,
78 line_height: Option<u64>,
79 font_weight: u16,
80 letter_spacing: u64,
81 font_family: Option<&'static str>,
82}
83
84impl From<TextStyle> for ShapingStyleKey {
85 fn from(style: TextStyle) -> Self {
86 Self {
87 font_size: style.font_size.0.to_bits(),
88 line_height: style.line_height.map(|height| height.0.to_bits()),
89 font_weight: style.font_weight,
90 letter_spacing: style.letter_spacing.0.to_bits(),
91 font_family: style.font_family,
92 }
93 }
94}
95
96#[tessera]
142pub fn text_table(
143 cells: Vec<TextTableCell>,
144 columns: Option<usize>,
145 modifier: Option<Modifier>,
146 style: Option<TextStyle>,
147 header_style: Option<TextStyle>,
148 cell_padding: Option<Padding>,
149 color: Option<Color>,
150 header_color: Option<Color>,
151 header_background: Option<Color>,
152 divider_color: Option<Color>,
153) {
154 let theme = use_context::<MaterialTheme>()
155 .map(|theme| theme.get())
156 .unwrap_or_default();
157 let style = style.unwrap_or(theme.typography.body_large);
158 let header_style = header_style.unwrap_or(TextStyle {
159 font_weight: 500,
160 ..style
161 });
162 let padding = cell_padding.unwrap_or_else(|| Padding::symmetric(Dp(24.0), Dp(16.0)));
163 let color = color.unwrap_or(theme.color_scheme.on_surface);
164 let header_color = header_color.unwrap_or(color);
165 let header_background = header_background.unwrap_or(theme.color_scheme.surface_container_low);
166 let divider_color = divider_color.unwrap_or(theme.color_scheme.outline_variant);
167 let mut cells = cells;
168 cells.sort_by_key(|cell| (cell.row, cell.column));
169 let columns = columns.unwrap_or_else(|| {
170 cells
171 .iter()
172 .map(|cell| {
173 cell.column
174 .checked_add(cell.column_span)
175 .expect("Table column overflow")
176 })
177 .max()
178 .unwrap_or(0)
179 });
180 let rows = validate(&cells, columns);
181 let metrics_key = (
182 cells.clone(),
183 ShapingStyleKey::from(style),
184 ShapingStyleKey::from(header_style),
185 [
186 padding.left.0.to_bits(),
187 padding.top.0.to_bits(),
188 padding.right.0.to_bits(),
189 padding.bottom.0.to_bits(),
190 ],
191 Dp(1.0).to_pixels_f32().to_bits(),
192 );
193 let intrinsic = remember_with_key(metrics_key, move || {
194 measure_intrinsics(&cells, style, header_style, padding)
195 })
196 .get();
197 let cache = remember(|| Arc::new(Mutex::new(None::<TrackLayout>))).get();
198 let cells = intrinsic.cells.clone();
199 let modifier = modifier.unwrap_or_default().semantics(SemanticsArgs {
200 role: Some(Role::Table),
201 merge_descendants: true,
202 collection_info: Some((rows, columns, false)),
203 ..Default::default()
204 });
205 layout()
206 .modifier(modifier)
207 .policy(TableLayout {
208 intrinsic,
209 rows,
210 columns,
211 cache,
212 })
213 .child(move || {
214 for row in 0..rows {
215 table_row(
216 cells
217 .iter()
218 .filter(|cell| cell.row == row)
219 .cloned()
220 .collect(),
221 row,
222 columns,
223 style,
224 header_style,
225 padding,
226 color,
227 header_color,
228 header_background,
229 divider_color,
230 );
231 }
232 });
233}
234
235#[tessera]
236fn table_row(
237 cells: Vec<TextTableCell>,
238 row: usize,
239 columns: usize,
240 style: TextStyle,
241 header_style: TextStyle,
242 padding: Padding,
243 color: Color,
244 header_color: Color,
245 header_background: Color,
246 divider_color: Color,
247) {
248 Modifier::new()
249 .semantics(SemanticsArgs {
250 role: Some(Role::Row),
251 merge_descendants: true,
252 collection_item_info: Some((row, 1, 0, columns, false)),
253 ..Default::default()
254 })
255 .attach();
256 for cell in cells {
257 let heading = cell.heading != TextTableHeading::None;
258 let modifier = Modifier::new().semantics(SemanticsArgs {
259 role: Some(match cell.heading {
260 TextTableHeading::None => Role::Cell,
261 TextTableHeading::Column => Role::ColumnHeader,
262 TextTableHeading::Row => Role::RowHeader,
263 }),
264 label: Some(cell.text.clone()),
265 merge_descendants: false,
266 collection_item_info: Some((
267 cell.row,
268 cell.row_span,
269 cell.column,
270 cell.column_span,
271 heading,
272 )),
273 ..Default::default()
274 });
275 let modifier = if heading {
276 modifier.background(header_background)
277 } else {
278 modifier
279 };
280 boxed()
281 .alignment(Alignment::CenterStart)
282 .modifier(modifier)
283 .children(move || {
284 text(cell.text.clone())
285 .style(if heading { header_style } else { style })
286 .color(if heading { header_color } else { color })
287 .modifier(Modifier::new().padding(padding));
288 spacer().modifier(
289 Modifier::new()
290 .fill_max_width()
291 .height(Dp(1.0))
292 .background(divider_color)
293 .align(Alignment::BottomStart),
294 );
295 });
296 }
297}
298
299fn validate(cells: &[TextTableCell], columns: usize) -> usize {
300 if cells.is_empty() {
301 return 0;
302 }
303 assert!(columns > 0, "A nonempty table requires columns");
304 let rows = cells
305 .iter()
306 .map(|cell| {
307 assert!(
308 cell.row_span > 0 && cell.column_span > 0,
309 "Table spans must be positive"
310 );
311 let end = cell
312 .column
313 .checked_add(cell.column_span)
314 .expect("Table column overflow");
315 assert!(end <= columns, "A table cell exceeds the column count");
316 cell.row
317 .checked_add(cell.row_span)
318 .expect("Table row overflow")
319 })
320 .max()
321 .unwrap_or(0);
322 let count = rows
323 .checked_mul(columns)
324 .filter(|count| *count <= MAX_GRID_POSITIONS)
325 .expect("Table exceeds the supported grid allocation");
326 let mut occupied = vec![false; count];
327 for cell in cells {
328 for row in cell.row..cell.row + cell.row_span {
329 for column in cell.column..cell.column + cell.column_span {
330 let slot = &mut occupied[row * columns + column];
331 assert!(!*slot, "Table cells overlap");
332 *slot = true;
333 }
334 }
335 }
336 rows
337}
338
339#[derive(Clone, PartialEq)]
340struct Intrinsics {
341 cells: Vec<TextTableCell>,
342 widths: Vec<(i32, i32)>,
343}
344
345fn shaped_width(content: &str, style: TextStyle) -> i32 {
346 let width = TextData::shape(
347 content.to_owned(),
348 Color::BLACK,
349 style,
350 TextConstraint::default(),
351 )
352 .measure_info()
353 .size[0];
354 i32::try_from(width).expect("Table text exceeds the supported physical width")
355}
356
357fn measure_intrinsics(
358 cells: &[TextTableCell],
359 style: TextStyle,
360 header_style: TextStyle,
361 padding: Padding,
362) -> Intrinsics {
363 let horizontal: Px = padding.left.into();
364 let right: Px = padding.right.into();
365 let padding_width = horizontal.0 + right.0;
366 let widths = cells
367 .iter()
368 .map(|cell| {
369 let style = if cell.heading == TextTableHeading::None {
370 style
371 } else {
372 header_style
373 };
374 let maximum = shaped_width(&cell.text, style) + padding_width;
375 let mut start = 0;
376 let mut minimum = 0;
377 for (end, _) in linebreaks(&cell.text) {
378 let segment = cell.text[start..end].trim_end_matches(char::is_whitespace);
379 minimum = minimum.max(shaped_width(segment, style));
380 start = end;
381 }
382 (
383 minimum + padding_width,
384 maximum.max(minimum + padding_width),
385 )
386 })
387 .collect();
388 Intrinsics {
389 cells: cells.to_vec(),
390 widths,
391 }
392}
393
394#[derive(Clone)]
395struct TableLayout {
396 intrinsic: Intrinsics,
397 rows: usize,
398 columns: usize,
399 cache: Arc<Mutex<Option<TrackLayout>>>,
400}
401impl PartialEq for TableLayout {
402 fn eq(&self, other: &Self) -> bool {
403 self.intrinsic == other.intrinsic
404 && self.rows == other.rows
405 && self.columns == other.columns
406 }
407}
408
409#[derive(Clone)]
410struct TrackLayout {
411 size: ComputedData,
412 positions: Vec<PxPosition>,
413 sizes: Vec<ComputedData>,
414}
415
416impl LayoutModifierNode for TableLayout {
417 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
418 let children = input.children();
419 assert_eq!(
420 children.len(),
421 self.intrinsic.cells.len(),
422 "Table cells must create one layout child each"
423 );
424 let parent = *input.parent_constraint().as_ref();
425 if children.is_empty() {
426 let size = ComputedData {
427 width: parent.width.clamp(Px::ZERO),
428 height: parent.height.clamp(Px::ZERO),
429 };
430 *self.cache.lock() = Some(TrackLayout {
431 size,
432 positions: Vec::new(),
433 sizes: Vec::new(),
434 });
435 return Ok(LayoutResult::new(size));
436 }
437 let mut minimum = vec![0; self.columns];
438 let mut maximum = vec![0; self.columns];
439 let mut order = (0..children.len()).collect::<Vec<_>>();
440 order.sort_by_key(|&index| self.intrinsic.cells[index].column_span);
441 for index in order {
442 let cell = &self.intrinsic.cells[index];
443 let range = cell.column..cell.column + cell.column_span;
444 grow_span(&mut minimum[range.clone()], self.intrinsic.widths[index].0);
445 grow_span(&mut maximum[range], self.intrinsic.widths[index].1);
446 }
447 for (maximum, minimum) in maximum.iter_mut().zip(&minimum) {
448 *maximum = (*maximum).max(*minimum);
449 }
450 let desired = maximum.iter().sum::<i32>();
451 let width = parent.width.clamp(Px(desired));
452 let columns = fit_columns(width.0, &minimum, &maximum);
453 let x = prefix(&columns);
454 let mut rows = vec![0; self.rows];
455 let mut heights = vec![0; children.len()];
456 for (index, child) in children.iter().enumerate() {
457 let cell = &self.intrinsic.cells[index];
458 let cell_width = x[cell.column + cell.column_span] - x[cell.column];
459 let measured = child.probe_intrinsic(
460 input,
461 &Constraint::new(AxisConstraint::exact(Px(cell_width)), AxisConstraint::NONE),
462 )?;
463 heights[index] = measured.height.0;
464 if cell.row_span == 1 {
465 rows[cell.row] = rows[cell.row].max(measured.height.0);
466 }
467 }
468 let mut order = (0..children.len()).collect::<Vec<_>>();
469 order.sort_by_key(|&index| self.intrinsic.cells[index].row_span);
470 for index in order {
471 let cell = &self.intrinsic.cells[index];
472 grow_span(
473 &mut rows[cell.row..cell.row + cell.row_span],
474 heights[index],
475 );
476 }
477 let y = prefix(&rows);
478 let size = ComputedData {
479 width,
480 height: parent.height.clamp(Px(*y.last().unwrap_or(&0))),
481 };
482 let mut result = LayoutResult::new(size);
483 let mut positions = Vec::with_capacity(children.len());
484 let mut sizes = Vec::with_capacity(children.len());
485 for (index, child) in children.iter().enumerate() {
486 let cell = &self.intrinsic.cells[index];
487 let constraint = Constraint::new(
488 AxisConstraint::exact(Px(x[cell.column + cell.column_span] - x[cell.column])),
489 AxisConstraint::exact(Px(y[cell.row + cell.row_span] - y[cell.row])),
490 );
491 let measured = child.measure(input, &constraint)?;
492 let position = PxPosition::new(Px(x[cell.column]), Px(y[cell.row]));
493 result.place_child(*child, position);
494 positions.push(position);
495 sizes.push(measured.size());
496 }
497 *self.cache.lock() = Some(TrackLayout {
498 size,
499 positions,
500 sizes,
501 });
502 Ok(result)
503 }
504
505 fn place_children(
506 &self,
507 scope: &PlacementScope<'_>,
508 _: &PlacementResult,
509 ) -> Option<PlacementResult> {
510 let tracks = self.cache.lock();
511 let tracks = tracks.as_ref()?;
512 let children = scope.children();
513 if tracks.size != scope.size()
514 || children.len() != tracks.positions.len()
515 || children
516 .iter()
517 .zip(&tracks.sizes)
518 .any(|(child, size)| child.size() != *size)
519 {
520 return None;
521 }
522 let mut result = LayoutResult::new(scope.size());
523 for (child, position) in children.iter().zip(&tracks.positions) {
524 result.place_child(*child, *position);
525 }
526 Some(result.into_placement_result())
527 }
528}
529
530fn prefix(values: &[i32]) -> Vec<i32> {
531 std::iter::once(0)
532 .chain(values.iter().scan(0, |sum, value| {
533 *sum += value;
534 Some(*sum)
535 }))
536 .collect()
537}
538
539fn distribute(total: i32, weights: &[i32]) -> Vec<i32> {
540 if weights.is_empty() {
541 return Vec::new();
542 }
543 let denominator = weights
544 .iter()
545 .map(|value| (*value).max(0) as i64)
546 .sum::<i64>();
547 let denominator = if denominator == 0 {
548 weights.len() as i64
549 } else {
550 denominator
551 };
552 let equal = weights.iter().all(|weight| *weight == 0);
553 let mut remainders = Vec::with_capacity(weights.len());
554 let mut values = weights
555 .iter()
556 .enumerate()
557 .map(|(index, weight)| {
558 let numerator = i64::from(total.max(0))
559 * if equal {
560 1
561 } else {
562 i64::from((*weight).max(0))
563 };
564 remainders.push((numerator % denominator, index));
565 (numerator / denominator) as i32
566 })
567 .collect::<Vec<_>>();
568 remainders.sort_by_key(|&(remainder, index)| (std::cmp::Reverse(remainder), index));
569 let residual = total.max(0) - values.iter().sum::<i32>();
570 for &(_, index) in remainders.iter().take(residual as usize) {
571 values[index] += 1;
572 }
573 values
574}
575
576fn grow_span(values: &mut [i32], required: i32) {
577 let deficit = (required - values.iter().sum::<i32>()).max(0);
578 let extra = distribute(deficit, &vec![1; values.len()]);
579 for (value, extra) in values.iter_mut().zip(extra) {
580 *value += extra;
581 }
582}
583
584fn fit_columns(width: i32, minimum: &[i32], maximum: &[i32]) -> Vec<i32> {
585 let min_total = minimum.iter().sum::<i32>();
586 let max_total = maximum.iter().sum::<i32>();
587 if width < min_total {
588 return distribute(width, minimum);
589 }
590 let weights = if width < max_total {
591 maximum
592 .iter()
593 .zip(minimum)
594 .map(|(max, min)| max - min)
595 .collect::<Vec<_>>()
596 } else {
597 maximum.to_vec()
598 };
599 let base = if width < max_total { minimum } else { maximum };
600 let extra = distribute(width - base.iter().sum::<i32>(), &weights);
601 base.iter()
602 .zip(extra)
603 .map(|(base, extra)| base + extra)
604 .collect()
605}
606
607#[cfg(test)]
608#[path = "text_table_tests.rs"]
609mod tests;