Merge pull request #3018 from iced-rs/feature/table-widget
`table` widget
This commit is contained in:
commit
71b0b6ae07
18 changed files with 1267 additions and 123 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -5633,6 +5633,13 @@ dependencies = [
|
|||
"iced",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "table"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"iced",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
|
|
|
|||
|
|
@ -532,3 +532,49 @@ where
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> From<Option<T>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
T: Into<Self>,
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn from(element: Option<T>) -> Self {
|
||||
struct Void;
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Void
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: Length::Fixed(0.0),
|
||||
height: Length::Fixed(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_tree: &mut Tree,
|
||||
_renderer: &Renderer,
|
||||
_limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::Node::new(Size::ZERO)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
_renderer: &mut Renderer,
|
||||
_theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
_layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
element.map(T::into).unwrap_or_else(|| Element::new(Void))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,22 +52,22 @@ impl Node {
|
|||
/// Aligns the [`Node`] in the given space.
|
||||
pub fn align(
|
||||
mut self,
|
||||
horizontal_alignment: Alignment,
|
||||
vertical_alignment: Alignment,
|
||||
align_x: Alignment,
|
||||
align_y: Alignment,
|
||||
space: Size,
|
||||
) -> Self {
|
||||
self.align_mut(horizontal_alignment, vertical_alignment, space);
|
||||
self.align_mut(align_x, align_y, space);
|
||||
self
|
||||
}
|
||||
|
||||
/// Mutable reference version of [`Self::align`].
|
||||
pub fn align_mut(
|
||||
&mut self,
|
||||
horizontal_alignment: Alignment,
|
||||
vertical_alignment: Alignment,
|
||||
align_x: Alignment,
|
||||
align_y: Alignment,
|
||||
space: Size,
|
||||
) {
|
||||
match horizontal_alignment {
|
||||
match align_x {
|
||||
Alignment::Start => {}
|
||||
Alignment::Center => {
|
||||
self.bounds.x += (space.width - self.bounds.width) / 2.0;
|
||||
|
|
@ -77,7 +77,7 @@ impl Node {
|
|||
}
|
||||
}
|
||||
|
||||
match vertical_alignment {
|
||||
match align_y {
|
||||
Alignment::Start => {}
|
||||
Alignment::Center => {
|
||||
self.bounds.y += (space.height - self.bounds.height) / 2.0;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ impl Length {
|
|||
|
||||
/// Adapts the [`Length`] so it can contain the other [`Length`] and
|
||||
/// match its fluidity.
|
||||
#[inline]
|
||||
pub fn enclose(self, other: Length) -> Self {
|
||||
match (self, other) {
|
||||
(Length::Shrink, Length::Fill | Length::FillPortion(_)) => other,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{Radians, Vector};
|
||||
use crate::{Length, Radians, Vector};
|
||||
|
||||
/// An amount of space in 2 dimensions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
|
|
@ -66,6 +66,15 @@ impl Size {
|
|||
}
|
||||
}
|
||||
|
||||
impl Size<Length> {
|
||||
/// Returns true if either `width` or `height` are 0-sized.
|
||||
#[inline]
|
||||
pub fn is_void(&self) -> bool {
|
||||
matches!(self.width, Length::Fixed(0.0))
|
||||
|| matches!(self.height, Length::Fixed(0.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<[T; 2]> for Size<T> {
|
||||
fn from([width, height]: [T; 2]) -> Self {
|
||||
Size { width, height }
|
||||
|
|
|
|||
|
|
@ -452,6 +452,13 @@ pub fn success(theme: &Theme) -> Style {
|
|||
}
|
||||
}
|
||||
|
||||
/// Text conveying some mildly negative information, like a warning.
|
||||
pub fn warning(theme: &Theme) -> Style {
|
||||
Style {
|
||||
color: Some(theme.palette().warning),
|
||||
}
|
||||
}
|
||||
|
||||
/// Text conveying some negative information, like an error.
|
||||
pub fn danger(theme: &Theme) -> Style {
|
||||
Style {
|
||||
|
|
|
|||
|
|
@ -276,13 +276,13 @@ fn view_content<'a>(
|
|||
button(
|
||||
"Split vertically",
|
||||
Message::Split(pane_grid::Axis::Vertical, pane),
|
||||
)
|
||||
),
|
||||
if total_panes > 1 && !is_pinned {
|
||||
Some(button("Close", Message::Close(pane)).style(button::danger))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
]
|
||||
.push_maybe(if total_panes > 1 && !is_pinned {
|
||||
Some(button("Close", Message::Close(pane)).style(button::danger))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.spacing(5)
|
||||
.max_width(160);
|
||||
|
||||
|
|
@ -300,7 +300,7 @@ fn view_controls<'a>(
|
|||
is_pinned: bool,
|
||||
is_maximized: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let row = row![].spacing(5).push_maybe(if total_panes > 1 {
|
||||
let maximize = if total_panes > 1 {
|
||||
let (content, message) = if is_maximized {
|
||||
("Restore", Message::Restore)
|
||||
} else {
|
||||
|
|
@ -315,7 +315,7 @@ fn view_controls<'a>(
|
|||
)
|
||||
} else {
|
||||
None
|
||||
});
|
||||
};
|
||||
|
||||
let close = button(text("Close").size(14))
|
||||
.style(button::danger)
|
||||
|
|
@ -326,7 +326,7 @@ fn view_controls<'a>(
|
|||
None
|
||||
});
|
||||
|
||||
row.push(close).into()
|
||||
row![maximize, close].spacing(5).into()
|
||||
}
|
||||
|
||||
mod style {
|
||||
|
|
|
|||
|
|
@ -88,18 +88,18 @@ impl QRGenerator {
|
|||
input,
|
||||
row![toggle_total_size, choose_theme]
|
||||
.spacing(20)
|
||||
.align_y(Center)
|
||||
.align_y(Center),
|
||||
self.total_size.map(|total_size| {
|
||||
slider(Self::SIZE_RANGE, total_size, Message::TotalSizeChanged)
|
||||
}),
|
||||
self.qr_code.as_ref().map(|data| {
|
||||
if let Some(total_size) = self.total_size {
|
||||
qr_code(data).total_size(total_size)
|
||||
} else {
|
||||
qr_code(data).cell_size(10.0)
|
||||
}
|
||||
})
|
||||
]
|
||||
.push_maybe(self.total_size.map(|total_size| {
|
||||
slider(Self::SIZE_RANGE, total_size, Message::TotalSizeChanged)
|
||||
}))
|
||||
.push_maybe(self.qr_code.as_ref().map(|data| {
|
||||
if let Some(total_size) = self.total_size {
|
||||
qr_code(data).total_size(total_size)
|
||||
} else {
|
||||
qr_code(data).cell_size(10.0)
|
||||
}
|
||||
}))
|
||||
.width(700)
|
||||
.spacing(20)
|
||||
.align_x(Center);
|
||||
|
|
|
|||
|
|
@ -158,15 +158,15 @@ impl Example {
|
|||
.spacing(10)
|
||||
.align_y(Center);
|
||||
|
||||
let crop_controls =
|
||||
column![crop_origin_controls, crop_dimension_controls]
|
||||
.push_maybe(
|
||||
self.crop_error
|
||||
.as_ref()
|
||||
.map(|error| text!("Crop error! \n{error}")),
|
||||
)
|
||||
.spacing(10)
|
||||
.align_x(Center);
|
||||
let crop_controls = column![
|
||||
crop_origin_controls,
|
||||
crop_dimension_controls,
|
||||
self.crop_error
|
||||
.as_ref()
|
||||
.map(|error| text!("Crop error! \n{error}")),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_x(Center);
|
||||
|
||||
let controls = {
|
||||
let save_result =
|
||||
|
|
@ -208,8 +208,8 @@ impl Example {
|
|||
]
|
||||
.spacing(10)
|
||||
.align_x(Center),
|
||||
save_result.map(text)
|
||||
]
|
||||
.push_maybe(save_result.map(text))
|
||||
.spacing(40)
|
||||
};
|
||||
|
||||
|
|
|
|||
10
examples/table/Cargo.toml
Normal file
10
examples/table/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "table"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["debug"]
|
||||
252
examples/table/src/main.rs
Normal file
252
examples/table/src/main.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
use iced::font;
|
||||
use iced::time::{Duration, hours, minutes};
|
||||
use iced::widget::{
|
||||
center_x, center_y, column, container, row, scrollable, slider, table,
|
||||
text, tooltip,
|
||||
};
|
||||
use iced::{Center, Element, Fill, Font, Right, Theme};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::application(Table::new, Table::update, Table::view)
|
||||
.theme(|_| Theme::CatppuccinMocha)
|
||||
.run()
|
||||
}
|
||||
|
||||
struct Table {
|
||||
events: Vec<Event>,
|
||||
padding: (f32, f32),
|
||||
separator: (f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
PaddingChanged(f32, f32),
|
||||
SeparatorChanged(f32, f32),
|
||||
}
|
||||
|
||||
impl Table {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
events: Event::list(),
|
||||
padding: (10.0, 5.0),
|
||||
separator: (1.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::PaddingChanged(x, y) => self.padding = (x, y),
|
||||
Message::SeparatorChanged(x, y) => self.separator = (x, y),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Message> {
|
||||
let table = {
|
||||
let bold = |header| {
|
||||
text(header).font(Font {
|
||||
weight: font::Weight::Bold,
|
||||
..Font::DEFAULT
|
||||
})
|
||||
};
|
||||
|
||||
let columns = [
|
||||
table::column(bold("Name"), |event: &Event| text(&event.name)),
|
||||
table::column(bold("Time"), |event: &Event| {
|
||||
let minutes = event.duration.as_secs() / 60;
|
||||
|
||||
text!("{minutes} min").style(if minutes > 90 {
|
||||
text::warning
|
||||
} else {
|
||||
text::default
|
||||
})
|
||||
})
|
||||
.align_x(Right)
|
||||
.align_y(Center),
|
||||
table::column(bold("Price"), |event: &Event| {
|
||||
if event.price > 0.0 {
|
||||
text!("${:.2}", event.price).style(
|
||||
if event.price > 100.0 {
|
||||
text::warning
|
||||
} else {
|
||||
text::default
|
||||
},
|
||||
)
|
||||
} else {
|
||||
text("Free").style(text::success).width(Fill).center()
|
||||
}
|
||||
})
|
||||
.align_x(Right)
|
||||
.align_y(Center),
|
||||
table::column(bold("Rating"), |event: &Event| {
|
||||
text!("{:.2}", event.rating).style(if event.rating > 4.7 {
|
||||
text::success
|
||||
} else if event.rating < 2.0 {
|
||||
text::danger
|
||||
} else {
|
||||
text::default
|
||||
})
|
||||
})
|
||||
.align_x(Right)
|
||||
.align_y(Center),
|
||||
];
|
||||
|
||||
table(columns, &self.events)
|
||||
.padding_x(self.padding.0)
|
||||
.padding_y(self.padding.1)
|
||||
.separator_x(self.separator.0)
|
||||
.separator_y(self.separator.1)
|
||||
};
|
||||
|
||||
let controls = {
|
||||
let labeled_slider =
|
||||
|label,
|
||||
range: std::ops::RangeInclusive<f32>,
|
||||
(x, y),
|
||||
on_change: fn(f32, f32) -> Message| {
|
||||
row![
|
||||
text(label).font(Font::MONOSPACE).size(14).width(100),
|
||||
tooltip(
|
||||
slider(range.clone(), x, move |x| on_change(x, y)),
|
||||
text!("{x:.0}px").font(Font::MONOSPACE).size(10),
|
||||
tooltip::Position::Left
|
||||
),
|
||||
tooltip(
|
||||
slider(range, y, move |y| on_change(x, y)),
|
||||
text!("{y:.0}px").font(Font::MONOSPACE).size(10),
|
||||
tooltip::Position::Right
|
||||
),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Center)
|
||||
};
|
||||
|
||||
column![
|
||||
labeled_slider(
|
||||
"Padding",
|
||||
0.0..=30.0,
|
||||
self.padding,
|
||||
Message::PaddingChanged
|
||||
),
|
||||
labeled_slider(
|
||||
"Separator",
|
||||
0.0..=5.0,
|
||||
self.separator,
|
||||
Message::SeparatorChanged
|
||||
)
|
||||
]
|
||||
.spacing(10)
|
||||
.width(400)
|
||||
};
|
||||
|
||||
column![
|
||||
center_y(scrollable(center_x(table)).spacing(10)).padding(10),
|
||||
center_x(controls).padding(10).style(container::dark)
|
||||
]
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
struct Event {
|
||||
name: String,
|
||||
duration: Duration,
|
||||
price: f32,
|
||||
rating: f32,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
fn list() -> Vec<Self> {
|
||||
vec![
|
||||
Event {
|
||||
name: "Get lost in a hacker bookstore".to_owned(),
|
||||
duration: hours(2),
|
||||
price: 0.0,
|
||||
rating: 4.9,
|
||||
},
|
||||
Event {
|
||||
name: "Buy vintage synth at Noisebridge flea market".to_owned(),
|
||||
duration: hours(1),
|
||||
price: 150.0,
|
||||
rating: 4.8,
|
||||
},
|
||||
Event {
|
||||
name: "Eat a questionable hot dog at 2AM".to_owned(),
|
||||
duration: minutes(20),
|
||||
price: 5.0,
|
||||
rating: 1.7,
|
||||
},
|
||||
Event {
|
||||
name: "Ride the MUNI for the story".to_owned(),
|
||||
duration: minutes(60),
|
||||
price: 3.0,
|
||||
rating: 4.1,
|
||||
},
|
||||
Event {
|
||||
name: "Scream into the void from Twin Peaks".to_owned(),
|
||||
duration: minutes(40),
|
||||
price: 0.0,
|
||||
rating: 4.9,
|
||||
},
|
||||
Event {
|
||||
name: "Buy overpriced coffee and feel things".to_owned(),
|
||||
duration: minutes(25),
|
||||
price: 6.5,
|
||||
rating: 4.5,
|
||||
},
|
||||
Event {
|
||||
name: "Attend an underground robot poetry slam".to_owned(),
|
||||
duration: hours(1),
|
||||
price: 12.0,
|
||||
rating: 4.8,
|
||||
},
|
||||
Event {
|
||||
name: "Browse cursed tech at a retro computer fair".to_owned(),
|
||||
duration: hours(2),
|
||||
price: 10.0,
|
||||
rating: 4.7,
|
||||
},
|
||||
Event {
|
||||
name: "Try to order at a secret ramen place with no sign"
|
||||
.to_owned(),
|
||||
duration: minutes(50),
|
||||
price: 14.0,
|
||||
rating: 4.6,
|
||||
},
|
||||
Event {
|
||||
name: "Join a spontaneous rooftop drone rave".to_owned(),
|
||||
duration: hours(3),
|
||||
price: 0.0,
|
||||
rating: 4.9,
|
||||
},
|
||||
Event {
|
||||
name: "Sketch a stranger at Dolores Park".to_owned(),
|
||||
duration: minutes(45),
|
||||
price: 0.0,
|
||||
rating: 4.4,
|
||||
},
|
||||
Event {
|
||||
name: "Visit the Museum of Obsolete APIs".to_owned(),
|
||||
duration: hours(1),
|
||||
price: 9.99,
|
||||
rating: 4.2,
|
||||
},
|
||||
Event {
|
||||
name: "Chase the last working payphone".to_owned(),
|
||||
duration: minutes(35),
|
||||
price: 0.25,
|
||||
rating: 4.0,
|
||||
},
|
||||
Event {
|
||||
name: "Trade zines with a punk on BART".to_owned(),
|
||||
duration: minutes(30),
|
||||
price: 3.5,
|
||||
rating: 4.7,
|
||||
},
|
||||
Event {
|
||||
name: "Get a tattoo of the Git logo".to_owned(),
|
||||
duration: hours(1),
|
||||
price: 200.0,
|
||||
rating: 4.6,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -142,17 +142,17 @@ impl Tour {
|
|||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Message> {
|
||||
let controls =
|
||||
row![]
|
||||
.push_maybe(self.screen.previous().is_some().then(|| {
|
||||
padded_button("Back")
|
||||
.on_press(Message::BackPressed)
|
||||
.style(button::secondary)
|
||||
}))
|
||||
.push(horizontal_space())
|
||||
.push_maybe(self.can_continue().then(|| {
|
||||
padded_button("Next").on_press(Message::NextPressed)
|
||||
}));
|
||||
let controls = row![
|
||||
self.screen.previous().is_some().then(|| {
|
||||
padded_button("Back")
|
||||
.on_press(Message::BackPressed)
|
||||
.style(button::secondary)
|
||||
}),
|
||||
horizontal_space(),
|
||||
self.can_continue().then(|| {
|
||||
padded_button("Next").on_press(Message::NextPressed)
|
||||
})
|
||||
];
|
||||
|
||||
let screen = match self.screen {
|
||||
Screen::Welcome => self.welcome(),
|
||||
|
|
|
|||
|
|
@ -145,23 +145,13 @@ where
|
|||
let child = child.into();
|
||||
let child_size = child.as_widget().size_hint();
|
||||
|
||||
self.width = self.width.enclose(child_size.width);
|
||||
self.height = self.height.enclose(child_size.height);
|
||||
|
||||
self.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an element to the [`Column`], if `Some`.
|
||||
pub fn push_maybe(
|
||||
self,
|
||||
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||
) -> Self {
|
||||
if let Some(child) = child {
|
||||
self.push(child)
|
||||
} else {
|
||||
self
|
||||
if !child_size.is_void() {
|
||||
self.width = self.width.enclose(child_size.width);
|
||||
self.height = self.height.enclose(child_size.height);
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Extends the [`Column`] with the given children.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ use crate::{Column, Grid, MouseArea, Pin, Pop, Row, Space, Stack, Themer};
|
|||
use std::borrow::Borrow;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use crate::table::table;
|
||||
|
||||
/// Creates a [`Column`] with the given children.
|
||||
///
|
||||
/// Columns distribute their children vertically.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ pub mod row;
|
|||
pub mod rule;
|
||||
pub mod scrollable;
|
||||
pub mod slider;
|
||||
pub mod table;
|
||||
pub mod text;
|
||||
pub mod text_editor;
|
||||
pub mod text_input;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::alignment;
|
||||
use crate::core::border;
|
||||
use crate::core::font::{self, Font};
|
||||
use crate::core::padding;
|
||||
|
|
@ -107,13 +108,17 @@ impl Content {
|
|||
let mut leftover = std::mem::take(&mut self.state.leftover);
|
||||
leftover.push_str(markdown);
|
||||
|
||||
let input = if leftover.trim_end().ends_with('|') {
|
||||
leftover.trim_end().trim_end_matches('|')
|
||||
} else {
|
||||
leftover.as_str()
|
||||
};
|
||||
|
||||
// Pop the last item
|
||||
let _ = self.items.pop();
|
||||
|
||||
// Re-parse last item and new text
|
||||
for (item, source, broken_links) in
|
||||
parse_with(&mut self.state, &leftover)
|
||||
{
|
||||
for (item, source, broken_links) in parse_with(&mut self.state, input) {
|
||||
if !broken_links.is_empty() {
|
||||
let _ = self.incomplete.insert(
|
||||
self.items.len(),
|
||||
|
|
@ -127,6 +132,8 @@ impl Content {
|
|||
self.items.push(item);
|
||||
}
|
||||
|
||||
self.state.leftover.push_str(&leftover[input.len()..]);
|
||||
|
||||
// Re-parse incomplete sections if new references are available
|
||||
if !self.incomplete.is_empty() {
|
||||
self.incomplete.retain(|index, section| {
|
||||
|
|
@ -215,6 +222,29 @@ pub enum Item {
|
|||
Quote(Vec<Item>),
|
||||
/// A horizontal separator.
|
||||
Rule,
|
||||
/// A table.
|
||||
Table {
|
||||
/// The columns of the table.
|
||||
columns: Vec<Column>,
|
||||
/// The rows of the table.
|
||||
rows: Vec<Row>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The column of a table.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Column {
|
||||
/// The header of the column.
|
||||
pub header: Vec<Item>,
|
||||
/// The alignment of the column.
|
||||
pub alignment: pulldown_cmark::Alignment,
|
||||
}
|
||||
|
||||
/// The row of a table.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Row {
|
||||
/// The cells of the row.
|
||||
cells: Vec<Vec<Item>>,
|
||||
}
|
||||
|
||||
/// A bunch of parsed Markdown text.
|
||||
|
|
@ -462,6 +492,12 @@ fn parse_with<'a>(
|
|||
enum Scope {
|
||||
List(List),
|
||||
Quote(Vec<Item>),
|
||||
Table {
|
||||
alignment: Vec<pulldown_cmark::Alignment>,
|
||||
columns: Vec<Column>,
|
||||
rows: Vec<Row>,
|
||||
current: Vec<Item>,
|
||||
},
|
||||
}
|
||||
|
||||
struct List {
|
||||
|
|
@ -479,7 +515,6 @@ fn parse_with<'a>(
|
|||
let mut emphasis = false;
|
||||
let mut strikethrough = false;
|
||||
let mut metadata = false;
|
||||
let mut table = false;
|
||||
let mut code_block = false;
|
||||
let mut link = None;
|
||||
let mut image = None;
|
||||
|
|
@ -535,6 +570,9 @@ fn parse_with<'a>(
|
|||
Scope::Quote(items) => {
|
||||
items.push(item);
|
||||
}
|
||||
Scope::Table { current, .. } => {
|
||||
current.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
|
|
@ -555,21 +593,19 @@ fn parse_with<'a>(
|
|||
#[allow(clippy::drain_collect)]
|
||||
parser.filter_map(move |(event, source)| match event {
|
||||
pulldown_cmark::Event::Start(tag) => match tag {
|
||||
pulldown_cmark::Tag::Strong if !metadata && !table => {
|
||||
pulldown_cmark::Tag::Strong if !metadata => {
|
||||
strong = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Emphasis if !metadata && !table => {
|
||||
pulldown_cmark::Tag::Emphasis if !metadata => {
|
||||
emphasis = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Strikethrough if !metadata && !table => {
|
||||
pulldown_cmark::Tag::Strikethrough if !metadata => {
|
||||
strikethrough = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Link { dest_url, .. }
|
||||
if !metadata && !table =>
|
||||
{
|
||||
pulldown_cmark::Tag::Link { dest_url, .. } if !metadata => {
|
||||
match Url::parse(&dest_url) {
|
||||
Ok(url)
|
||||
if url.scheme() == "http"
|
||||
|
|
@ -584,13 +620,13 @@ fn parse_with<'a>(
|
|||
}
|
||||
pulldown_cmark::Tag::Image {
|
||||
dest_url, title, ..
|
||||
} if !metadata && !table => {
|
||||
} if !metadata => {
|
||||
image = Url::parse(&dest_url)
|
||||
.ok()
|
||||
.map(|url| (url, title.into_string()));
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
|
||||
pulldown_cmark::Tag::List(first_item) if !metadata => {
|
||||
let prev = if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -616,7 +652,7 @@ fn parse_with<'a>(
|
|||
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::BlockQuote(_kind) if !metadata && !table => {
|
||||
pulldown_cmark::Tag::BlockQuote(_kind) if !metadata => {
|
||||
let prev = if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -634,7 +670,7 @@ fn parse_with<'a>(
|
|||
}
|
||||
pulldown_cmark::Tag::CodeBlock(
|
||||
pulldown_cmark::CodeBlockKind::Fenced(language),
|
||||
) if !metadata && !table => {
|
||||
) if !metadata => {
|
||||
#[cfg(feature = "highlighter")]
|
||||
{
|
||||
highlighter = Some({
|
||||
|
|
@ -672,38 +708,54 @@ fn parse_with<'a>(
|
|||
metadata = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Table(_) => {
|
||||
table = true;
|
||||
pulldown_cmark::Tag::Table(alignment) => {
|
||||
stack.push(Scope::Table {
|
||||
columns: Vec::with_capacity(alignment.len()),
|
||||
alignment,
|
||||
current: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::TableHead => {
|
||||
strong = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::TableRow => {
|
||||
let Scope::Table { rows, .. } = stack.last_mut()? else {
|
||||
return None;
|
||||
};
|
||||
|
||||
rows.push(Row { cells: Vec::new() });
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
pulldown_cmark::Event::End(tag) => match tag {
|
||||
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Heading(level, Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::Strong if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Heading(level) if !metadata => produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Heading(level, Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
),
|
||||
pulldown_cmark::TagEnd::Strong if !metadata => {
|
||||
strong = false;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Emphasis if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Emphasis if !metadata => {
|
||||
emphasis = false;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Strikethrough if !metadata => {
|
||||
strikethrough = false;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Link if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Link if !metadata => {
|
||||
link = None;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Paragraph if !metadata => {
|
||||
if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -715,7 +767,7 @@ fn parse_with<'a>(
|
|||
)
|
||||
}
|
||||
}
|
||||
pulldown_cmark::TagEnd::Item if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Item if !metadata => {
|
||||
if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -727,7 +779,7 @@ fn parse_with<'a>(
|
|||
)
|
||||
}
|
||||
}
|
||||
pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::List(_) if !metadata => {
|
||||
let scope = stack.pop()?;
|
||||
|
||||
let Scope::List(list) = scope else {
|
||||
|
|
@ -744,9 +796,7 @@ fn parse_with<'a>(
|
|||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::BlockQuote(_kind)
|
||||
if !metadata && !table =>
|
||||
{
|
||||
pulldown_cmark::TagEnd::BlockQuote(_kind) if !metadata => {
|
||||
let scope = stack.pop()?;
|
||||
|
||||
let Scope::Quote(quote) = scope else {
|
||||
|
|
@ -760,7 +810,7 @@ fn parse_with<'a>(
|
|||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::Image if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Image if !metadata => {
|
||||
let (url, title) = image.take()?;
|
||||
let alt = Text::new(spans.drain(..).collect());
|
||||
|
||||
|
|
@ -774,7 +824,7 @@ fn parse_with<'a>(
|
|||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::CodeBlock if !metadata => {
|
||||
code_block = false;
|
||||
|
||||
#[cfg(feature = "highlighter")]
|
||||
|
|
@ -798,12 +848,60 @@ fn parse_with<'a>(
|
|||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Table => {
|
||||
table = false;
|
||||
let scope = stack.pop()?;
|
||||
|
||||
let Scope::Table { columns, rows, .. } = scope else {
|
||||
return None;
|
||||
};
|
||||
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Table { columns, rows },
|
||||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::TableHead => {
|
||||
strong = false;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::TableCell => {
|
||||
if !spans.is_empty() {
|
||||
let _ = produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
);
|
||||
}
|
||||
|
||||
let Scope::Table {
|
||||
alignment,
|
||||
columns,
|
||||
rows,
|
||||
current,
|
||||
} = stack.last_mut()?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if columns.len() < alignment.len() {
|
||||
columns.push(Column {
|
||||
header: std::mem::take(current),
|
||||
alignment: alignment[columns.len()],
|
||||
});
|
||||
} else {
|
||||
rows.last_mut()
|
||||
.expect("table row")
|
||||
.cells
|
||||
.push(std::mem::take(current));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
pulldown_cmark::Event::Text(text) if !metadata && !table => {
|
||||
pulldown_cmark::Event::Text(text) if !metadata => {
|
||||
if code_block {
|
||||
code.push_str(&text);
|
||||
|
||||
|
|
@ -844,7 +942,7 @@ fn parse_with<'a>(
|
|||
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Event::Code(code) if !metadata && !table => {
|
||||
pulldown_cmark::Event::Code(code) if !metadata => {
|
||||
let span = Span::Standard {
|
||||
text: code.into_string(),
|
||||
strong,
|
||||
|
|
@ -857,7 +955,7 @@ fn parse_with<'a>(
|
|||
spans.push(span);
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Event::SoftBreak if !metadata && !table => {
|
||||
pulldown_cmark::Event::SoftBreak if !metadata => {
|
||||
spans.push(Span::Standard {
|
||||
text: String::from(" "),
|
||||
strikethrough,
|
||||
|
|
@ -868,7 +966,7 @@ fn parse_with<'a>(
|
|||
});
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Event::HardBreak if !metadata && !table => {
|
||||
pulldown_cmark::Event::HardBreak if !metadata => {
|
||||
spans.push(Span::Standard {
|
||||
text: String::from("\n"),
|
||||
strikethrough,
|
||||
|
|
@ -1113,6 +1211,7 @@ where
|
|||
} => viewer.ordered_list(settings, *start, items),
|
||||
Item::Quote(quote) => viewer.quote(settings, quote),
|
||||
Item::Rule => viewer.rule(settings),
|
||||
Item::Table { columns, rows } => viewer.table(settings, columns, rows),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1313,6 +1412,74 @@ where
|
|||
horizontal_rule(2).into()
|
||||
}
|
||||
|
||||
/// Displays a table using the default look.
|
||||
pub fn table<'a, Message, Theme, Renderer>(
|
||||
viewer: &impl Viewer<'a, Message, Theme, Renderer>,
|
||||
settings: Settings,
|
||||
columns: &'a [Column],
|
||||
rows: &'a [Row],
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
use crate::table;
|
||||
|
||||
table(
|
||||
columns.iter().enumerate().map(move |(i, column)| {
|
||||
table::column(
|
||||
items(viewer, settings, &column.header),
|
||||
move |row: &Row| {
|
||||
if let Some(cells) = row.cells.get(i) {
|
||||
items(viewer, settings, cells)
|
||||
} else {
|
||||
text("").into()
|
||||
}
|
||||
},
|
||||
)
|
||||
.align_x(match column.alignment {
|
||||
pulldown_cmark::Alignment::None
|
||||
| pulldown_cmark::Alignment::Left => {
|
||||
alignment::Horizontal::Left
|
||||
}
|
||||
pulldown_cmark::Alignment::Center => {
|
||||
alignment::Horizontal::Center
|
||||
}
|
||||
pulldown_cmark::Alignment::Right => {
|
||||
alignment::Horizontal::Right
|
||||
}
|
||||
})
|
||||
}),
|
||||
rows,
|
||||
)
|
||||
.padding_x(settings.spacing.0)
|
||||
.padding_y(settings.spacing.0 / 2.0)
|
||||
.separator_x(0)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Displays a column of items with the default look.
|
||||
pub fn items<'a, Message, Theme, Renderer>(
|
||||
viewer: &impl Viewer<'a, Message, Theme, Renderer>,
|
||||
settings: Settings,
|
||||
items: &'a [Item],
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
column(
|
||||
items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, content)| item(viewer, settings, content, i)),
|
||||
)
|
||||
.spacing(settings.spacing.0)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A view strategy to display a Markdown [`Item`].
|
||||
pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
where
|
||||
|
|
@ -1429,6 +1596,18 @@ where
|
|||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
rule()
|
||||
}
|
||||
|
||||
/// Displays a table.
|
||||
///
|
||||
/// By default, it calls [`table`].
|
||||
fn table(
|
||||
&self,
|
||||
settings: Settings,
|
||||
columns: &'a [Column],
|
||||
rows: &'a [Row],
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
table(self, settings, columns, rows)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
@ -1446,7 +1625,11 @@ where
|
|||
|
||||
/// The theme catalog of Markdown items.
|
||||
pub trait Catalog:
|
||||
container::Catalog + scrollable::Catalog + rule::Catalog + text::Catalog
|
||||
container::Catalog
|
||||
+ scrollable::Catalog
|
||||
+ rule::Catalog
|
||||
+ text::Catalog
|
||||
+ crate::table::Catalog
|
||||
{
|
||||
/// The styling class of a Markdown code block.
|
||||
fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
|
||||
|
|
|
|||
|
|
@ -136,23 +136,13 @@ where
|
|||
let child = child.into();
|
||||
let child_size = child.as_widget().size_hint();
|
||||
|
||||
self.width = self.width.enclose(child_size.width);
|
||||
self.height = self.height.enclose(child_size.height);
|
||||
|
||||
self.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an element to the [`Row`], if `Some`.
|
||||
pub fn push_maybe(
|
||||
self,
|
||||
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||
) -> Self {
|
||||
if let Some(child) = child {
|
||||
self.push(child)
|
||||
} else {
|
||||
self
|
||||
if !child_size.is_void() {
|
||||
self.width = self.width.enclose(child_size.width);
|
||||
self.height = self.height.enclose(child_size.height);
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Extends the [`Row`] with the given children.
|
||||
|
|
|
|||
646
widget/src/table.rs
Normal file
646
widget/src/table.rs
Normal file
|
|
@ -0,0 +1,646 @@
|
|||
//! Display tables.
|
||||
use crate::core;
|
||||
use crate::core::alignment;
|
||||
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,
|
||||
};
|
||||
|
||||
/// Creates a new [`Table`] with the given columns and rows.
|
||||
///
|
||||
/// Columns can be created using the [`column()`] function, while rows can be any
|
||||
/// iterator over some data type `T`.
|
||||
pub fn table<'a, 'b, T, Message, Theme, Renderer>(
|
||||
columns: impl IntoIterator<Item = Column<'a, 'b, T, Message, Theme, Renderer>>,
|
||||
rows: impl IntoIterator<Item = T>,
|
||||
) -> Table<'a, Message, Theme, Renderer>
|
||||
where
|
||||
T: Clone,
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
Table::new(columns, rows)
|
||||
}
|
||||
|
||||
/// Creates a new [`Column`] with the given header and view function.
|
||||
///
|
||||
/// The view function will be called for each row in a [`Table`] and it must
|
||||
/// produce the resulting contents of a cell.
|
||||
pub fn column<'a, 'b, T, E, Message, Theme, Renderer>(
|
||||
header: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
view: impl Fn(T) -> E + 'b,
|
||||
) -> Column<'a, 'b, 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// A grid-like visual representation of data distributed in columns and rows.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Table<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
columns: Vec<Column_>,
|
||||
cells: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding_x: f32,
|
||||
padding_y: f32,
|
||||
separator_x: f32,
|
||||
separator_y: f32,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
struct Column_ {
|
||||
width: Length,
|
||||
align_x: alignment::Horizontal,
|
||||
align_y: alignment::Vertical,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Table<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
/// Creates a new [`Table`] with the given columns and rows.
|
||||
///
|
||||
/// Columns can be created using the [`column()`] function, while rows can be any
|
||||
/// iterator over some data type `T`.
|
||||
pub fn new<'b, T>(
|
||||
columns: impl IntoIterator<
|
||||
Item = Column<'a, 'b, T, Message, Theme, Renderer>,
|
||||
>,
|
||||
rows: impl IntoIterator<Item = T>,
|
||||
) -> Self
|
||||
where
|
||||
T: 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, views): (Vec<_>, Vec<_>) = columns
|
||||
.map(|column| {
|
||||
width = width.enclose(column.width);
|
||||
|
||||
cells.push(column.header);
|
||||
|
||||
(
|
||||
Column_ {
|
||||
width: column.width,
|
||||
align_x: column.align_x,
|
||||
align_y: column.align_y,
|
||||
},
|
||||
column.view,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for row in rows {
|
||||
for view in &views {
|
||||
let cell = view(row.clone());
|
||||
let size_hint = cell.as_widget().size_hint();
|
||||
|
||||
height = height.enclose(size_hint.height);
|
||||
|
||||
cells.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
if width == Length::Shrink {
|
||||
if let Some(first) = columns.first_mut() {
|
||||
first.width = Length::Fill;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
columns,
|
||||
cells,
|
||||
width,
|
||||
height,
|
||||
padding_x: 10.0,
|
||||
padding_y: 5.0,
|
||||
separator_x: 1.0,
|
||||
separator_y: 1.0,
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Table`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the padding of the cells of the [`Table`].
|
||||
pub fn padding(self, padding: impl Into<Pixels>) -> Self {
|
||||
let padding = padding.into();
|
||||
|
||||
self.padding_x(padding).padding_y(padding)
|
||||
}
|
||||
|
||||
/// Sets the horizontal padding of the cells of the [`Table`].
|
||||
pub fn padding_x(mut self, padding: impl Into<Pixels>) -> Self {
|
||||
self.padding_x = padding.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the vertical padding of the cells of the [`Table`].
|
||||
pub fn padding_y(mut self, padding: impl Into<Pixels>) -> Self {
|
||||
self.padding_y = padding.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the thickness of the line separator between the cells of the [`Table`].
|
||||
pub fn separator(self, separator: impl Into<Pixels>) -> Self {
|
||||
let separator = separator.into();
|
||||
|
||||
self.separator_x(separator).separator_y(separator)
|
||||
}
|
||||
|
||||
/// Sets the thickness of the horizontal line separator between the cells of the [`Table`].
|
||||
pub fn separator_x(mut self, separator: impl Into<Pixels>) -> Self {
|
||||
self.separator_x = separator.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the thickness of the vertical line separator between the cells of the [`Table`].
|
||||
pub fn separator_y(mut self, separator: impl Into<Pixels>) -> Self {
|
||||
self.separator_y = separator.into().0;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Metrics {
|
||||
columns: Vec<f32>,
|
||||
rows: Vec<f32>,
|
||||
}
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
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 columns = self.columns.len();
|
||||
let rows = self.cells.len() / columns;
|
||||
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let available = limits.max();
|
||||
let table_fluid = self.width.fluid();
|
||||
|
||||
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];
|
||||
|
||||
let mut column_factors = vec![0; self.columns.len()];
|
||||
let mut total_row_factors = 0;
|
||||
let mut total_fluid_height = 0.0;
|
||||
let mut row_factor = 0;
|
||||
|
||||
let spacing_x = self.padding_x * 2.0 + self.separator_x;
|
||||
let spacing_y = self.padding_y * 2.0 + self.separator_y;
|
||||
|
||||
// FIRST PASS
|
||||
// Lay out non-fluid cells
|
||||
let mut x = self.padding_x;
|
||||
let mut y = self.padding_y;
|
||||
|
||||
for (i, (cell, state)) in
|
||||
self.cells.iter().zip(&mut tree.children).enumerate()
|
||||
{
|
||||
let row = i / columns;
|
||||
let column = i % columns;
|
||||
|
||||
let width = self.columns[column].width;
|
||||
let size = cell.as_widget().size();
|
||||
|
||||
if column == 0 {
|
||||
x = self.padding_x;
|
||||
|
||||
if row > 0 {
|
||||
y += metrics.rows[row - 1] + spacing_y;
|
||||
|
||||
if row_factor != 0 {
|
||||
total_fluid_height += metrics.rows[row - 1];
|
||||
total_row_factors += row_factor;
|
||||
|
||||
row_factor = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let width_factor = width.fill_factor();
|
||||
let height_factor = size.height.fill_factor();
|
||||
|
||||
if width_factor != 0 || height_factor != 0 || size.width.is_fill() {
|
||||
column_factors[column] =
|
||||
column_factors[column].max(width_factor);
|
||||
|
||||
row_factor = row_factor.max(height_factor);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let limits = layout::Limits::new(
|
||||
Size::ZERO,
|
||||
Size::new(available.width - x, available.height - y),
|
||||
)
|
||||
.width(width);
|
||||
|
||||
let layout = cell.as_widget().layout(state, renderer, &limits);
|
||||
let size = limits.resolve(width, 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;
|
||||
|
||||
x += size.width + spacing_x;
|
||||
}
|
||||
|
||||
// SECOND PASS
|
||||
// Lay out fluid cells, using metrics from the first pass as limits
|
||||
let left = Size::new(
|
||||
available.width
|
||||
- metrics
|
||||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| column_factors[*i] == 0)
|
||||
.map(|(_, width)| width)
|
||||
.sum::<f32>(),
|
||||
available.height - total_fluid_height,
|
||||
);
|
||||
|
||||
let width_unit = (left.width
|
||||
- spacing_x * self.columns.len().saturating_sub(1) as f32
|
||||
- self.padding_x * 2.0)
|
||||
/ 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)
|
||||
/ total_row_factors as f32;
|
||||
|
||||
let mut x = self.padding_x;
|
||||
let mut y = self.padding_y;
|
||||
|
||||
for (i, (cell, state)) in
|
||||
self.cells.iter().zip(&mut tree.children).enumerate()
|
||||
{
|
||||
let row = i / columns;
|
||||
let column = i % columns;
|
||||
|
||||
let size = cell.as_widget().size();
|
||||
|
||||
let width = self.columns[column].width;
|
||||
let width_factor = width.fill_factor();
|
||||
let height_factor = size.height.fill_factor();
|
||||
|
||||
if column == 0 {
|
||||
x = self.padding_x;
|
||||
|
||||
if row > 0 {
|
||||
y += metrics.rows[row - 1] + spacing_y;
|
||||
}
|
||||
}
|
||||
|
||||
if width_factor == 0
|
||||
&& size.width.fill_factor() == 0
|
||||
&& size.height.fill_factor() == 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let max_width = if width_factor == 0 {
|
||||
if size.width.is_fill() {
|
||||
metrics.columns[column]
|
||||
} else {
|
||||
(available.width - x).max(0.0)
|
||||
}
|
||||
} else {
|
||||
width_unit * width_factor as f32
|
||||
};
|
||||
|
||||
let max_height = if height_factor == 0 {
|
||||
if size.height.is_fill() {
|
||||
metrics.rows[row]
|
||||
} else {
|
||||
(available.height - y).max(0.0)
|
||||
}
|
||||
} else {
|
||||
height_unit * height_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;
|
||||
|
||||
x += size.width + spacing_x;
|
||||
}
|
||||
|
||||
// THIRD PASS
|
||||
// Position each cell
|
||||
let mut x = self.padding_x;
|
||||
let mut y = self.padding_y;
|
||||
|
||||
for (i, cell) in cells.iter_mut().enumerate() {
|
||||
let row = i / columns;
|
||||
let column = i % columns;
|
||||
|
||||
if column == 0 {
|
||||
x = self.padding_x;
|
||||
|
||||
if row > 0 {
|
||||
y += metrics.rows[row - 1] + spacing_y;
|
||||
}
|
||||
}
|
||||
|
||||
let Column_ {
|
||||
align_x, align_y, ..
|
||||
} = &self.columns[column];
|
||||
|
||||
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]),
|
||||
);
|
||||
|
||||
x += metrics.columns[column] + spacing_x;
|
||||
}
|
||||
|
||||
let intrinsic = limits.resolve(
|
||||
self.width,
|
||||
self.height,
|
||||
Size::new(
|
||||
x - spacing_x + self.padding_x,
|
||||
y + metrics
|
||||
.rows
|
||||
.last()
|
||||
.copied()
|
||||
.map(|height| height + self.padding_y)
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
for width in
|
||||
&metrics.columns[..metrics.columns.len().saturating_sub(1)]
|
||||
{
|
||||
x += width + self.padding_x;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if self.separator_y > 0.0 {
|
||||
let mut y = self.padding_y;
|
||||
|
||||
for height in &metrics.rows[..metrics.rows.len().saturating_sub(1)]
|
||||
{
|
||||
y += height + self.padding_y;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// A vertical visualization of some data with a header.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Column<
|
||||
'a,
|
||||
'b,
|
||||
T,
|
||||
Message,
|
||||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> {
|
||||
header: Element<'a, Message, Theme, Renderer>,
|
||||
view: Box<dyn Fn(T) -> Element<'a, Message, Theme, Renderer> + 'b>,
|
||||
width: Length,
|
||||
align_x: alignment::Horizontal,
|
||||
align_y: alignment::Vertical,
|
||||
}
|
||||
|
||||
impl<'a, 'b, T, Message, Theme, Renderer>
|
||||
Column<'a, 'b, T, Message, Theme, Renderer>
|
||||
{
|
||||
/// Sets the width of the [`Column`].
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of a [`Table`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The background color of the horizontal line separator between cells.
|
||||
pub separator_x: Background,
|
||||
/// The background color of the vertical line separator between cells.
|
||||
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();
|
||||
let separator = palette.background.strong.color.into();
|
||||
|
||||
Style {
|
||||
separator_x: separator,
|
||||
separator_y: separator,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue