pub mod group;
pub use group::{Group, Size};
use crate::ribbon::group::is_collapsed;
use iced_core::{
Background, Border, Clipboard, Color, Element, Event, Font, Gradient, Layout, Length, Padding,
Pixels, Point, Rectangle, Shadow, Shell, Size as IcedSize, Theme, Vector, Widget,
gradient::Linear,
keyboard,
layout::{self, Limits, Node},
mouse::{self, Cursor, Interaction},
overlay,
renderer::{self, Quad},
touch,
widget::{
Tree,
tree::{self, Tag},
},
window,
};
use core::panic;
use iced_widget::{button, svg, text};
use std::{f32, time::Instant};
const SEPARATOR_WIDTH: f32 = 1.0;
pub struct Ribbon<'a, Id, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
where
Id: Clone + Eq,
Theme: Catalog + button::Catalog + text::Catalog,
Renderer: iced_core::Renderer + iced_core::text::Renderer,
{
groups: Vec<Group<'a, Id, Message, Theme, Renderer>>,
width: Length,
height: Length,
padding: Padding,
spacing: f32,
on_group_dropdown_dismiss: Option<OnDismiss<'a, Message>>,
class: <Theme as Catalog>::Class<'a>,
}
impl<'a, Id, Message, Theme, Renderer> Ribbon<'a, Id, Message, Theme, Renderer>
where
Id: Clone + Eq,
Message: 'a + Clone,
Theme: 'a + Catalog + button::Catalog + svg::Catalog + text::Catalog,
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
Renderer: 'a + iced_core::Renderer + iced_core::svg::Renderer + iced_core::text::Renderer,
<Renderer as iced_core::text::Renderer>::Font: From<iced_core::Font>,
<Renderer as iced_core::text::Renderer>::Paragraph: Clone,
{
#[must_use]
pub fn new(groups: impl IntoIterator<Item = Group<'a, Id, Message, Theme, Renderer>>) -> Self {
Self {
groups: groups.into_iter().collect(),
width: Length::Fill,
height: Length::Shrink,
padding: Padding::new(4.0),
spacing: 0.0,
on_group_dropdown_dismiss: None,
class: <Theme as Catalog>::default(),
}
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
#[must_use]
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
#[must_use]
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
#[must_use]
pub fn open_group(mut self, id: Id) -> Self {
let mut is_found = false;
for group in &mut self.groups {
if group.id() == id {
is_found = true;
group.open_dropdown();
} else {
group.close_dropdown();
}
}
if !is_found {
panic!("expect group with id")
};
self
}
#[must_use]
pub fn on_group_dropdown_dismiss(mut self, on_dismiss: Message) -> Self {
self.on_group_dropdown_dismiss = Some(OnDismiss::Direct(on_dismiss));
self
}
#[must_use]
pub fn on_group_dropdown_dismiss_with(mut self, on_dismiss: impl Fn() -> Message + 'a) -> Self {
self.on_group_dropdown_dismiss = Some(OnDismiss::Closure(Box::new(on_dismiss)));
self
}
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
<Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
fn total_spacing(&self) -> f32 {
(self.groups.len() - 1) as f32 * self.spacing
}
fn can_shrink(&self, current_sizes: &[group::Size]) -> bool {
self.groups
.iter()
.zip(current_sizes)
.any(|(group, current_size)| group.shrink_hint(*current_size).is_some())
}
fn should_shrink(&mut self, available_width: f32, current_sizes: &[group::Size]) -> bool {
let groups_width = self
.groups
.iter()
.zip(current_sizes)
.fold(0.0, |total, (group, size)| {
total + group.size_width(*size).unwrap()
})
+ self.total_spacing()
+ self.padding.left
+ self.padding.right;
(available_width - groups_width) < 0.0 && self.can_shrink(current_sizes)
}
}
fn group_sizes(tree: &Tree) -> Vec<group::Size> {
tree.children.iter().map(group::current_size).collect()
}
enum OnDismiss<'a, Message> {
Direct(Message),
Closure(Box<dyn Fn() -> Message + 'a>),
}
impl<'a, Message: Clone> OnDismiss<'a, Message> {
fn get(&self) -> Message {
match self {
OnDismiss::Direct(msg) => msg.clone(),
OnDismiss::Closure(f) => f(),
}
}
}
#[derive(Debug, Default)]
struct State {
now: Option<Instant>,
open_collapsed_group: Option<usize>,
available_width: f32,
group_sizes: Vec<group::Size>,
}
impl<'a, Id, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Ribbon<'a, Id, Message, Theme, Renderer>
where
Id: Clone + Eq,
Message: 'a + Clone,
Theme: 'a + Catalog + button::Catalog + svg::Catalog + text::Catalog,
Renderer: 'a + iced_core::svg::Renderer + iced_core::text::Renderer,
<Renderer as iced_core::text::Renderer>::Font: From<iced_core::Font>,
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
<Renderer as iced_core::text::Renderer>::Paragraph: Clone,
{
fn tag(&self) -> Tag {
Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn children(&self) -> Vec<Tree> {
self.groups.iter().map(Group::tree).collect()
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children_custom(&self.groups, |tree, group| group.diff(tree), Group::tree);
}
fn size(&self) -> IcedSize<Length> {
IcedSize::new(self.width, self.height)
}
fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
let limits = limits.width(self.width).height(self.height);
let groups_max_height = self.groups.iter_mut().zip(&mut tree.children).fold(
0.0_f32,
|max_height, (group, tree)| {
group
.height_check_layout(tree, renderer, &limits)
.size()
.height
.max(max_height)
},
);
let previous_available_width = tree.state.downcast_ref::<State>().available_width;
let available_width = limits.shrink(self.padding).width(self.width).max().width;
let previous_sizes = &tree.state.downcast_ref::<State>().group_sizes;
if previous_available_width != available_width || *previous_sizes != group_sizes(tree) {
let is_group_animating = tree.children.iter().any(|child| {
let state = child.state.downcast_ref::<group::State>();
state.is_resizing()
});
if !is_group_animating {
let mut current_sizes = self
.groups
.iter()
.map(|group| group.maximum_size())
.collect::<Vec<_>>();
while self.should_shrink(available_width, ¤t_sizes) {
let max_size = self
.groups
.iter()
.zip(¤t_sizes)
.filter(|(group, current_size)| group.can_shrink(**current_size))
.fold(group::Size::Collapsed, |size, (_, current_size)| {
group::Size::max(size, *current_size)
});
for (group, current_size) in
self.groups.iter_mut().zip(current_sizes.iter_mut()).rev()
{
if let Some(size_hint) = group.shrink_hint(*current_size)
&& *current_size >= max_size
{
*current_size = size_hint;
break;
}
}
}
for (group_tree, size) in tree.children.iter_mut().zip(¤t_sizes) {
match tree.state.downcast_ref::<State>().now {
Some(now) => group::resize(group_tree, *size, now),
None => group::resize_fixed(group_tree, *size),
}
}
let group_sizes = group_sizes(tree);
let state = tree.state.downcast_mut::<State>();
state.available_width = available_width;
state.group_sizes = group_sizes;
}
}
let groups_limits =
limits.max_height(groups_max_height + self.padding.top + self.padding.bottom);
layout::padded(
&groups_limits,
self.width,
self.height,
self.padding,
|limits| {
let (size, nodes) = self.groups.iter_mut().zip(&mut tree.children).fold(
(IcedSize::ZERO, vec![]),
|(mut total_size, mut nodes), (group, group_tree)| {
let group_node = group
.layout(group_tree, renderer, limits)
.move_to([total_size.width, 0.0]);
let total_width = total_size.width + group_node.size().width;
nodes.push(group_node);
let separator_width = SEPARATOR_WIDTH + self.spacing;
let separator_node =
Node::new(IcedSize::new(separator_width, limits.max().height))
.move_to([total_width, 0.0]);
total_size =
IcedSize::new(total_width + separator_width, groups_max_height);
nodes.push(separator_node);
(total_size, nodes)
},
);
Node::with_children(size, nodes)
},
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.groups
.iter_mut()
.zip(&mut tree.children)
.zip(layout.child(0).children().step_by(2))
.for_each(|((group, state), layout)| {
group.operate(state, layout, renderer, operation);
});
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
for ((group, group_tree), layout) in self
.groups
.iter_mut()
.zip(&mut tree.children)
.zip(layout.child(0).children().step_by(2))
{
group.update(
group_tree, event, layout, cursor, renderer, clipboard, shell, viewport,
);
if shell.is_event_captured() {
return;
}
}
let state = tree.state.downcast_mut::<State>();
match event {
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
state.open_collapsed_group = None;
}
Event::Window(window::Event::RedrawRequested(now)) => {
state.now = Some(*now);
}
_ => {}
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
let ribbon_style = <Theme as Catalog>::style(theme, &self.class);
let groups_bounds = layout.bounds();
renderer.fill_quad(
Quad {
bounds: groups_bounds,
border: ribbon_style.border,
shadow: ribbon_style.shadow,
snap: ribbon_style.snap,
},
ribbon_style.background.unwrap_or(Color::TRANSPARENT.into()),
);
let mut layouts = layout.child(0).children();
let mut trees = tree.children.iter();
let mut next_group_trees = tree.children.iter().skip(1);
for group in self.groups.iter() {
let group_tree = trees.next().unwrap();
group.draw(
group_tree,
renderer,
theme,
style,
layouts.next().unwrap(),
cursor,
viewport,
);
let layout = layouts.next().unwrap();
let bounds = layout.bounds();
let is_group_collapsed = is_collapsed(group_tree);
let is_next_group_collapsed = next_group_trees.next().is_some_and(is_collapsed);
let x = match (is_group_collapsed, is_next_group_collapsed) {
(true, _) => bounds.x,
(_, true) => bounds.x + (bounds.width - 1.0),
(false, false) => bounds.x + (bounds.width - 1.0) / 2.0,
};
renderer.fill_quad(
Quad {
bounds: Rectangle {
x,
y: bounds.y,
width: 1.0,
height: bounds.height,
},
..Quad::default()
},
ribbon_style.separator_color.unwrap_or(Color::TRANSPARENT),
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> Interaction {
self.groups
.iter()
.zip(&tree.children)
.zip(layout.child(0).children().step_by(2))
.map(|((group, tree), layout)| {
group.mouse_interaction(tree, layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let overlays = self
.groups
.iter_mut()
.zip(&mut tree.children)
.zip(layout.child(0).children().step_by(2))
.flat_map(|((group, tree), layout)| {
if is_collapsed(tree) && group.is_dropdown_open() {
Some(overlay::Element::new(Box::new(GroupDropdown {
position: layout.position(),
tree,
group,
padding: self.padding,
on_dismiss: self.on_group_dropdown_dismiss.as_ref(),
class: &self.class,
})))
} else {
group.overlay(tree, layout, renderer, viewport, translation)
}
})
.collect();
Some(overlay::Group::with_children(overlays).overlay())
}
}
impl<'a, Id, Message, Theme, Renderer> From<Ribbon<'a, Id, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Id: 'a + Clone + Eq,
Message: 'a + Clone,
Theme: 'a + Catalog + button::Catalog + svg::Catalog + text::Catalog,
Renderer: 'a + iced_core::svg::Renderer + iced_core::text::Renderer,
<Renderer as iced_core::text::Renderer>::Font: From<Font>,
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
<Renderer as iced_core::text::Renderer>::Paragraph: Clone,
{
fn from(ribbon: Ribbon<'a, Id, Message, Theme, Renderer>) -> Self {
Element::new(ribbon)
}
}
struct GroupDropdown<
'a,
'b,
Id,
Message,
Theme = iced_core::Theme,
Renderer = iced_widget::Renderer,
> where
Id: Clone + Eq,
Theme: Catalog + button::Catalog + text::Catalog,
Renderer: iced_core::Renderer + iced_core::text::Renderer,
{
position: Point,
tree: &'b mut Tree,
group: &'b mut Group<'a, Id, Message, Theme, Renderer>,
padding: Padding,
on_dismiss: Option<&'b OnDismiss<'b, Message>>,
class: &'b <Theme as Catalog>::Class<'a>,
}
impl<'a, 'b, Id, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for GroupDropdown<'a, 'b, Id, Message, Theme, Renderer>
where
Id: Clone + Eq,
Message: 'a + Clone,
Theme: 'a + Catalog + button::Catalog + svg::Catalog + text::Catalog,
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
Renderer: 'a + iced_core::Renderer + iced_core::svg::Renderer + iced_core::text::Renderer,
<Renderer as iced_core::text::Renderer>::Font: From<Font>,
<Renderer as iced_core::text::Renderer>::Paragraph: Clone,
{
fn update(
&mut self,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
let bounds = layout.bounds();
let max_size = self.group.maximum_size();
self.group.size_update(
max_size,
self.tree,
event,
layout.child(0),
cursor,
renderer,
clipboard,
shell,
&bounds,
);
let mut publish_dismiss = || {
if let Some(on_dismiss) = self.on_dismiss {
shell.publish(on_dismiss.get());
}
};
if cursor.position_over(bounds).is_none() {
match event {
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Window(window::Event::Resized(_)) => publish_dismiss(),
Event::Keyboard(keyboard::Event::KeyPressed { key, .. })
if *key == keyboard::Key::Named(keyboard::key::Named::Escape) =>
{
publish_dismiss()
}
_ => {}
}
}
}
fn layout(&mut self, renderer: &Renderer, bounds: IcedSize) -> layout::Node {
let limits = Limits::new(IcedSize::ZERO, bounds);
let mut node = layout::padded(
&limits,
Length::Shrink,
Length::Shrink,
self.padding,
|limits| {
self.group
.size_layout(self.group.maximum_size(), true, self.tree, renderer, limits)
},
);
let size = node.size();
let mut position = Point::new(self.position.x, self.position.y + size.height);
if position.x + size.width > bounds.width {
position.x = f32::max(0.0, bounds.width - size.width);
}
if position.y + size.height > bounds.height {
position.y = f32::max(0.0, bounds.height - size.height);
}
node.move_to_mut(position);
node
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
) {
let bounds = layout.bounds();
let background_style = <Theme as Catalog>::style(theme, self.class);
renderer.fill_quad(
renderer::Quad {
bounds,
border: background_style.border,
shadow: background_style.shadow,
snap: background_style.snap,
},
background_style
.background
.unwrap_or(Color::TRANSPARENT.into()),
);
let max_size = self.group.maximum_size();
let layout = layout.child(0);
let bounds = layout.bounds();
self.group.size_draw(
max_size, self.tree, renderer, theme, style, layout, cursor, &bounds,
);
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
) -> Interaction {
self.group.size_mouse_interaction(
self.group.maximum_size(),
self.tree,
layout.child(0),
cursor,
&layout.bounds(),
renderer,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Style {
pub background: Option<Background>,
pub border: Border,
pub shadow: Shadow,
pub separator_color: Option<Color>,
pub snap: bool,
}
impl Style {
pub fn border(self, border: impl Into<Border>) -> Self {
Self {
border: border.into(),
..self
}
}
pub fn background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
..self
}
}
pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
Self {
shadow: shadow.into(),
..self
}
}
pub fn separator_color(self, color: Color) -> Self {
Self {
separator_color: Some(color),
..self
}
}
}
impl From<Color> for Style {
fn from(color: Color) -> Self {
Self::default().background(color)
}
}
impl From<Gradient> for Style {
fn from(gradient: Gradient) -> Self {
Self::default().background(gradient)
}
}
impl From<Linear> for Style {
fn from(gradient: Linear) -> Self {
Self::default().background(gradient)
}
}
pub trait Catalog {
type Class<'a>;
fn default<'a>() -> Self::Class<'a>;
fn style(&self, class: &Self::Class<'_>) -> Style;
}
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 Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(|_| Style::default())
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
pub fn group<'a, Id, Message, Theme, Renderer>(
id: Id,
header: impl text::IntoFragment<'a>,
content: impl Fn(Size) -> Option<Element<'a, Message, Theme, Renderer>> + 'a,
) -> Group<'a, Id, Message, Theme, Renderer>
where
Id: Clone + Eq,
Message: 'a + Clone,
Theme: 'a + button::Catalog + svg::Catalog + text::Catalog,
<Theme as svg::Catalog>::Class<'a>: From<svg::StyleFn<'a, Theme>>,
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
Renderer: 'a + iced_core::Renderer + iced_core::svg::Renderer + iced_core::text::Renderer,
<Renderer as iced_core::text::Renderer>::Font: From<Font>,
<Renderer as iced_core::text::Renderer>::Paragraph: Clone,
{
Group::new(id, header, content)
}