Draft new grid widget

This commit is contained in:
Héctor Ramón Jiménez 2025-04-10 02:49:32 +02:00
parent 193a340d6d
commit b89e78cd82
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
5 changed files with 388 additions and 27 deletions

View file

@ -7,7 +7,7 @@ publish = false
[dependencies]
iced.workspace = true
iced.features = ["tokio", "sipper", "image", "web-colors", "debug"]
iced.features = ["tokio", "sipper", "image", "debug"]
reqwest.version = "0.12"
reqwest.features = ["json"]

View file

@ -9,8 +9,8 @@ use crate::civitai::{Error, Id, Image, Rgba, Size};
use iced::animation;
use iced::time::{Instant, milliseconds};
use iced::widget::{
button, center_x, container, horizontal_space, image, mouse_area, opaque,
pop, row, scrollable, stack,
button, container, grid, horizontal_space, image, mouse_area, opaque, pop,
scrollable, stack,
};
use iced::window;
use iced::{
@ -175,19 +175,18 @@ impl Gallery {
}
pub fn view(&self) -> Element<'_, Message> {
let gallery = if self.images.is_empty() {
row((0..=Image::LIMIT).map(|_| placeholder()))
} else {
row(self.images.iter().map(|image| {
card(image, self.previews.get(&image.id), self.now)
}))
}
.spacing(10)
.wrap();
let images = self
.images
.iter()
.map(|image| card(image, self.previews.get(&image.id), self.now))
.chain((self.images.len()..=Image::LIMIT).map(|_| placeholder()));
let content =
container(scrollable(center_x(gallery)).spacing(10)).padding(10);
let gallery = grid(images)
.fluid(Preview::WIDTH)
.ratio(Preview::WIDTH as f32 / Preview::HEIGHT as f32)
.spacing(10);
let content = container(scrollable(gallery).spacing(10)).padding(10);
let viewer = self.viewer.view(self.now);
stack![content, viewer].into()
@ -204,7 +203,6 @@ fn card<'a>(
if let Preview::Ready { thumbnail, .. } = &preview {
image(&thumbnail.handle)
.width(Fill)
.height(Fill)
.content_fit(ContentFit::Cover)
.opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now))
.scale(thumbnail.zoom.interpolate(1.0, 1.1, now))
@ -216,7 +214,6 @@ fn card<'a>(
if let Some(blurhash) = preview.blurhash(now) {
let blurhash = image(&blurhash.handle)
.width(Fill)
.height(Fill)
.content_fit(ContentFit::Cover)
.opacity(blurhash.fade_in.interpolate(0.0, 1.0, now));
@ -228,14 +225,9 @@ fn card<'a>(
horizontal_space().into()
};
let card = mouse_area(
container(image)
.width(Preview::WIDTH)
.height(Preview::HEIGHT)
.style(container::dark),
)
.on_enter(Message::ThumbnailHovered(metadata.id, true))
.on_exit(Message::ThumbnailHovered(metadata.id, false));
let card = mouse_area(container(image).height(Fill).style(container::dark))
.on_enter(Message::ThumbnailHovered(metadata.id, true))
.on_exit(Message::ThumbnailHovered(metadata.id, false));
if let Some(preview) = preview {
let is_thumbnail = matches!(preview, Preview::Ready { .. });
@ -254,8 +246,7 @@ fn card<'a>(
fn placeholder<'a>() -> Element<'a, Message> {
container(horizontal_space())
.width(Preview::WIDTH)
.height(Preview::HEIGHT)
.height(Fill)
.style(container::dark)
.into()
}

357
widget/src/grid.rs Normal file
View file

@ -0,0 +1,357 @@
//! Distribute content on a grid.
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
Clipboard, Element, Event, Length, Pixels, Rectangle, Shell, Size, Vector,
Widget,
};
/// A container that distributes its contents on a responsive grid.
#[allow(missing_debug_implementations)]
pub struct Grid<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
spacing: f32,
columns: Constraint,
width: Option<Pixels>,
ratio: Option<Pixels>,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
enum Constraint {
MaxWidth(Pixels),
Amount(usize),
}
impl<'a, Message, Theme, Renderer> Grid<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
/// Creates an empty [`Grid`].
pub fn new() -> Self {
Self::from_vec(Vec::new())
}
/// Creates a [`Grid`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::from_vec(Vec::with_capacity(capacity))
}
/// Creates a [`Grid`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
let iterator = children.into_iter();
Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`Grid`] from an already allocated [`Vec`].
pub fn from_vec(
children: Vec<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
spacing: 0.0,
columns: Constraint::Amount(3),
width: None,
ratio: None,
children,
}
}
/// Sets the spacing _between_ cells in the [`Grid`].
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
/// Sets the width of the [`Grid`] in [`Pixels`].
///
/// By default, a [`Grid`] will [`Fill`] its parent.
///
/// [`Fill`]: Length::Fill
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = Some(width.into());
self
}
/// Sets the amount of columns in the [`Grid`].
pub fn columns(mut self, column: usize) -> Self {
self.columns = Constraint::Amount(column);
self
}
/// Makes the amount of columns dynamic in the [`Grid`], never
/// exceeding the provided `max_width`.
pub fn fluid(mut self, max_width: impl Into<Pixels>) -> Self {
self.columns = Constraint::MaxWidth(max_width.into());
self
}
/// Sets the amount of horizontal pixels per each vertical pixel of a cell in the [`Grid`].
pub fn ratio(mut self, ratio: impl Into<Pixels>) -> Self {
self.ratio = Some(ratio.into());
self
}
/// Adds an [`Element`] to the [`Grid`].
pub fn push(
mut self,
child: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
self.children.push(child.into());
self
}
/// Adds an element to the [`Grid`], 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
}
}
/// Extends the [`Grid`] with the given children.
pub fn extend(
self,
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
children.into_iter().fold(self, Self::push)
}
}
impl<Message, Renderer> Default for Grid<'_, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
FromIterator<Element<'a, Message, Theme, Renderer>>
for Grid<'a, Message, Theme, Renderer>
{
fn from_iter<
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
>(
iter: T,
) -> Self {
Self::with_children(iter)
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Grid<'_, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&self.children);
}
fn size(&self) -> Size<Length> {
Size {
width: self
.width
.map(|pixels| Length::Fixed(pixels.0))
.unwrap_or(Length::Fill),
height: Length::Shrink,
}
}
fn layout(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let size = self.size();
let limits = limits.width(size.width).height(size.height);
let available = limits.max();
let cells_per_row = match self.columns {
Constraint::MaxWidth(pixels) => (available.width
/ (pixels.0 + self.spacing / 2.0))
.ceil() as usize,
Constraint::Amount(amount) => amount,
};
let total_rows = self.children.len() / cells_per_row;
let cell_width = (available.width
- self.spacing * (cells_per_row - 1) as f32)
/ cells_per_row as f32;
let cell_height = if let Some(ratio) = self.ratio {
cell_width / ratio.0
} else if available.height.is_finite() {
available.height / total_rows as f32
} else {
f32::INFINITY
};
let cell_limits = layout::Limits::new(
Size::new(cell_width, 0.0),
Size::new(cell_width, cell_height),
);
let mut nodes = Vec::new();
let mut x = 0.0;
let mut y = 0.0;
for (i, (child, tree)) in
self.children.iter().zip(&mut tree.children).enumerate()
{
let node = child
.as_widget()
.layout(tree, renderer, &cell_limits)
.move_to((x, y));
x += node.size().width + self.spacing;
if (i + 1) % cells_per_row == 0 {
y += cell_height + self.spacing;
x = 0.0;
}
nodes.push(node);
}
if x == 0.0 {
y -= self.spacing;
} else {
y += cell_height;
}
layout::Node::with_children(Size::new(available.width, y), nodes)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
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 update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
for ((child, state), layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
{
child.as_widget_mut().update(
state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
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: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if let Some(viewport) = layout.bounds().intersection(viewport) {
for ((child, state), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().intersects(&viewport))
{
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,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Grid<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(row: Grid<'a, Message, Theme, Renderer>) -> Self {
Self::new(row)
}
}

View file

@ -24,7 +24,7 @@ use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip};
use crate::vertical_slider::{self, VerticalSlider};
use crate::{Column, MouseArea, Pin, Pop, Row, Space, Stack, Themer};
use crate::{Column, Grid, MouseArea, Pin, Pop, Row, Space, Stack, Themer};
use std::borrow::Borrow;
use std::ops::RangeInclusive;
@ -529,6 +529,16 @@ where
Row::with_children(children)
}
/// Creates a new [`Grid`] from an iterator.
pub fn grid<'a, Message, Theme, Renderer>(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Grid<'a, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
Grid::with_children(children)
}
/// Creates a new [`Stack`] with the given children.
///
/// [`Stack`]: crate::Stack

View file

@ -10,6 +10,7 @@ pub use iced_runtime::core;
mod action;
mod column;
mod grid;
mod mouse_area;
mod pin;
mod space;
@ -59,6 +60,8 @@ pub use combo_box::ComboBox;
#[doc(no_inline)]
pub use container::Container;
#[doc(no_inline)]
pub use grid::Grid;
#[doc(no_inline)]
pub use mouse_area::MouseArea;
#[doc(no_inline)]
pub use pane_grid::PaneGrid;