iced-yoda/widget/src/table.rs

608 lines
17 KiB
Rust
Raw Normal View History

2025-07-15 05:28:33 +02:00
#![allow(missing_docs, missing_debug_implementations)]
use crate::core;
use crate::core::alignment;
2025-07-15 05:28:33 +02:00
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget;
use crate::core::{
Alignment, Background, Element, Layout, Length, Pixels, Rectangle, Size,
Widget,
2025-07-15 05:28:33 +02:00
};
pub fn table<'a, R, T, Message, Theme, Renderer>(
columns: impl IntoIterator<Item = Column<'a, T, Message, Theme, Renderer>>,
rows: R,
) -> Table<'a, Message, Theme, Renderer>
where
R: IntoIterator<Item = T>,
R::IntoIter: Clone,
Theme: Catalog,
Renderer: core::Renderer,
{
Table::new(columns, rows)
}
pub fn column<'a, T, E, Message, Theme, Renderer>(
header: impl Into<Element<'a, Message, Theme, Renderer>>,
view: impl Fn(T) -> E + 'a,
) -> Column<'a, T, Message, Theme, Renderer>
where
T: 'a,
E: Into<Element<'a, Message, Theme, Renderer>>,
{
Column {
header: header.into(),
view: Box::new(move |data| view(data).into()),
width: Length::Shrink,
align_x: alignment::Horizontal::Left,
align_y: alignment::Vertical::Top,
2025-07-15 05:28:33 +02:00
}
}
pub struct Table<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
Theme: Catalog,
{
columns: Vec<Column_>,
2025-07-15 05:28:33 +02:00
cells: Vec<Element<'a, Message, Theme, Renderer>>,
width: Length,
height: Length,
padding_x: f32,
padding_y: f32,
2025-07-15 05:28:33 +02:00
separator_x: f32,
separator_y: f32,
class: Theme::Class<'a>,
}
struct Column_ {
width: Length,
align_x: alignment::Horizontal,
align_y: alignment::Vertical,
}
2025-07-15 05:28:33 +02:00
impl<'a, Message, Theme, Renderer> Table<'a, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
pub fn new<R, T>(
columns: impl IntoIterator<Item = Column<'a, T, Message, Theme, Renderer>>,
rows: R,
) -> Self
where
R: IntoIterator<Item = T>,
R::IntoIter: Clone,
{
let columns = columns.into_iter();
let rows = rows.into_iter();
let mut width = Length::Shrink;
let mut height = Length::Shrink;
let mut cells = Vec::with_capacity(
columns.size_hint().0 * (1 + rows.size_hint().0),
);
let mut columns: Vec<_> = columns
.into_iter()
.map(|column| {
cells.push(column.header);
cells.extend(rows.clone().map(|row| {
let cell = (column.view)(row);
let size_hint = cell.as_widget().size_hint();
height = height.enclose(size_hint.height);
2025-07-15 05:28:33 +02:00
cell
}));
2025-07-15 05:28:33 +02:00
width = width.enclose(column.width);
2025-07-15 05:28:33 +02:00
Column_ {
width: column.width,
align_x: column.align_x,
align_y: column.align_y,
}
})
.collect();
2025-07-15 05:28:33 +02:00
if width == Length::Shrink {
if let Some(first) = columns.first_mut() {
first.width = Length::Fill;
}
}
2025-07-15 05:28:33 +02:00
Self {
columns,
2025-07-15 05:28:33 +02:00
cells,
width,
height,
padding_x: 10.0,
padding_y: 10.0,
2025-07-15 05:28:33 +02:00
separator_x: 1.0,
separator_y: 1.0,
class: Theme::default(),
}
}
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
pub fn padding(self, padding: impl Into<Pixels>) -> Self {
let padding = padding.into();
2025-07-15 05:28:33 +02:00
self.padding_x(padding).padding_y(padding)
2025-07-15 05:28:33 +02:00
}
pub fn padding_x(mut self, padding: impl Into<Pixels>) -> Self {
self.padding_x = padding.into().0;
2025-07-15 05:28:33 +02:00
self
}
pub fn padding_y(mut self, padding: impl Into<Pixels>) -> Self {
self.padding_y = padding.into().0;
2025-07-15 05:28:33 +02:00
self
}
pub fn separator(self, separator: impl Into<Pixels>) -> Self {
let separator = separator.into();
self.separator_x(separator).separator_y(separator)
}
pub fn separator_x(mut self, separator: impl Into<Pixels>) -> Self {
self.separator_x = separator.into().0;
self
}
pub fn separator_y(mut self, separator: impl Into<Pixels>) -> Self {
self.separator_y = separator.into().0;
self
}
2025-07-15 05:28:33 +02:00
}
pub struct Metrics {
columns: Vec<f32>,
rows: Vec<f32>,
2025-07-15 05:28:33 +02:00
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Table<'a, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn tag(&self) -> widget::tree::Tag {
widget::tree::Tag::of::<Metrics>()
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(Metrics {
columns: Vec::new(),
rows: Vec::new(),
2025-07-15 05:28:33 +02:00
})
}
fn children(&self) -> Vec<widget::Tree> {
self.cells
.iter()
.map(|cell| widget::Tree::new(cell.as_widget()))
.collect()
}
fn diff(&self, state: &mut widget::Tree) {
state.diff_children(&self.cells);
}
fn layout(
&self,
tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let metrics = tree.state.downcast_mut::<Metrics>();
let rows = self.cells.len() / self.columns.len();
let limits = limits.width(self.width).height(self.height);
2025-07-15 05:28:33 +02:00
let available = limits.max();
let table_fluid = self.width.fluid();
2025-07-15 05:28:33 +02:00
let mut cells = Vec::with_capacity(self.cells.len());
cells.resize(self.cells.len(), layout::Node::default());
metrics.columns = vec![0.0; self.columns.len()];
metrics.rows = vec![0.0; rows];
2025-07-15 05:28:33 +02:00
let mut column_factors = vec![0; self.columns.len()];
let mut row_factors = vec![0; rows];
let spacing_x = self.padding_x * 2.0 + self.separator_x;
let spacing_y = self.padding_y * 2.0 + self.separator_y;
2025-07-15 05:28:33 +02:00
// FIRST PASS
// Lay out non-fluid cells
let mut x = self.padding_x;
let mut y = self.padding_y;
2025-07-15 05:28:33 +02:00
for (i, (cell, state)) in
self.cells.iter().zip(&mut tree.children).enumerate()
{
let column = i / rows;
let row = i % rows;
let width = self.columns[column].width;
2025-07-15 05:28:33 +02:00
let size = cell.as_widget().size();
if row == 0 {
y = self.padding_y;
if column > 0 {
x += metrics.columns[column - 1] + spacing_x;
}
}
let width_factor = width.fill_factor();
let height_factor = size.height.fill_factor();
if width_factor != 0 || height_factor != 0 || size.width.is_fill() {
2025-07-15 05:28:33 +02:00
column_factors[column] =
column_factors[column].max(width_factor);
2025-07-15 05:28:33 +02:00
row_factors[row] = row_factors[row].max(height_factor);
2025-07-15 05:28:33 +02:00
continue;
}
let limits = layout::Limits::new(
Size::ZERO,
Size::new(available.width - x, available.height - y),
)
.width(width);
2025-07-15 05:28:33 +02:00
let layout = cell.as_widget().layout(state, renderer, &limits);
let size = limits.resolve(width, Length::Shrink, layout.size());
2025-07-15 05:28:33 +02:00
metrics.columns[column] = metrics.columns[column].max(size.width);
metrics.rows[row] = metrics.rows[row].max(size.height);
2025-07-15 05:28:33 +02:00
cells[i] = layout;
y += size.height + spacing_y;
2025-07-15 05:28:33 +02:00
}
// SECOND PASS
// Lay out fluid cells, using metrics from the first pass as limits
let left = Size::new(
available.width
- metrics
.columns
2025-07-15 05:28:33 +02:00
.iter()
.enumerate()
.filter(|(i, _)| column_factors[*i] == 0)
.map(|(_, width)| width)
.sum::<f32>(),
available.height
- metrics
.rows
2025-07-15 05:28:33 +02:00
.iter()
.enumerate()
.filter(|(i, _)| row_factors[*i] == 0)
.map(|(_, height)| height)
.sum::<f32>(),
);
let width_unit = (left.width
- spacing_x * self.columns.len().saturating_sub(1) as f32
- self.padding_x * 2.0)
2025-07-15 05:28:33 +02:00
/ column_factors.iter().sum::<u16>() as f32;
let height_unit = (left.height
- spacing_y * rows.saturating_sub(1) as f32
- self.padding_y * 2.0)
2025-07-15 05:28:33 +02:00
/ row_factors.iter().sum::<u16>() as f32;
let mut x = self.padding_x;
let mut y = self.padding_y;
2025-07-15 05:28:33 +02:00
for (i, (cell, state)) in
self.cells.iter().zip(&mut tree.children).enumerate()
{
let column = i / rows;
let row = i % rows;
2025-07-15 05:28:33 +02:00
let size = cell.as_widget().size();
let width = self.columns[column].width;
let width_factor = width.fill_factor();
if row == 0 {
y = self.padding_y;
if column > 0 {
x += metrics.columns[column - 1] + spacing_x;
}
}
if width_factor == 0
&& size.width.fill_factor() == 0
&& size.height.fill_factor() == 0
{
continue;
}
let row_factor = row_factors[row];
let max_width = if width_factor == 0 {
if size.width.is_fill() {
metrics.columns[column]
2025-07-15 05:28:33 +02:00
} else {
(available.width - x).max(0.0)
}
} else {
width_unit * width_factor as f32
};
let max_height = if row_factor == 0 {
if size.height.is_fill() {
metrics.rows[row]
2025-07-15 05:28:33 +02:00
} else {
(available.height - y).max(0.0)
}
} else {
height_unit * row_factor as f32
};
let limits = layout::Limits::new(
Size::ZERO,
Size::new(max_width, max_height),
)
.width(width);
let layout = cell.as_widget().layout(state, renderer, &limits);
let size = limits.resolve(
if let Length::Fixed(_) = width {
width
} else {
table_fluid
},
Length::Shrink,
layout.size(),
);
metrics.columns[column] = metrics.columns[column].max(size.width);
metrics.rows[row] = metrics.rows[row].max(size.height);
cells[i] = layout;
y += size.height + spacing_y;
2025-07-15 05:28:33 +02:00
}
// THIRD PASS
// Position each cell
let mut x = self.padding_x;
let mut y = self.padding_y;
2025-07-15 05:28:33 +02:00
for (i, cell) in cells.iter_mut().enumerate() {
let column = i / rows;
let row = i % rows;
if row == 0 {
y = self.padding_y;
2025-07-15 05:28:33 +02:00
if column > 0 {
x += metrics.columns[column - 1] + spacing_x;
2025-07-15 05:28:33 +02:00
}
}
let Column_ {
align_x, align_y, ..
} = &self.columns[column];
2025-07-15 05:28:33 +02:00
cell.move_to_mut((x, y));
cell.align_mut(
Alignment::from(*align_x),
Alignment::from(*align_y),
Size::new(metrics.columns[column], metrics.rows[row]),
);
2025-07-15 05:28:33 +02:00
y += metrics.rows[row] + spacing_y;
2025-07-15 05:28:33 +02:00
}
let intrinsic = limits.resolve(
self.width,
self.height,
Size::new(
x + metrics
.columns
2025-07-15 05:28:33 +02:00
.last()
.copied()
.map(|width| width + self.padding_x)
2025-07-15 05:28:33 +02:00
.unwrap_or_default(),
y - spacing_y + self.padding_y,
2025-07-15 05:28:33 +02:00
),
);
layout::Node::with_children(intrinsic, cells)
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
for ((cell, state), layout) in
self.cells.iter().zip(&tree.children).zip(layout.children())
{
cell.as_widget()
.draw(state, renderer, theme, style, layout, cursor, viewport);
}
let bounds = layout.bounds();
let metrics = tree.state.downcast_ref::<Metrics>();
let style = theme.style(&self.class);
if self.separator_x > 0.0 {
let mut x = self.padding_x;
2025-07-15 05:28:33 +02:00
for width in
&metrics.columns[..metrics.columns.len().saturating_sub(1)]
2025-07-15 05:28:33 +02:00
{
x += width + self.padding_x;
2025-07-15 05:28:33 +02:00
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + x,
y: bounds.y,
width: self.separator_x,
height: bounds.height,
},
snap: true,
..renderer::Quad::default()
},
style.separator_x,
);
x += self.separator_x + self.padding_x;
2025-07-15 05:28:33 +02:00
}
}
if self.separator_y > 0.0 {
let mut y = self.padding_y;
2025-07-15 05:28:33 +02:00
for height in &metrics.rows[..metrics.rows.len().saturating_sub(1)]
2025-07-15 05:28:33 +02:00
{
y += height + self.padding_y;
2025-07-15 05:28:33 +02:00
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
y: bounds.y + y,
width: bounds.width,
height: self.separator_y,
},
snap: true,
..renderer::Quad::default()
},
style.separator_y,
);
y += self.separator_y + self.padding_y;
2025-07-15 05:28:33 +02:00
}
}
}
}
impl<'a, Message, Theme, Renderer> From<Table<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: Catalog + 'a,
Renderer: core::Renderer + 'a,
{
fn from(table: Table<'a, Message, Theme, Renderer>) -> Self {
Element::new(table)
}
}
pub struct Column<
'a,
T,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> {
header: Element<'a, Message, Theme, Renderer>,
view: Box<dyn Fn(T) -> Element<'a, Message, Theme, Renderer> + 'a>,
width: Length,
align_x: alignment::Horizontal,
align_y: alignment::Vertical,
2025-07-15 05:28:33 +02:00
}
impl<'a, T, Message, Theme, Renderer> Column<'a, T, Message, Theme, Renderer> {
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the alignment for the horizontal axis of the [`Column`].
pub fn align_x(
mut self,
alignment: impl Into<alignment::Horizontal>,
) -> Self {
self.align_x = alignment.into();
self
}
/// Sets the alignment for the vertical axis of the [`Column`].
pub fn align_y(
mut self,
alignment: impl Into<alignment::Vertical>,
) -> Self {
self.align_y = alignment.into();
self
}
2025-07-15 05:28:33 +02:00
}
#[derive(Debug, Clone, Copy)]
pub struct Style {
pub separator_x: Background,
pub separator_y: Background,
}
/// The theme catalog of a [`Table`].
pub trait Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>) -> Style;
}
/// A styling function for a [`Table`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl<Theme> From<Style> for StyleFn<'_, Theme> {
fn from(style: Style) -> Self {
Box::new(move |_theme| style)
}
}
impl Catalog for crate::Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
/// The default style of a [`Table`].
pub fn default(theme: &crate::Theme) -> Style {
let palette = theme.extended_palette();
Style {
separator_x: palette.background.strong.color.into(),
separator_y: palette.background.strong.color.into(),
}
}