1pub mod item;
17
18pub use item::Item;
19
20use iced_core::{
21 Animation, Background, Border, Clipboard, Color, Element, Event, Gradient, Layout, Length,
22 Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
23 animation::Easing,
24 gradient::Linear,
25 layout::{self, Limits, Node},
26 mouse::{Cursor, Interaction},
27 overlay,
28 renderer::Quad,
29 widget::{
30 Tree,
31 tree::{self, Tag},
32 },
33 window,
34};
35use std::time::Instant;
36
37const INDICATOR_HEIGHT: f32 = 2.0;
38
39pub struct SelectorBar<'a, Id, Message, Theme = iced_core::Theme, Renderer = iced_widget::Renderer>
55where
56 Theme: Catalog + item::Catalog,
57{
58 width: Length,
59 height: Length,
60 padding: Padding,
61 items: Vec<Item<'a, Id, Message, Theme, Renderer>>,
62 selected_id: Id,
63 on_select: Box<dyn Fn(Id) -> Message + 'a>,
64 spacing: Pixels,
65 class: <Theme as Catalog>::Class<'a>,
66}
67
68impl<'a, Id, Message, Theme, Renderer> SelectorBar<'a, Id, Message, Theme, Renderer>
69where
70 Id: Eq,
71 Theme: Catalog + item::Catalog,
72{
73 pub fn new(
75 items: impl IntoIterator<Item = Item<'a, Id, Message, Theme, Renderer>>,
76 selected_id: Id,
77 on_select: impl Fn(Id) -> Message + 'a,
78 ) -> Self {
79 let items = items.into_iter().collect::<Vec<_>>();
80 assert!(
81 items.iter().any(|item| item.id == selected_id),
82 "selected item ID does not exist"
83 );
84
85 Self {
86 width: Length::Fill,
87 height: Length::Shrink,
88 padding: Padding::ZERO,
89 items,
90 selected_id,
91 on_select: Box::new(on_select),
92 spacing: 0.into(),
93 class: <Theme as Catalog>::default(),
94 }
95 }
96
97 #[must_use]
99 pub fn width(mut self, width: impl Into<Length>) -> Self {
100 self.width = width.into();
101 self
102 }
103
104 #[must_use]
106 pub fn height(mut self, height: impl Into<Length>) -> Self {
107 self.height = height.into();
108 self
109 }
110
111 #[must_use]
113 pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
114 self.spacing = amount.into();
115 self
116 }
117
118 #[must_use]
120 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
121 self.padding = padding.into();
122 self
123 }
124
125 #[must_use]
127 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
128 where
129 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
130 {
131 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
132 self
133 }
134}
135
136#[derive(Debug)]
137enum IndicatorStatus<Id> {
138 Fixed {
139 current: Id,
140 },
141 Widening {
142 current: Id,
143 animation: Animation<f32>,
144 },
145 Hovered {
146 current: Id,
147 },
148 Narrowing {
149 current: Id,
150 animation: Animation<f32>,
151 },
152 Moving {
153 from: Id,
154 to: Id,
155 animation: Animation<bool>,
156 },
157}
158
159impl<Id> IndicatorStatus<Id>
160where
161 Id: Clone + PartialEq,
162{
163 fn is_animating(&self, at: Instant) -> bool {
164 match self {
165 IndicatorStatus::Fixed { .. } => false,
166 IndicatorStatus::Widening { animation, .. } => animation.is_animating(at),
167 IndicatorStatus::Hovered { .. } => false,
168 IndicatorStatus::Narrowing { animation, .. } => animation.is_animating(at),
169 IndicatorStatus::Moving { animation, .. } => animation.is_animating(at),
170 }
171 }
172
173 fn current_id(&self) -> Id {
174 match self {
175 IndicatorStatus::Fixed { current }
176 | IndicatorStatus::Widening { current, .. }
177 | IndicatorStatus::Hovered { current }
178 | IndicatorStatus::Narrowing { current, .. }
179 | IndicatorStatus::Moving { to: current, .. } => current.clone(),
180 }
181 }
182
183 fn widen(&mut self, from: f32, at: Instant) {
184 *self = IndicatorStatus::Widening {
185 current: self.current_id(),
186 animation: Animation::new(from)
187 .slow()
188 .easing(Easing::EaseOutQuart)
189 .go(1.0, at),
190 };
191 }
192
193 fn narrow(&mut self, from: f32, at: Instant) {
194 *self = IndicatorStatus::Narrowing {
195 current: self.current_id(),
196 animation: Animation::new(from)
197 .slow()
198 .easing(Easing::EaseOutQuart)
199 .go(1.0, at),
200 };
201 }
202
203 fn move_to(&mut self, from: Id, to: Id, at: Instant) {
204 *self = IndicatorStatus::Moving {
205 from,
206 to,
207 animation: Animation::new(false)
208 .very_quick()
209 .easing(Easing::EaseOutQuart)
210 .go(true, at),
211 };
212 }
213}
214
215#[derive(Debug)]
216struct State<Id> {
217 now: Option<Instant>,
218 pressed_item: Option<Id>,
219 indicator_status: IndicatorStatus<Id>,
220}
221
222impl<Id> State<Id> {
223 fn new(current: Id) -> Self {
224 Self {
225 now: None,
226 pressed_item: None,
227 indicator_status: IndicatorStatus::Fixed { current },
228 }
229 }
230}
231
232impl<'a, Id, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
233 for SelectorBar<'a, Id, Message, Theme, Renderer>
234where
235 Id: 'static + Clone + Default + PartialEq,
236 Theme: Catalog + item::Catalog,
237 Renderer: iced_core::Renderer,
238{
239 fn tag(&self) -> Tag {
240 Tag::of::<State<Id>>()
241 }
242
243 fn state(&self) -> tree::State {
244 tree::State::new(State::<Id>::new(self.selected_id.clone()))
245 }
246
247 fn children(&self) -> Vec<Tree> {
248 self.items
249 .iter()
250 .map(|tab| Tree {
251 tag: tab.tag(),
252 state: tab.state(),
253 children: tab.children(),
254 })
255 .collect()
256 }
257
258 fn diff(&self, tree: &mut Tree) {
259 tree.diff_children_custom(
260 &self.items[..],
261 |tree, item| item.diff(tree),
262 |item| Tree {
263 tag: item.tag(),
264 state: item.state(),
265 children: item.children(),
266 },
267 );
268 }
269
270 fn size(&self) -> Size<Length> {
271 Size::new(self.width, self.height)
272 }
273
274 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
275 layout::padded(limits, self.width, self.height, self.padding, |limits| {
276 let (nodes, items_size) = self.items.iter_mut().zip(&mut tree.children).fold(
277 (vec![], Size::ZERO),
278 |(mut nodes, total_size), (tab, tree)| {
279 let node = tab
280 .layout(tree, renderer, limits)
281 .move_to([total_size.width, 0.0]);
282
283 let width = total_size.width + node.size().width + self.spacing.0;
284 let height = total_size.height.max(node.size().height);
285 let total_size = Size::new(width, height);
286
287 nodes.push(node);
288 (nodes, total_size)
289 },
290 );
291
292 Node::with_children(items_size, nodes)
293 })
294 }
295
296 fn operate(
297 &mut self,
298 tree: &mut Tree,
299 layout: Layout<'_>,
300 renderer: &Renderer,
301 operation: &mut dyn iced_core::widget::Operation,
302 ) {
303 operation.container(None, layout.bounds());
304
305 operation.traverse(&mut |operation| {
306 self.items
307 .iter_mut()
308 .zip(&mut tree.children)
309 .zip(layout.child(0).children())
310 .for_each(|((item, state), layout)| {
311 item.operate(state, layout, renderer, operation);
312 });
313 });
314 }
315
316 fn update(
317 &mut self,
318 tree: &mut Tree,
319 event: &Event,
320 layout: Layout<'_>,
321 cursor: Cursor,
322 renderer: &Renderer,
323 clipboard: &mut dyn Clipboard,
324 shell: &mut Shell<'_, Message>,
325 viewport: &Rectangle,
326 ) {
327 let state = tree.state.downcast_mut::<State<Id>>();
328
329 for ((item, tree), layout) in &mut self
330 .items
331 .iter_mut()
332 .zip(&mut tree.children)
333 .zip(layout.child(0).children())
334 {
335 item.update(
336 tree, event, layout, cursor, renderer, clipboard, shell, viewport,
337 );
338
339 if shell.is_event_captured() {
340 return;
341 }
342 }
343
344 if let Some(item) = self.items.iter().find(|item| item.is_pressed()) {
346 state.pressed_item = Some(item.id.clone());
347 } else if let Some(item) = self.items.iter().find(|item| item.is_hovered()) {
348 if state.pressed_item.as_ref().is_some_and(|id| *id == item.id) {
349 shell.publish((self.on_select)(item.id.clone()));
350 }
351
352 state.pressed_item = None;
353 } else {
354 state.pressed_item = None;
355 }
356
357 if !self
360 .items
361 .iter()
362 .any(|item| item.id == state.indicator_status.current_id())
363 {
364 state.indicator_status = IndicatorStatus::Fixed {
365 current: self.selected_id.clone(),
366 };
367
368 return;
369 }
370
371 if let Event::Window(window::Event::RedrawRequested(now)) = event {
372 let hovered_item = self.items.iter().find(|item| {
373 item.status
374 .is_some_and(|status| matches!(status, item::Status::Hovered))
375 });
376
377 let previous_id = state.indicator_status.current_id();
378
379 if previous_id != self.selected_id {
381 state
382 .indicator_status
383 .move_to(previous_id.clone(), self.selected_id.clone(), *now);
384 } else {
385 match &mut state.indicator_status {
386 IndicatorStatus::Fixed { current } => {
387 if hovered_item.is_some_and(|item| item.id == *current) {
388 state.indicator_status.widen(0.0, *now);
389 }
390 }
391 IndicatorStatus::Widening { current, animation } => {
392 if hovered_item.is_none_or(|item| item.id != *current) {
393 let value_now = animation.interpolate_with(|v| v, *now);
394 let from = 1.0 - value_now;
395 state.indicator_status.narrow(from, *now);
396 } else if !animation.is_animating(*now) {
397 state.indicator_status = IndicatorStatus::Hovered {
398 current: current.clone(),
399 };
400 }
401 }
402 IndicatorStatus::Hovered { current } => {
403 if hovered_item.is_none_or(|item| item.id != *current) {
404 state.indicator_status.narrow(0.0, *now);
405 }
406 }
407 IndicatorStatus::Narrowing { current, animation } => {
408 if hovered_item.is_some_and(|item| item.id == *current) {
409 let value_now = animation.interpolate_with(|v| v, *now);
410 let from = 1.0 - value_now;
411 state.indicator_status.widen(from, *now);
412 } else if !animation.is_animating(*now) {
413 state.indicator_status = IndicatorStatus::Fixed {
414 current: current.clone(),
415 };
416 }
417 }
418 IndicatorStatus::Moving { animation, .. } => {
419 if !animation.is_animating(*now) {
420 state.indicator_status.widen(0.0, *now);
421 }
422 }
423 }
424 }
425
426 if state.indicator_status.is_animating(*now) {
427 shell.request_redraw();
428 }
429
430 state.now = Some(*now);
431 }
432 }
433
434 fn mouse_interaction(
435 &self,
436 tree: &Tree,
437 layout: Layout<'_>,
438 cursor: Cursor,
439 viewport: &Rectangle,
440 renderer: &Renderer,
441 ) -> Interaction {
442 self.items
443 .iter()
444 .zip(&tree.children)
445 .zip(layout.children())
446 .map(|((item, tree), layout)| {
447 item.mouse_interaction(tree, layout, cursor, viewport, renderer)
448 })
449 .max()
450 .unwrap_or_default()
451 }
452
453 fn draw(
454 &self,
455 tree: &Tree,
456 renderer: &mut Renderer,
457 theme: &Theme,
458 style: &iced_core::renderer::Style,
459 layout: Layout<'_>,
460 cursor: Cursor,
461 viewport: &Rectangle,
462 ) {
463 let bounds = layout.bounds();
464 let selector_bar_style = <Theme as Catalog>::style(theme, &self.class);
465
466 renderer.fill_quad(
468 Quad {
469 bounds,
470 border: selector_bar_style.border,
471 shadow: selector_bar_style.shadow,
472 snap: selector_bar_style.snap,
473 },
474 selector_bar_style
475 .background
476 .unwrap_or(Color::TRANSPARENT.into()),
477 );
478
479 for ((item, tree), layout) in self
481 .items
482 .iter()
483 .zip(&tree.children)
484 .zip(layout.child(0).children())
485 {
486 item.draw(tree, renderer, theme, style, layout, cursor, viewport);
487 }
488
489 let item = |id| {
490 self.items
491 .iter()
492 .zip(layout.child(0).children())
493 .find(|(item, _)| item.id == id)
494 .unwrap()
495 };
496
497 let state = tree.state.downcast_ref::<State<Id>>();
499
500 let Some(now) = state.now else {
501 return;
502 };
503
504 let (selected_item, selected_layout) = item(self.selected_id.clone());
505 let padding = selected_item.padding;
506 let base_x = selected_layout.position().x;
507 let outer_width = selected_layout.bounds().width;
508 let inner_width = outer_width - padding.left - padding.right;
509
510 let (x, width) = match &state.indicator_status {
511 IndicatorStatus::Fixed { .. } => (base_x + padding.left, inner_width),
512 IndicatorStatus::Widening { animation, .. } => {
513 let value = animation.interpolate_with(|v| v, now);
514 let x = base_x + padding.left * (1.0 - value);
515 let width = inner_width + 2.0 * padding.left * value;
516 (x, width)
517 }
518 IndicatorStatus::Hovered { .. } => (base_x, inner_width + padding.left + padding.right),
519 IndicatorStatus::Narrowing { animation, .. } => {
520 let value = animation.interpolate_with(|v| v, now);
521 let x = base_x + padding.left * value;
522 let width = outer_width - 2.0 * padding.left * value;
523 (x, width)
524 }
525 IndicatorStatus::Moving {
526 from, animation, ..
527 } => {
528 let from_x = item(from.clone()).1.position().x;
529 let to_x = selected_layout.bounds().x;
530 let x = animation.interpolate(from_x, to_x, now);
531 (x, inner_width)
532 }
533 };
534
535 let bounds = Rectangle {
536 x,
537 y: bounds.y + bounds.height - self.padding.top - INDICATOR_HEIGHT,
538 width,
539 height: INDICATOR_HEIGHT,
540 };
541
542 let indicator_style = <Theme as item::Catalog>::style(
543 theme,
544 &selected_item.class,
545 selected_item.status.unwrap_or_default(),
546 )
547 .active_indicator;
548
549 renderer.fill_quad(
550 Quad {
551 bounds,
552 border: indicator_style.border,
553 shadow: indicator_style.shadow,
554 snap: indicator_style.snap,
555 },
556 indicator_style
557 .background
558 .unwrap_or(Color::TRANSPARENT.into()),
559 );
560 }
561
562 fn overlay<'b>(
563 &'b mut self,
564 tree: &'b mut Tree,
565 layout: Layout<'b>,
566 renderer: &Renderer,
567 viewport: &Rectangle,
568 translation: Vector,
569 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
570 let overlays = self
571 .items
572 .iter_mut()
573 .zip(&mut tree.children)
574 .zip(layout.child(0).children())
575 .flat_map(|((item, tree), layout)| {
576 item.overlay(tree, layout, renderer, viewport, translation)
577 })
578 .collect();
579
580 Some(overlay::Group::with_children(overlays).overlay())
581 }
582}
583
584impl<'a, Id, Message, Theme, Renderer> From<SelectorBar<'a, Id, Message, Theme, Renderer>>
585 for Element<'a, Message, Theme, Renderer>
586where
587 Id: 'static + Clone + std::fmt::Debug + Default + PartialEq,
588 Message: 'a,
589 Theme: 'a + Catalog + item::Catalog,
590 Renderer: 'a + iced_core::Renderer,
591{
592 fn from(bar: SelectorBar<'a, Id, Message, Theme, Renderer>) -> Self {
593 Element::new(bar)
594 }
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Default)]
599pub struct Style {
600 pub background: Option<Background>,
602 pub border: Border,
604 pub shadow: Shadow,
606 pub snap: bool,
608}
609
610impl Style {
611 pub fn border(self, border: impl Into<Border>) -> Self {
613 Self {
614 border: border.into(),
615 ..self
616 }
617 }
618
619 pub fn background(self, background: impl Into<Background>) -> Self {
621 Self {
622 background: Some(background.into()),
623 ..self
624 }
625 }
626
627 pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
629 Self {
630 shadow: shadow.into(),
631 ..self
632 }
633 }
634}
635
636impl From<Color> for Style {
637 fn from(color: Color) -> Self {
638 Self::default().background(color)
639 }
640}
641
642impl From<Gradient> for Style {
643 fn from(gradient: Gradient) -> Self {
644 Self::default().background(gradient)
645 }
646}
647
648impl From<Linear> for Style {
649 fn from(gradient: Linear) -> Self {
650 Self::default().background(gradient)
651 }
652}
653
654pub trait Catalog {
655 type Class<'a>;
656
657 fn default<'a>() -> Self::Class<'a>;
658 fn style(&self, class: &Self::Class<'_>) -> Style;
659}
660
661pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
662
663impl Catalog for Theme {
664 type Class<'a> = StyleFn<'a, Self>;
665
666 fn default<'a>() -> Self::Class<'a> {
667 Box::new(transparent)
668 }
669
670 fn style(&self, class: &Self::Class<'_>) -> Style {
671 class(self)
672 }
673}
674
675pub fn transparent(_theme: &Theme) -> Style {
676 Style {
677 background: None,
678 border: Border::default(),
679 shadow: Shadow::default(),
680 snap: true,
681 }
682}
683
684pub fn item<'a, Id, Message, Theme, Renderer>(
685 id: Id,
686 content: impl Into<Element<'a, Message, Theme, Renderer>>,
687) -> Item<'a, Id, Message, Theme, Renderer>
688where
689 Theme: item::Catalog,
690{
691 Item::new(id, content)
692}