use iced_core::{
Alignment, Animation, Clipboard, Element, Event, Font, Layout, Length, Pixels, Rectangle,
Shell, Size as IcedSize, Vector, Widget,
animation::Easing,
layout::{Limits, Node},
mouse::{Cursor, Interaction},
overlay,
text::{LineHeight, Wrapping},
widget::{
Tree,
tree::{self, Tag},
},
window,
};
use iced_palace::widget::{EllipsizedText, ellipsized_text};
use iced_widget::{Button, Svg, button, column, space::vertical, svg, text};
use std::{iter::once, time::Instant, vec};
const LAUNCHER_SIZE: IcedSize = IcedSize {
width: 16.0,
height: 16.0,
};
const LAUNCHER_ICON_SIZE: IcedSize = IcedSize {
width: 8.0,
height: 8.0,
};
const HEADER_SPACING: f32 = 2.0;
pub struct Group<'a, Id, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
where
Id: Clone + Eq,
Theme: button::Catalog + text::Catalog,
Renderer: iced_core::Renderer + iced_core::text::Renderer,
{
id: Id,
header: EllipsizedText<'a, Theme, Renderer>,
launcher: Button<'a, Message, Theme, Renderer>,
is_launcher_visible: bool,
collapsed_button: Option<Button<'a, Message, Theme, Renderer>>,
content: Vec<Option<Element<'a, Message, Theme, Renderer>>>,
size_widths: Vec<Option<f32>>,
is_dropdown_open: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum Size {
#[default]
Collapsed,
Small,
Medium,
Large,
}
impl Size {
fn smaller(&self) -> Self {
match self {
Size::Collapsed => Size::Collapsed,
Size::Small => Size::Collapsed,
Size::Medium => Size::Small,
Size::Large => Size::Medium,
}
}
pub(super) fn is_collapsed(&self) -> bool {
matches!(self, Self::Collapsed)
}
}
impl<'a, Id, Message, Theme, Renderer> Group<'a, Id, Message, Theme, Renderer>
where
Id: Clone + Eq,
Message: 'a + Clone,
Theme: 'a + 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,
{
#[must_use]
pub fn new(
id: Id,
header: impl text::IntoFragment<'a>,
content: impl Fn(Size) -> Option<Element<'a, Message, Theme, Renderer>> + 'a,
) -> Self
where
<Theme as svg::Catalog>::Class<'a>: From<svg::StyleFn<'a, Theme>>,
{
let content_fn = Box::new(content);
let mut content = [Size::Collapsed, Size::Small, Size::Medium, Size::Large]
.iter()
.map(|size| content_fn(*size))
.collect::<Vec<_>>();
assert!(
content.iter().any(Option::is_some),
"group should contain at least one element"
);
let header_fragment = header.into_fragment();
let header = ellipsized_text(header_fragment.clone()).wrapping(Wrapping::None);
let icon = text("⌄").size(20.0).align_x(Alignment::Center);
let collapsed_button = content.remove(0).map(|content| {
Button::new(column![content, vertical(), icon,].align_x(Alignment::Center))
.padding([8, 16])
});
let launcher_handle = svg::Handle::from_path(format!(
"{}/assets/image/ribbon_launcher.svg",
env!("CARGO_MANIFEST_DIR")
));
let launcher = Button::new(
Svg::new(launcher_handle)
.width(LAUNCHER_ICON_SIZE.width)
.height(LAUNCHER_ICON_SIZE.height),
)
.width(LAUNCHER_SIZE.width)
.height(LAUNCHER_SIZE.height)
.padding([
(LAUNCHER_SIZE.height - LAUNCHER_ICON_SIZE.height) / 2.0,
(LAUNCHER_SIZE.width - LAUNCHER_ICON_SIZE.width) / 2.0,
]);
Self {
id,
header,
launcher,
is_launcher_visible: false,
collapsed_button,
content,
size_widths: vec![],
is_dropdown_open: false,
}
}
pub fn header_size(mut self, size: impl Into<Pixels>) -> Self {
self.header = self.header.size(size);
self
}
pub fn header_line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
self.header = self.header.line_height(line_height);
self
}
pub fn header_font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.header = self.header.font(font);
self
}
pub fn header_wrapping(mut self, wrapping: Wrapping) -> Self {
self.header = self.header.wrapping(wrapping);
self
}
#[must_use]
pub fn on_launcher_press(mut self, on_press: Message) -> Self {
self.launcher = self.launcher.on_press(on_press);
self.is_launcher_visible = true;
self
}
#[must_use]
pub fn on_collapsed_press(mut self, on_press: Message) -> Self {
self.collapsed_button = self
.collapsed_button
.map(|button| button.on_press(on_press));
self
}
#[must_use]
pub fn on_collapsed_press_with(mut self, on_press: impl Fn() -> Message + 'a) -> Self {
self.collapsed_button = self
.collapsed_button
.map(|button| button.on_press_with(on_press));
self
}
#[must_use]
pub fn header_style(mut self, style: impl Fn(&Theme) -> text::Style + 'a) -> Self
where
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
{
self.header = self.header.style(style);
self
}
#[must_use]
pub fn launcher_style(
mut self,
style: impl Fn(&Theme, button::Status) -> button::Style + 'a,
) -> Self
where
<Theme as button::Catalog>::Class<'a>: From<button::StyleFn<'a, Theme>>,
{
self.launcher = self.launcher.style(style);
self
}
#[must_use]
pub fn collapsed_style(
mut self,
style: impl Fn(&Theme, button::Status) -> button::Style + 'a,
) -> Self
where
<Theme as button::Catalog>::Class<'a>: From<button::StyleFn<'a, Theme>>,
{
self.collapsed_button = self.collapsed_button.map(|button| button.style(style));
self
}
pub(super) fn id(&self) -> Id {
self.id.clone()
}
fn iced_sizes(&self) -> impl Iterator<Item = Option<IcedSize<Length>>> {
once(
self.collapsed_button
.as_ref()
.map(|button| button.size_hint()),
)
.chain(self.content.iter().map(|opt_element| {
opt_element
.as_ref()
.map(|element| element.as_widget().size_hint())
}))
}
fn init_size_widths(&mut self, tree: &mut Tree, renderer: &Renderer) {
if self.size_widths.is_empty() {
let mut trees = tree.children[2..].iter_mut();
let limits = &Limits::NONE;
self.size_widths = vec![self.collapsed_button.as_mut().map(|button| {
let node = button.layout(trees.next().unwrap(), renderer, limits);
node.size().width
})];
self.size_widths
.extend(self.content.iter_mut().map(|opt_element| {
opt_element.as_mut().map(|element| {
let widget = element.as_widget_mut();
if widget.size_hint().width.is_fill() {
panic!("expect non-fill width")
}
let tree = trees.next().unwrap();
let node = widget.layout(tree, renderer, limits);
node.size().width
})
}));
assert!(
self.size_widths
.iter()
.flatten()
.collect::<Vec<_>>()
.windows(2)
.all(|widths| widths[0] < widths[1]),
"group content widths must be in ascending order"
);
}
}
pub(super) fn size_width(&self, size: Size) -> Option<f32> {
self.size_widths[match size {
Size::Collapsed => 0,
Size::Small => 1,
Size::Medium => 2,
Size::Large => 3,
}]
}
fn content_sizes(&self) -> [Option<Size>; 4] {
[
self.content[2].as_ref().map(|_| Size::Large),
self.content[1].as_ref().map(|_| Size::Medium),
self.content[0].as_ref().map(|_| Size::Small),
self.collapsed_button.as_ref().map(|_| Size::Collapsed),
]
}
pub(super) fn maximum_size(&self) -> Size {
self.content_sizes()
.iter()
.find(|size| size.is_some())
.unwrap()
.unwrap()
}
pub(super) fn shrink_hint(&self, current_size: Size) -> Option<Size> {
let content_sizes = self.content_sizes();
let has_size = |size| content_sizes.contains(&Some(size));
let mut check_size = current_size;
while check_size != Size::Collapsed {
let smaller_size = check_size.smaller();
if has_size(smaller_size) {
return Some(smaller_size);
}
check_size = smaller_size;
}
None
}
pub(super) fn can_shrink(&self, current_size: Size) -> bool {
self.shrink_hint(current_size).is_some()
}
fn header_tree(&self) -> Tree {
Tree {
tag: <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::tag(&self.header),
state: <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::state(&self.header),
children: <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::children(&self.header),
}
}
fn content_state_index(&self, size: Size) -> usize {
self.content_sizes()
.iter()
.rev()
.fold(1, |mut idx, check_size| {
if let Some(check_size) = check_size
&& *check_size <= size
{
idx += 1;
}
idx
})
}
fn content_widget(&self, size: Size) -> &dyn Widget<Message, Theme, Renderer> {
match size {
Size::Collapsed => self.collapsed_button.as_ref().unwrap(),
Size::Small => self.content[0].as_ref().unwrap().as_widget(),
Size::Medium => self.content[1].as_ref().unwrap().as_widget(),
Size::Large => self.content[2].as_ref().unwrap().as_widget(),
}
}
fn content_widget_mut(&mut self, size: Size) -> &mut dyn Widget<Message, Theme, Renderer> {
match size {
Size::Collapsed => self.collapsed_button.as_mut().unwrap(),
Size::Small => self.content[0].as_mut().unwrap().as_widget_mut(),
Size::Medium => self.content[1].as_mut().unwrap().as_widget_mut(),
Size::Large => self.content[2].as_mut().unwrap().as_widget_mut(),
}
}
pub(super) fn is_dropdown_open(&self) -> bool {
self.is_dropdown_open
}
pub(super) fn open_dropdown(&mut self) {
self.is_dropdown_open = true;
}
pub(super) fn close_dropdown(&mut self) {
self.is_dropdown_open = false;
}
fn new_state(&self) -> State {
let max_size = if self.content[2].is_some() {
Size::Large
} else if self.content[1].is_some() {
Size::Medium
} else if self.content[0].is_some() {
Size::Small
} else if self.collapsed_button.is_some() {
Size::Collapsed
} else {
panic!()
};
State::new(max_size, self.iced_sizes().collect())
}
pub(super) fn tree(&self) -> Tree {
Tree {
tag: self.tag(),
state: self.state(),
children: self.children(),
}
}
pub(super) fn tag(&self) -> Tag {
Tag::of::<State>()
}
pub(super) fn state(&self) -> tree::State {
tree::State::new(self.new_state())
}
pub(super) fn children(&self) -> Vec<Tree> {
let mut children = vec![
self.header_tree(),
Tree {
tag: self.launcher.tag(),
state: self.launcher.state(),
children: self.launcher.children(),
},
];
if let Some(button) = &self.collapsed_button {
children.push(Tree {
tag: button.tag(),
state: button.state(),
children: button.children(),
});
}
children.extend(
self.content
.iter()
.flatten()
.map(|element| Tree::new(element.as_widget())),
);
children
}
pub(super) fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>();
if state
.content_sizes
.iter()
.zip(self.iced_sizes())
.any(|(a, b)| a != &b)
{
*state = self.new_state();
}
let child_count = 2 + self.content_sizes().iter().flatten().count();
if tree.children.len() > child_count {
tree.children.truncate(child_count);
}
if tree.children.is_empty() {
tree.children.push(self.header_tree());
} else {
<EllipsizedText<'_, _, _> as Widget<Message, _, _>>::diff(
&self.header,
&mut tree.children[0],
);
}
if tree.children.len() < 2 {
tree.children.push(Tree {
tag: self.launcher.tag(),
state: self.launcher.state(),
children: self.launcher.children(),
});
} else {
self.launcher.diff(&mut tree.children[1]);
}
let mut content_idx = 2;
if let Some(button) = &self.collapsed_button {
content_idx += 1;
if tree.children.len() < 3 {
tree.children.push(Tree {
tag: button.tag(),
state: button.state(),
children: button.children(),
});
} else {
button.diff(&mut tree.children[2]);
}
}
for (i, element) in self.content.iter().flatten().enumerate() {
let widget = element.as_widget();
let index = i + content_idx;
if tree.children.len() <= index {
let element_tree = Tree::new(widget);
tree.children.push(element_tree);
} else {
widget.diff(&mut tree.children[index]);
}
}
}
pub(super) fn size_layout(
&mut self,
size: Size,
compress_header: bool,
tree: &mut Tree,
renderer: &Renderer,
limits: &Limits,
) -> Node {
self.init_size_widths(tree, renderer);
let limits = limits.loose();
let header_node = <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::layout(
&mut self.header,
&mut tree.children[0],
renderer,
&limits,
);
let shrink_height = if size.is_collapsed() {
0.0
} else {
header_node.size().height + HEADER_SPACING
};
let content_limits = limits.shrink([0.0, shrink_height]);
let content_state_index = self.content_state_index(size);
let content_node = self.content_widget_mut(size).layout(
&mut tree.children[content_state_index],
renderer,
&content_limits,
);
let contents_size = content_node.size();
let mut launcher_node = self
.launcher
.layout(&mut tree.children[1], renderer, &limits);
let launcher_width = if self.is_launcher_visible {
launcher_node.size().width
} else {
0.0
};
let header_limits = limits
.shrink(IcedSize::new(0.0, contents_size.height))
.max_width(contents_size.width - launcher_width);
let mut header_node = <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::layout(
&mut self.header,
&mut tree.children[0],
renderer,
&header_limits,
);
let mut header_size = header_node.size();
let state = tree.state.downcast_mut::<State>();
if size.is_collapsed() {
header_size.height = 0.0;
}
let header_x = ((contents_size.width - header_size.width) / 2.0).max(0.0);
let header_y = if compress_header {
contents_size.height + HEADER_SPACING
} else {
(contents_size.height + HEADER_SPACING).max(limits.max().height - header_size.height)
};
header_node.move_to_mut([header_x, header_y]);
let width = match &state.width_status {
WidthStatus::Fixed { .. } => contents_size.width,
WidthStatus::Resizing {
from_size,
to_size,
animation,
now,
..
} => {
let from_width = self.size_width(*from_size).unwrap();
let to_width = self.size_width(*to_size).unwrap();
animation.interpolate(from_width, to_width, *now)
}
}
.round();
let group_size = IcedSize::new(
width,
contents_size.height + HEADER_SPACING + header_size.height,
);
let launcher_x = contents_size.width - LAUNCHER_SIZE.width;
let header_y_centre = header_y + header_size.height / 2.0;
let launcher_y = header_y_centre - LAUNCHER_SIZE.height / 2.0;
launcher_node.move_to_mut([launcher_x, launcher_y]);
let nodes = vec![header_node, launcher_node, content_node];
Node::with_children(group_size, nodes)
}
pub(super) fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
let size = current_size(tree);
self.size_layout(size, false, tree, renderer, limits)
}
pub(super) fn height_check_layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &Limits,
) -> Node {
let size = current_size(tree);
self.size_layout(size, true, tree, renderer, limits)
}
pub(super) 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| {
<EllipsizedText<'_, _, _> as Widget<Message, _, _>>::operate(
&mut self.header,
tree.children.get_mut(0).unwrap(),
layout.child(0),
renderer,
operation,
);
if self.is_launcher_visible {
self.launcher.operate(
tree.children.get_mut(1).unwrap(),
layout.child(1),
renderer,
operation,
);
}
let size = current_size(tree);
let content_state_index = self.content_state_index(size);
self.content_widget_mut(size).operate(
&mut tree.children[content_state_index],
layout.child(2),
renderer,
operation,
);
});
}
#[allow(clippy::too_many_arguments)]
pub(super) fn size_update(
&mut self,
size: Size,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.header.update(
tree.children.get_mut(0).unwrap(),
event,
layout.child(0),
cursor,
renderer,
clipboard,
shell,
viewport,
);
if self.is_launcher_visible {
self.launcher.update(
tree.children.get_mut(1).unwrap(),
event,
layout.child(1),
cursor,
renderer,
clipboard,
shell,
viewport,
);
if shell.is_event_captured() {
return;
}
}
let content_state_index = self.content_state_index(size);
self.content_widget_mut(size).update(
&mut tree.children[content_state_index],
event,
layout.child(2),
cursor,
renderer,
clipboard,
shell,
viewport,
);
if shell.is_event_captured() {
return;
}
let state = tree.state.downcast_mut::<State>();
if let Event::Window(window::Event::RedrawRequested(now)) = event {
match &mut state.width_status {
WidthStatus::Fixed { .. } => {}
WidthStatus::Resizing {
to_size,
animation,
now: animation_now,
last_progress,
..
} => {
*animation_now = *now;
if !animation.is_animating(*now) {
state.width_status = WidthStatus::new(*to_size);
shell.request_redraw();
} else {
let progress = animation.interpolate(0.0, 1.0, *now);
if let Some(last) = last_progress
&& *last != progress
{
*last_progress = Some(*last);
shell.invalidate_layout();
}
shell.request_redraw();
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub(super) 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,
) {
let size = current_size(tree);
self.size_update(
size, tree, event, layout, cursor, renderer, clipboard, shell, viewport,
);
}
#[allow(clippy::too_many_arguments)]
pub(super) fn size_draw(
&self,
size: Size,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced_core::renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
if !size.is_collapsed() {
<EllipsizedText<'_, _, _> as Widget<Message, _, _>>::draw(
&self.header,
&tree.children[0],
renderer,
theme,
style,
layout.child(0),
cursor,
viewport,
);
}
if self.is_launcher_visible {
self.launcher.draw(
&tree.children[1],
renderer,
theme,
style,
layout.child(1),
cursor,
viewport,
);
}
let state = tree.state.downcast_ref::<State>();
if !state.width_status.is_resizing() {
self.content_widget(size).draw(
&tree.children[self.content_state_index(size)],
renderer,
theme,
style,
layout.child(2),
cursor,
viewport,
);
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced_core::renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
let size = current_size(tree);
self.size_draw(size, tree, renderer, theme, style, layout, cursor, viewport);
}
pub(super) fn size_mouse_interaction(
&self,
size: Size,
tree: &Tree,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> Interaction {
let header_layout = layout.child(0);
let is_over_header = cursor.is_over(header_layout.bounds());
if !size.is_collapsed() && is_over_header {
return <EllipsizedText<'_, _, _> as Widget<Message, _, _>>::mouse_interaction(
&self.header,
&tree.children[0],
header_layout,
cursor,
viewport,
renderer,
);
}
let launcher_layout = layout.child(1);
let is_over_launcher = cursor.is_over(launcher_layout.bounds());
if self.is_launcher_visible && is_over_launcher {
return self.launcher.mouse_interaction(
&tree.children[1],
launcher_layout,
cursor,
viewport,
renderer,
);
}
let content_state_index = self.content_state_index(size);
self.content_widget(size).mouse_interaction(
&tree.children[content_state_index],
layout.child(2),
cursor,
viewport,
renderer,
)
}
pub(super) fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> Interaction {
let size = current_size(tree);
self.size_mouse_interaction(size, tree, layout, cursor, viewport, renderer)
}
pub(super) fn size_overlay<'b>(
&'b mut self,
size: Size,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let idx = self.content_state_index(size);
self.content_widget_mut(size).overlay(
&mut tree.children[idx],
layout.child(2),
renderer,
viewport,
translation,
)
}
pub(super) 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 size = current_size(tree);
self.size_overlay(size, tree, layout, renderer, viewport, translation)
}
}
pub(super) fn current_size(tree: &Tree) -> Size {
tree.state.downcast_ref::<State>().current_size()
}
pub(super) fn is_collapsed(tree: &Tree) -> bool {
tree.state.downcast_ref::<State>().is_collapsed()
}
pub(super) fn resize(tree: &mut Tree, target_size: Size, now: Instant) {
let width_status = &mut tree.state.downcast_mut::<State>().width_status;
let from_size = match width_status {
WidthStatus::Fixed { current_size: size } | WidthStatus::Resizing { to_size: size, .. } => {
size
}
};
if *from_size == target_size {
return;
}
*width_status = WidthStatus::Resizing {
from_size: *from_size,
to_size: target_size,
animation: Animation::new(false)
.quick()
.easing(Easing::EaseOutExpo)
.go(true, now),
now,
last_progress: None,
};
}
pub(super) fn resize_fixed(tree: &mut Tree, target_size: Size) {
let width_status = &mut tree.state.downcast_mut::<State>().width_status;
*width_status = WidthStatus::Fixed {
current_size: target_size,
}
}
#[derive(Debug)]
pub(super) struct State {
content_sizes: Vec<Option<IcedSize<Length>>>,
width_status: WidthStatus,
}
impl State {
fn new(current_size: Size, content_sizes: Vec<Option<IcedSize<Length>>>) -> Self {
State {
content_sizes,
width_status: WidthStatus::Fixed { current_size },
}
}
fn current_size(&self) -> Size {
match self.width_status {
WidthStatus::Fixed { current_size }
| WidthStatus::Resizing {
to_size: current_size,
..
} => current_size,
}
}
fn is_collapsed(&self) -> bool {
matches!(self.current_size(), Size::Collapsed)
}
pub(super) fn is_resizing(&self) -> bool {
self.width_status.is_resizing()
}
}
#[derive(Debug)]
enum WidthStatus {
Fixed {
current_size: Size,
},
Resizing {
from_size: Size,
to_size: Size,
animation: Animation<bool>,
now: Instant,
last_progress: Option<f32>,
},
}
impl WidthStatus {
fn new(current_size: Size) -> Self {
Self::Fixed { current_size }
}
fn is_resizing(&self) -> bool {
matches!(self, WidthStatus::Resizing { .. })
}
}