feat(widget): initial implementation of Grid widget
This commit is contained in:
parent
9ddadd330f
commit
e1d1b0bad5
5 changed files with 509 additions and 0 deletions
|
|
@ -116,6 +116,9 @@ optional = true
|
|||
version = "0.8"
|
||||
optional = true
|
||||
|
||||
[dependencies.taffy]
|
||||
git = "https://github.com/DioxusLabs/taffy"
|
||||
features = ["grid"]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
|
|
|||
162
src/widget/grid/layout.rs
Normal file
162
src/widget/grid/layout.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::widget::Assignment;
|
||||
use crate::{Element, Renderer};
|
||||
use iced_core::layout::{Limits, Node};
|
||||
use iced_core::{Alignment, Length, Padding, Point, Size};
|
||||
|
||||
use taffy::geometry::{Line, Rect};
|
||||
use taffy::style::{AlignContent, AlignItems, Display, GridPlacement, Style};
|
||||
use taffy::style_helpers::{auto, length};
|
||||
use taffy::Taffy;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn resolve<Message>(
|
||||
renderer: &Renderer,
|
||||
limits: &Limits,
|
||||
items: &[Element<'_, Message>],
|
||||
assignments: &[Assignment],
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
column_alignment: Alignment,
|
||||
row_alignment: Alignment,
|
||||
column_spacing: f32,
|
||||
row_spacing: f32,
|
||||
) -> Node {
|
||||
let max_size = limits.max();
|
||||
|
||||
let mut leafs = Vec::with_capacity(items.len());
|
||||
let mut nodes = Vec::with_capacity(items.len());
|
||||
|
||||
let mut taffy = Taffy::with_capacity(items.len() + 1);
|
||||
|
||||
// Attach widgets as child nodes.
|
||||
for (child, assignment) in items.iter().zip(assignments.iter()) {
|
||||
// Calculate the dimensdrawions of the item.
|
||||
let child_node = child.as_widget().layout(renderer, &limits);
|
||||
let size = child_node.size();
|
||||
|
||||
nodes.push(child_node);
|
||||
|
||||
// Attach widget as leaf to be later assigned to grid.
|
||||
let leaf = taffy.new_leaf(Style {
|
||||
grid_column: Line {
|
||||
start: GridPlacement::Line((assignment.column as i16).into()),
|
||||
end: GridPlacement::Span(assignment.height.into()),
|
||||
},
|
||||
grid_row: Line {
|
||||
start: GridPlacement::Line((assignment.row as i16).into()),
|
||||
end: GridPlacement::Span(assignment.width.into()),
|
||||
},
|
||||
size: taffy::geometry::Size {
|
||||
width: length(size.width),
|
||||
height: length(size.height),
|
||||
},
|
||||
..Style::default()
|
||||
});
|
||||
|
||||
match leaf {
|
||||
Ok(leaf) => leafs.push(leaf),
|
||||
Err(why) => {
|
||||
tracing::error!(%why, "cannot add leaf node to grid");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let root = taffy.new_with_children(
|
||||
Style {
|
||||
align_items: Some(match width {
|
||||
Length::Fill | Length::FillPortion(_) => AlignItems::Stretch,
|
||||
_ => match row_alignment {
|
||||
Alignment::Start => AlignItems::Start,
|
||||
Alignment::Center => AlignItems::Center,
|
||||
Alignment::End => AlignItems::End,
|
||||
},
|
||||
}),
|
||||
|
||||
display: Display::Grid,
|
||||
|
||||
gap: taffy::geometry::Size {
|
||||
width: length(row_spacing),
|
||||
height: length(column_spacing),
|
||||
},
|
||||
|
||||
justify_content: Some(match height {
|
||||
Length::Fill | Length::FillPortion(_) => AlignContent::Stretch,
|
||||
_ => match column_alignment {
|
||||
Alignment::Start => AlignContent::Start,
|
||||
Alignment::Center => AlignContent::Center,
|
||||
Alignment::End => AlignContent::End,
|
||||
},
|
||||
}),
|
||||
|
||||
padding: Rect {
|
||||
left: length(padding.left),
|
||||
right: length(padding.right),
|
||||
top: length(padding.top),
|
||||
bottom: length(padding.bottom),
|
||||
},
|
||||
|
||||
size: taffy::geometry::Size {
|
||||
width: match width {
|
||||
Length::Fixed(fixed) => length(fixed),
|
||||
_ => auto(),
|
||||
},
|
||||
height: match height {
|
||||
Length::Fixed(fixed) => length(fixed),
|
||||
_ => auto(),
|
||||
},
|
||||
},
|
||||
|
||||
..Style::default()
|
||||
},
|
||||
&leafs,
|
||||
);
|
||||
|
||||
let root = match root {
|
||||
Ok(root) => root,
|
||||
Err(why) => {
|
||||
tracing::error!(%why, "grid root style invalid");
|
||||
return Node::new(Size::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(why) = taffy.compute_layout(
|
||||
root,
|
||||
taffy::geometry::Size {
|
||||
width: length(max_size.width),
|
||||
height: length(max_size.height),
|
||||
},
|
||||
) {
|
||||
tracing::error!(%why, "grid layout did not compute");
|
||||
return Node::new(Size::ZERO);
|
||||
}
|
||||
|
||||
let grid_layout = match taffy.layout(root) {
|
||||
Ok(layout) => layout,
|
||||
Err(why) => {
|
||||
tracing::error!(%why, "cannot get layout of grid");
|
||||
return Node::new(Size::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
for (leaf, node) in leafs.into_iter().zip(nodes.iter_mut()) {
|
||||
if let Ok(leaf_layout) = taffy.layout(leaf) {
|
||||
let location = leaf_layout.location;
|
||||
node.move_to(Point {
|
||||
x: location.x,
|
||||
y: location.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let grid_size = Size {
|
||||
width: grid_layout.size.width,
|
||||
height: grid_layout.size.height,
|
||||
};
|
||||
|
||||
Node::with_children(grid_size.pad(padding), nodes)
|
||||
}
|
||||
12
src/widget/grid/mod.rs
Normal file
12
src/widget/grid/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
pub mod layout;
|
||||
pub mod widget;
|
||||
|
||||
pub use widget::{Grid, Item};
|
||||
|
||||
/// Responsively generates rows and columns of widgets based on its dimmensions.
|
||||
pub const fn grid<'a, Message>() -> Grid<'a, Message> {
|
||||
Grid::new()
|
||||
}
|
||||
329
src/widget/grid/widget.rs
Normal file
329
src/widget/grid/widget.rs
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use crate::{Element, Renderer};
|
||||
use derive_setters::Setters;
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::widget::{Operation, Tree};
|
||||
use iced_core::{
|
||||
layout, mouse, overlay, renderer, Alignment, Clipboard, Layout, Length, Padding, Rectangle,
|
||||
Shell, Widget,
|
||||
};
|
||||
use iced_renderer::core::widget::OperationOutputWrapper;
|
||||
|
||||
/// Responsively generates rows and columns of widgets based on its dimmensions.
|
||||
#[must_use]
|
||||
#[derive(Setters)]
|
||||
pub struct Grid<'a, Message> {
|
||||
#[setters(skip)]
|
||||
children: Vec<Element<'a, Message>>,
|
||||
/// Where children shall be assigned in the grid.
|
||||
#[setters(skip)]
|
||||
assignments: Vec<Assignment>,
|
||||
/// Sets the padding around the widget.
|
||||
padding: Padding,
|
||||
/// Alignment across columns
|
||||
column_alignment: Alignment,
|
||||
/// Alignment across rows
|
||||
row_alignment: Alignment,
|
||||
/// Sets the space between each column of items.
|
||||
column_spacing: u16,
|
||||
/// Sets the space between each item in a row.
|
||||
row_spacing: u16,
|
||||
/// Sets the width of the grid.
|
||||
width: Length,
|
||||
/// Sets the height of the grid.
|
||||
height: Length,
|
||||
/// Sets the max width
|
||||
max_width: f32,
|
||||
#[setters(skip)]
|
||||
column: u16,
|
||||
#[setters(skip)]
|
||||
row: u16,
|
||||
}
|
||||
|
||||
impl<'a, Message> Grid<'a, Message> {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
children: Vec::new(),
|
||||
assignments: Vec::new(),
|
||||
padding: Padding::ZERO,
|
||||
column_alignment: Alignment::Start,
|
||||
row_alignment: Alignment::Start,
|
||||
column_spacing: 4,
|
||||
row_spacing: 4,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
max_width: f32::INFINITY,
|
||||
column: 1,
|
||||
row: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a new element with a given grid assignment.
|
||||
pub fn push(mut self, widget: impl Into<Element<'a, Message>>) -> Self {
|
||||
self.children.push(widget.into());
|
||||
|
||||
self.assignments.push(Assignment {
|
||||
column: self.column,
|
||||
row: self.row,
|
||||
width: 1,
|
||||
height: 1,
|
||||
});
|
||||
|
||||
self.column += 1;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach a new element with custom properties
|
||||
pub fn push_with<W, S>(mut self, widget: W, setup: S) -> Self
|
||||
where
|
||||
W: Into<Element<'a, Message>>,
|
||||
S: Fn(Assignment) -> Assignment,
|
||||
{
|
||||
self.children.push(widget.into());
|
||||
|
||||
self.assignments.push(setup(Assignment {
|
||||
column: self.column,
|
||||
row: self.row,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}));
|
||||
|
||||
self.column += 1;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn insert_row(mut self) -> Self {
|
||||
self.row += 1;
|
||||
self.column = 1;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone> Widget<Message, Renderer> for Grid<'a, Message> {
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.diff_children(self.children.as_mut_slice());
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
||||
let limits = limits
|
||||
.max_width(self.max_width)
|
||||
.width(self.width())
|
||||
.height(self.height());
|
||||
|
||||
super::layout::resolve(
|
||||
renderer,
|
||||
&limits,
|
||||
&self.children,
|
||||
&self.assignments,
|
||||
self.width,
|
||||
self.height,
|
||||
self.padding,
|
||||
self.column_alignment,
|
||||
self.row_alignment,
|
||||
f32::from(self.column_spacing),
|
||||
f32::from(self.row_spacing),
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|((child, state), layout)| {
|
||||
child
|
||||
.as_widget()
|
||||
.operate(state, layout, renderer, operation);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child
|
||||
.as_widget()
|
||||
.mouse_interaction(state, layout, cursor, viewport, renderer)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &crate::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
for ((child, state), layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
child
|
||||
.as_widget()
|
||||
.draw(state, renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
overlay::from_children(&mut self.children, tree, layout, renderer)
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
/// get the a11y nodes for the widget
|
||||
fn a11y_nodes(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
state: &Tree,
|
||||
p: mouse::Cursor,
|
||||
) -> iced_accessibility::A11yTree {
|
||||
use iced_accessibility::A11yTree;
|
||||
A11yTree::join(
|
||||
self.children
|
||||
.iter()
|
||||
.zip(layout.children())
|
||||
.zip(state.children.iter())
|
||||
.map(|((c, c_layout), state)| c.as_widget().a11y_nodes(c_layout, state, p)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone> From<Grid<'a, Message>> for Element<'a, Message> {
|
||||
fn from(flex_row: Grid<'a, Message>) -> Self {
|
||||
Self::new(flex_row)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Setters)]
|
||||
#[must_use]
|
||||
pub struct Assignment {
|
||||
pub(super) column: u16,
|
||||
pub(super) row: u16,
|
||||
pub(super) width: u16,
|
||||
pub(super) height: u16,
|
||||
}
|
||||
|
||||
impl Assignment {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
column: 0,
|
||||
row: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Assignment {
|
||||
fn from((column, row): (u16, u16)) -> Self {
|
||||
Self {
|
||||
column,
|
||||
row,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16, u16, u16)> for Assignment {
|
||||
fn from((column, row, width, height): (u16, u16, u16, u16)) -> Self {
|
||||
Self {
|
||||
column,
|
||||
row,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub struct Item<'a, Message> {
|
||||
widget: Element<'a, Message>,
|
||||
assignment: Assignment,
|
||||
}
|
||||
|
||||
impl<'a, Message> Item<'a, Message> {
|
||||
pub fn width(mut self, width: u16) -> Self {
|
||||
self.assignment.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, height: u16) -> Self {
|
||||
self.assignment.height = height;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +106,9 @@ pub mod divider {
|
|||
pub mod flex_row;
|
||||
pub use flex_row::{flex_row, FlexRow};
|
||||
|
||||
pub mod grid;
|
||||
pub use grid::{grid, Grid};
|
||||
|
||||
mod header_bar;
|
||||
pub use header_bar::{header_bar, HeaderBar};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue