1use iced_core::{
19 Alignment, Animation, Clipboard, Element, Event, Layout, Length, Padding, Rectangle, Shell,
20 Size, Vector, Widget,
21 animation::Easing,
22 layout::{self, Limits, Node, flex::Axis},
23 mouse::{Cursor, Interaction},
24 overlay,
25 widget::{
26 Tree,
27 tree::{self, Tag},
28 },
29 window,
30};
31
32use std::{
33 borrow,
34 sync::atomic::{self, AtomicUsize},
35 time::Instant,
36};
37
38pub struct Expander<'a, Message, Theme = iced_core::Theme, Renderer = iced_widget::Renderer> {
62 header_content: [Element<'a, Message, Theme, Renderer>; 2],
64 id: Option<Id>,
65 width: Length,
66 height: Length,
67 direction: Direction,
68 is_expanded: bool,
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Hash)]
73pub struct Id(IdInternal);
74
75#[derive(Clone, Debug, PartialEq, Eq, Hash)]
76enum IdInternal {
77 Unique(usize),
78 Str(borrow::Cow<'static, str>),
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
82pub enum Direction {
83 Left,
84 Up,
85 Right,
86 #[default]
87 Down,
88}
89
90impl<'a, Message, Theme, Renderer> Expander<'a, Message, Theme, Renderer> {
91 #[must_use]
93 pub fn new<H, C>(header: H, content: C, is_expanded: bool) -> Self
94 where
95 H: Into<Element<'a, Message, Theme, Renderer>>,
96 C: Into<Element<'a, Message, Theme, Renderer>>,
97 {
98 Self {
99 header_content: [header.into(), content.into()],
100 id: None,
101 width: Length::Shrink,
102 height: Length::Shrink,
103 direction: Direction::Down,
104 is_expanded,
105 }
106 }
107
108 #[must_use]
110 pub fn id(mut self, id: impl Into<Id>) -> Self {
111 self.id = Some(id.into());
112 self
113 }
114
115 #[must_use]
117 pub fn width(mut self, width: impl Into<Length>) -> Self {
118 self.width = width.into();
119 self
120 }
121
122 #[must_use]
124 pub fn height(mut self, height: impl Into<Length>) -> Self {
125 self.height = height.into();
126 self
127 }
128
129 #[must_use]
131 pub fn direction(mut self, direction: Direction) -> Self {
132 self.direction = direction;
133 self
134 }
135}
136
137static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
138
139impl Id {
140 pub const fn new(id: &'static str) -> Self {
142 Self(IdInternal::Str(borrow::Cow::Borrowed(id)))
143 }
144
145 pub fn unique() -> Self {
149 Self(IdInternal::Unique(
150 NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed),
151 ))
152 }
153}
154
155struct State {
156 id: Option<Id>,
157 now: Option<Instant>,
158 animation: Animation<bool>,
159 was_animating: bool,
160 open_factor: f32,
161 direction: Option<Direction>,
162}
163
164impl State {
165 fn animation(is_expanded: bool) -> Animation<bool> {
166 Animation::new(is_expanded)
167 .easing(Easing::EaseOutQuart)
168 .slow()
169 }
170
171 fn new(id: Option<Id>, is_expanded: bool) -> Self {
172 Self {
173 id,
174 now: None,
175 animation: State::animation(is_expanded),
176 was_animating: false,
177 open_factor: is_expanded.into(),
178 direction: None,
179 }
180 }
181
182 fn reset_animation(&mut self, is_expanded: bool) {
183 self.animation = State::animation(is_expanded);
184 }
185}
186
187fn is_fully_closed<'a, Message, Theme, Renderer>(
188 expander: &Expander<'a, Message, Theme, Renderer>,
189 state: &State,
190) -> bool {
191 match state.now {
192 Some(now) => !expander.is_expanded && !state.animation.is_animating(now),
193 None => !expander.is_expanded,
194 }
195}
196
197impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
198 for Expander<'_, Message, Theme, Renderer>
199where
200 Renderer: iced_core::Renderer,
201{
202 fn size(&self) -> Size<Length> {
203 Size::new(self.width, self.height)
204 }
205
206 fn tag(&self) -> Tag {
207 Tag::of::<State>()
208 }
209
210 fn state(&self) -> tree::State {
211 tree::State::new(State::new(self.id.clone(), self.is_expanded))
212 }
213
214 fn children(&self) -> Vec<Tree> {
215 self.header_content.iter().map(Tree::new).collect()
216 }
217
218 fn diff(&self, tree: &mut Tree) {
219 let state = tree.state.downcast_mut::<State>();
220
221 if self.id != state.id {
222 *state = State::new(self.id.clone(), self.is_expanded);
223 } else {
224 tree.diff_children(&self.header_content);
225 }
226 }
227
228 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
229 let state = tree.state.downcast_ref::<State>();
230
231 if is_fully_closed(self, state) {
232 let header_limits = limits.width(self.width).height(self.height);
233
234 let header_node = self.header_content[0].as_widget_mut().layout(
235 &mut tree.children[0],
236 renderer,
237 &header_limits,
238 );
239
240 Node::with_children(header_node.size(), vec![header_node])
241 } else {
242 let axis = match self.direction {
243 Direction::Down | Direction::Up => Axis::Vertical,
244 Direction::Left | Direction::Right => Axis::Horizontal,
245 };
246
247 let target_node = layout::flex::resolve(
249 axis,
250 renderer,
251 limits,
252 self.width,
253 self.height,
254 Padding::ZERO,
255 0.0,
256 Alignment::Start,
257 &mut self.header_content,
258 &mut tree.children,
259 );
260
261 let child_nodes = target_node.children();
262 let mut header_node = child_nodes[0].clone();
263 let header_size = header_node.size();
264 let content_target_size = child_nodes[1].size();
265
266 let content_wrapper_size = match self.direction {
267 Direction::Left | Direction::Right => Size {
268 width: content_target_size.width * state.open_factor,
269 height: content_target_size.height * state.open_factor.ceil(),
270 },
271 Direction::Up | Direction::Down => Size {
272 width: content_target_size.width * state.open_factor.ceil(),
273 height: content_target_size.height * state.open_factor,
274 },
275 };
276
277 let content_limits = Limits::new(Size::ZERO, content_wrapper_size);
278
279 let mut content_node = self.header_content[1].as_widget_mut().layout(
280 &mut tree.children[1],
281 renderer,
282 &content_limits,
283 );
284
285 match self.direction {
286 Direction::Right => {
287 let dx = content_target_size.width - content_wrapper_size.width;
288 content_node.translate_mut([-dx, 0.0]);
289 }
290 Direction::Down => {
291 let dy = content_target_size.height - content_wrapper_size.height;
292 content_node.translate_mut([0.0, -dy]);
293 }
294 _ => {}
295 }
296
297 let mut content_wrapper_node =
298 Node::with_children(content_wrapper_size, vec![content_node]);
299
300 match self.direction {
301 Direction::Left => header_node.move_to_mut([content_wrapper_size.width, 0.0]),
302 Direction::Up => header_node.move_to_mut([0.0, content_wrapper_size.height]),
303 Direction::Right => content_wrapper_node.move_to_mut([header_size.width, 0.0]),
304 Direction::Down => content_wrapper_node.move_to_mut([0.0, header_size.height]),
305 }
306
307 let size = match self.direction {
308 Direction::Left | Direction::Right => Size::new(
309 header_size.width + content_wrapper_size.width,
310 header_size.height.max(content_wrapper_size.height),
311 ),
312 Direction::Up | Direction::Down => Size::new(
313 header_size.width.max(content_wrapper_size.width),
314 header_size.height + content_wrapper_size.height,
315 ),
316 };
317
318 Node::with_children(size, vec![header_node, content_wrapper_node])
319 }
320 }
321
322 fn operate(
323 &mut self,
324 tree: &mut Tree,
325 layout: Layout<'_>,
326 renderer: &Renderer,
327 operation: &mut dyn iced_core::widget::Operation,
328 ) {
329 operation.container(None, layout.bounds());
330
331 operation.traverse(&mut |operation| {
332 self.header_content[0].as_widget_mut().operate(
334 &mut tree.children[0],
335 layout.child(0),
336 renderer,
337 operation,
338 );
339
340 if !is_fully_closed(self, tree.state.downcast_ref::<State>()) {
342 self.header_content[1].as_widget_mut().operate(
343 &mut tree.children[1],
344 layout.child(1).child(0),
345 renderer,
346 operation,
347 );
348 }
349 });
350 }
351
352 fn update(
353 &mut self,
354 tree: &mut Tree,
355 event: &Event,
356 layout: Layout<'_>,
357 cursor: Cursor,
358 renderer: &Renderer,
359 clipboard: &mut dyn Clipboard,
360 shell: &mut Shell<'_, Message>,
361 viewport: &Rectangle,
362 ) {
363 self.header_content[0].as_widget_mut().update(
365 &mut tree.children[0],
366 event,
367 layout.child(0),
368 cursor,
369 renderer,
370 clipboard,
371 shell,
372 viewport,
373 );
374
375 if !is_fully_closed(self, tree.state.downcast_ref::<State>()) {
377 let wrapper_layout = layout.child(1);
378
379 self.header_content[1].as_widget_mut().update(
380 &mut tree.children[1],
381 event,
382 wrapper_layout.child(0),
383 cursor,
384 renderer,
385 clipboard,
386 shell,
387 &wrapper_layout.bounds(),
388 );
389 }
390
391 if shell.is_event_captured() {
392 return;
393 }
394
395 if let Event::Window(window::Event::RedrawRequested(now)) = event {
396 let state = tree.state.downcast_mut::<State>();
397 state.now = Some(*now);
398
399 if state.animation.value() != self.is_expanded {
400 state.direction = Some(self.direction);
401 state.animation.go_mut(self.is_expanded, *now);
402 state.open_factor = self.is_expanded.into();
403 }
404
405 if state.animation.is_animating(*now) {
406 if state
408 .direction
409 .is_some_and(|direction| self.direction != direction)
410 {
411 state.was_animating = false;
412 state.reset_animation(self.is_expanded);
413 } else {
414 state.was_animating = true;
415 shell.request_redraw();
416 }
417
418 let open_factor = state.animation.interpolate(0.0, 1.0, *now);
420
421 if open_factor != state.open_factor {
422 state.open_factor = open_factor;
423 shell.invalidate_layout();
424 }
425 } else if state.was_animating {
426 state.was_animating = false;
427 state.open_factor = self.is_expanded.into();
428 shell.request_redraw();
429 shell.invalidate_layout();
430 }
431 }
432 }
433
434 fn draw(
435 &self,
436 tree: &Tree,
437 renderer: &mut Renderer,
438 theme: &Theme,
439 style: &iced_core::renderer::Style,
440 layout: Layout<'_>,
441 cursor: Cursor,
442 viewport: &Rectangle,
443 ) {
444 self.header_content[0].as_widget().draw(
446 &tree.children[0],
447 renderer,
448 theme,
449 style,
450 layout.child(0),
451 cursor,
452 viewport,
453 );
454
455 if !is_fully_closed(self, tree.state.downcast_ref::<State>()) {
457 let wrapper_layout = layout.child(1);
458
459 if let Some(viewport) = wrapper_layout.bounds().intersection(viewport) {
460 self.header_content[1].as_widget().draw(
461 &tree.children[1],
462 renderer,
463 theme,
464 style,
465 wrapper_layout.child(0),
466 cursor,
467 &viewport,
468 );
469 }
470 }
471 }
472
473 fn mouse_interaction(
474 &self,
475 tree: &Tree,
476 layout: Layout<'_>,
477 cursor: iced_core::mouse::Cursor,
478 viewport: &Rectangle,
479 renderer: &Renderer,
480 ) -> Interaction {
481 let header_layout = layout.child(0);
482 let is_over_header = cursor.is_over(header_layout.bounds());
483
484 if is_over_header {
485 return self.header_content[0].as_widget().mouse_interaction(
486 &tree.children[0],
487 header_layout,
488 cursor,
489 viewport,
490 renderer,
491 );
492 }
493
494 if !is_fully_closed(self, tree.state.downcast_ref::<State>()) {
496 let content_wrapper_layout = layout.child(1);
497 let is_over_content = cursor.is_over(content_wrapper_layout.bounds());
498
499 if is_over_content {
500 return self.header_content[1].as_widget().mouse_interaction(
501 &tree.children[1],
502 content_wrapper_layout.child(0),
503 cursor,
504 &content_wrapper_layout.bounds(),
505 renderer,
506 );
507 }
508 }
509
510 Interaction::default()
511 }
512
513 fn overlay<'a>(
514 &'a mut self,
515 tree: &'a mut Tree,
516 layout: Layout<'a>,
517 renderer: &Renderer,
518 viewport: &Rectangle,
519 translation: Vector,
520 ) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
521 let is_fully_closed = is_fully_closed(self, tree.state.downcast_ref::<State>());
522 let mut elements = self.header_content.iter_mut();
523 let mut children = tree.children.iter_mut();
524
525 let header_overlay = elements.next().unwrap().as_widget_mut().overlay(
526 children.next().unwrap(),
527 layout.child(0),
528 renderer,
529 viewport,
530 translation,
531 );
532
533 let content_overlay = if !is_fully_closed {
534 let wrapper_layout = layout.child(1);
535
536 elements.next().unwrap().as_widget_mut().overlay(
537 children.next().unwrap(),
538 wrapper_layout.child(0),
539 renderer,
540 &wrapper_layout.bounds(),
541 translation,
542 )
543 } else {
544 None
545 };
546
547 if header_overlay.is_some() || content_overlay.is_some() {
548 Some(
549 overlay::Group::with_children(
550 header_overlay.into_iter().chain(content_overlay).collect(),
551 )
552 .overlay(),
553 )
554 } else {
555 None
556 }
557 }
558}
559
560impl<'a, Message, Theme, Renderer> From<Expander<'a, Message, Theme, Renderer>>
561 for Element<'a, Message, Theme, Renderer>
562where
563 Message: 'a,
564 Theme: 'a,
565 Renderer: 'a + iced_core::Renderer,
566{
567 fn from(expander: Expander<'a, Message, Theme, Renderer>) -> Self {
568 Element::new(expander)
569 }
570}
571
572impl From<&'static str> for Id {
573 fn from(value: &'static str) -> Self {
574 Self::new(value)
575 }
576}
577
578impl From<String> for Id {
579 fn from(value: String) -> Self {
580 Self(IdInternal::Str(borrow::Cow::Owned(value)))
581 }
582}