use iced_core::{
Alignment, Animation, Clipboard, Element, Event, Layout, Length, Padding, Rectangle, Shell,
Size, Vector, Widget,
animation::Easing,
layout::{self, Limits, Node, flex::Axis},
mouse::{Cursor, Interaction},
overlay,
widget::{
Tree,
tree::{self, Tag},
},
window,
};
use std::{
borrow,
sync::atomic::{self, AtomicUsize},
time::Instant,
};
pub struct Expander<'a, Message, Theme = iced_core::Theme, Renderer = iced_widget::Renderer> {
header_content: [Element<'a, Message, Theme, Renderer>; 2],
id: Option<Id>,
width: Length,
height: Length,
direction: Direction,
is_expanded: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Id(IdInternal);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum IdInternal {
Unique(usize),
Str(borrow::Cow<'static, str>),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum Direction {
Left,
Up,
Right,
#[default]
Down,
}
impl<'a, Message, Theme, Renderer> Expander<'a, Message, Theme, Renderer> {
#[must_use]
pub fn new<H, C>(header: H, content: C, is_expanded: bool) -> Self
where
H: Into<Element<'a, Message, Theme, Renderer>>,
C: Into<Element<'a, Message, Theme, Renderer>>,
{
Self {
header_content: [header.into(), content.into()],
id: None,
width: Length::Shrink,
height: Length::Shrink,
direction: Direction::Down,
is_expanded,
}
}
#[must_use]
pub fn id(mut self, id: impl Into<Id>) -> Self {
self.id = Some(id.into());
self
}
#[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 direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
}
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
impl Id {
pub const fn new(id: &'static str) -> Self {
Self(IdInternal::Str(borrow::Cow::Borrowed(id)))
}
pub fn unique() -> Self {
Self(IdInternal::Unique(
NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed),
))
}
}
struct State {
id: Option<Id>,
now: Option<Instant>,
animation: Animation<bool>,
was_animating: bool,
open_factor: f32,
direction: Option<Direction>,
}
impl State {
fn animation(is_expanded: bool) -> Animation<bool> {
Animation::new(is_expanded)
.easing(Easing::EaseOutQuart)
.slow()
}
fn new(id: Option<Id>, is_expanded: bool) -> Self {
Self {
id,
now: None,
animation: State::animation(is_expanded),
was_animating: false,
open_factor: is_expanded.into(),
direction: None,
}
}
fn reset_animation(&mut self, is_expanded: bool) {
self.animation = State::animation(is_expanded);
}
}
fn is_fully_closed<'a, Message, Theme, Renderer>(
expander: &Expander<'a, Message, Theme, Renderer>,
state: &State,
) -> bool {
match state.now {
Some(now) => !expander.is_expanded && !state.animation.is_animating(now),
None => !expander.is_expanded,
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Expander<'_, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
fn size(&self) -> Size<Length> {
Size::new(self.width, self.height)
}
fn tag(&self) -> Tag {
Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new(self.id.clone(), self.is_expanded))
}
fn children(&self) -> Vec<Tree> {
self.header_content.iter().map(Tree::new).collect()
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>();
if self.id != state.id {
*state = State::new(self.id.clone(), self.is_expanded);
} else {
tree.diff_children(&self.header_content);
}
}
fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
let state = tree.state.downcast_ref::<State>();
if is_fully_closed(self, state) {
let header_limits = limits.width(self.width).height(self.height);
let header_node = self.header_content[0].as_widget_mut().layout(
&mut tree.children[0],
renderer,
&header_limits,
);
Node::with_children(header_node.size(), vec![header_node])
} else {
let axis = match self.direction {
Direction::Down | Direction::Up => Axis::Vertical,
Direction::Left | Direction::Right => Axis::Horizontal,
};
let target_node = layout::flex::resolve(
axis,
renderer,
limits,
self.width,
self.height,
Padding::ZERO,
0.0,
Alignment::Start,
&mut self.header_content,
&mut tree.children,
);
let child_nodes = target_node.children();
let mut header_node = child_nodes[0].clone();
let header_size = header_node.size();
let content_target_size = child_nodes[1].size();
let content_wrapper_size = match self.direction {
Direction::Left | Direction::Right => Size {
width: content_target_size.width * state.open_factor,
height: content_target_size.height * state.open_factor.ceil(),
},
Direction::Up | Direction::Down => Size {
width: content_target_size.width * state.open_factor.ceil(),
height: content_target_size.height * state.open_factor,
},
};
let content_limits = Limits::new(Size::ZERO, content_wrapper_size);
let mut content_node = self.header_content[1].as_widget_mut().layout(
&mut tree.children[1],
renderer,
&content_limits,
);
match self.direction {
Direction::Right => {
let dx = content_target_size.width - content_wrapper_size.width;
content_node.translate_mut([-dx, 0.0]);
}
Direction::Down => {
let dy = content_target_size.height - content_wrapper_size.height;
content_node.translate_mut([0.0, -dy]);
}
_ => {}
}
let mut content_wrapper_node =
Node::with_children(content_wrapper_size, vec![content_node]);
match self.direction {
Direction::Left => header_node.move_to_mut([content_wrapper_size.width, 0.0]),
Direction::Up => header_node.move_to_mut([0.0, content_wrapper_size.height]),
Direction::Right => content_wrapper_node.move_to_mut([header_size.width, 0.0]),
Direction::Down => content_wrapper_node.move_to_mut([0.0, header_size.height]),
}
let size = match self.direction {
Direction::Left | Direction::Right => Size::new(
header_size.width + content_wrapper_size.width,
header_size.height.max(content_wrapper_size.height),
),
Direction::Up | Direction::Down => Size::new(
header_size.width.max(content_wrapper_size.width),
header_size.height + content_wrapper_size.height,
),
};
Node::with_children(size, vec![header_node, content_wrapper_node])
}
}
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.header_content[0].as_widget_mut().operate(
&mut tree.children[0],
layout.child(0),
renderer,
operation,
);
if !is_fully_closed(self, tree.state.downcast_ref::<State>()) {
self.header_content[1].as_widget_mut().operate(
&mut tree.children[1],
layout.child(1).child(0),
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,
) {
self.header_content[0].as_widget_mut().update(
&mut tree.children[0],
event,
layout.child(0),
cursor,
renderer,
clipboard,
shell,
viewport,
);
if !is_fully_closed(self, tree.state.downcast_ref::<State>()) {
let wrapper_layout = layout.child(1);
self.header_content[1].as_widget_mut().update(
&mut tree.children[1],
event,
wrapper_layout.child(0),
cursor,
renderer,
clipboard,
shell,
&wrapper_layout.bounds(),
);
}
if shell.is_event_captured() {
return;
}
if let Event::Window(window::Event::RedrawRequested(now)) = event {
let state = tree.state.downcast_mut::<State>();
state.now = Some(*now);
if state.animation.value() != self.is_expanded {
state.direction = Some(self.direction);
state.animation.go_mut(self.is_expanded, *now);
state.open_factor = self.is_expanded.into();
}
if state.animation.is_animating(*now) {
if state
.direction
.is_some_and(|direction| self.direction != direction)
{
state.was_animating = false;
state.reset_animation(self.is_expanded);
} else {
state.was_animating = true;
shell.request_redraw();
}
let open_factor = state.animation.interpolate(0.0, 1.0, *now);
if open_factor != state.open_factor {
state.open_factor = open_factor;
shell.invalidate_layout();
}
} else if state.was_animating {
state.was_animating = false;
state.open_factor = self.is_expanded.into();
shell.request_redraw();
shell.invalidate_layout();
}
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced_core::renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
self.header_content[0].as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout.child(0),
cursor,
viewport,
);
if !is_fully_closed(self, tree.state.downcast_ref::<State>()) {
let wrapper_layout = layout.child(1);
if let Some(viewport) = wrapper_layout.bounds().intersection(viewport) {
self.header_content[1].as_widget().draw(
&tree.children[1],
renderer,
theme,
style,
wrapper_layout.child(0),
cursor,
&viewport,
);
}
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: iced_core::mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> Interaction {
let header_layout = layout.child(0);
let is_over_header = cursor.is_over(header_layout.bounds());
if is_over_header {
return self.header_content[0].as_widget().mouse_interaction(
&tree.children[0],
header_layout,
cursor,
viewport,
renderer,
);
}
if !is_fully_closed(self, tree.state.downcast_ref::<State>()) {
let content_wrapper_layout = layout.child(1);
let is_over_content = cursor.is_over(content_wrapper_layout.bounds());
if is_over_content {
return self.header_content[1].as_widget().mouse_interaction(
&tree.children[1],
content_wrapper_layout.child(0),
cursor,
&content_wrapper_layout.bounds(),
renderer,
);
}
}
Interaction::default()
}
fn overlay<'a>(
&'a mut self,
tree: &'a mut Tree,
layout: Layout<'a>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
let is_fully_closed = is_fully_closed(self, tree.state.downcast_ref::<State>());
let mut elements = self.header_content.iter_mut();
let mut children = tree.children.iter_mut();
let header_overlay = elements.next().unwrap().as_widget_mut().overlay(
children.next().unwrap(),
layout.child(0),
renderer,
viewport,
translation,
);
let content_overlay = if !is_fully_closed {
let wrapper_layout = layout.child(1);
elements.next().unwrap().as_widget_mut().overlay(
children.next().unwrap(),
wrapper_layout.child(0),
renderer,
&wrapper_layout.bounds(),
translation,
)
} else {
None
};
if header_overlay.is_some() || content_overlay.is_some() {
Some(
overlay::Group::with_children(
header_overlay.into_iter().chain(content_overlay).collect(),
)
.overlay(),
)
} else {
None
}
}
}
impl<'a, Message, Theme, Renderer> From<Expander<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: 'a + iced_core::Renderer,
{
fn from(expander: Expander<'a, Message, Theme, Renderer>) -> Self {
Element::new(expander)
}
}
impl From<&'static str> for Id {
fn from(value: &'static str) -> Self {
Self::new(value)
}
}
impl From<String> for Id {
fn from(value: String) -> Self {
Self(IdInternal::Str(borrow::Cow::Owned(value)))
}
}