pub mod item;
pub use item::Item;
use iced_core::{
Animation, Background, Border, Clipboard, Color, Element, Event, Gradient, Layout, Length,
Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
animation::Easing,
gradient::Linear,
layout::{self, Limits, Node},
mouse::{Cursor, Interaction},
overlay,
renderer::Quad,
widget::{
Tree,
tree::{self, Tag},
},
window,
};
use std::time::Instant;
const INDICATOR_HEIGHT: f32 = 2.0;
pub struct SelectorBar<'a, Id, Message, Theme = iced_core::Theme, Renderer = iced_widget::Renderer>
where
Theme: Catalog + item::Catalog,
{
width: Length,
height: Length,
padding: Padding,
items: Vec<Item<'a, Id, Message, Theme, Renderer>>,
selected_id: Id,
on_select: Box<dyn Fn(Id) -> Message + 'a>,
spacing: Pixels,
class: <Theme as Catalog>::Class<'a>,
}
impl<'a, Id, Message, Theme, Renderer> SelectorBar<'a, Id, Message, Theme, Renderer>
where
Id: Eq,
Theme: Catalog + item::Catalog,
{
pub fn new(
items: impl IntoIterator<Item = Item<'a, Id, Message, Theme, Renderer>>,
selected_id: Id,
on_select: impl Fn(Id) -> Message + 'a,
) -> Self {
let items = items.into_iter().collect::<Vec<_>>();
assert!(
items.iter().any(|item| item.id == selected_id),
"selected item ID does not exist"
);
Self {
width: Length::Fill,
height: Length::Shrink,
padding: Padding::ZERO,
items,
selected_id,
on_select: Box::new(on_select),
spacing: 0.into(),
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();
self
}
#[must_use]
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
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
}
}
#[derive(Debug)]
enum IndicatorStatus<Id> {
Fixed {
current: Id,
},
Widening {
current: Id,
animation: Animation<f32>,
},
Hovered {
current: Id,
},
Narrowing {
current: Id,
animation: Animation<f32>,
},
Moving {
from: Id,
to: Id,
animation: Animation<bool>,
},
}
impl<Id> IndicatorStatus<Id>
where
Id: Clone + PartialEq,
{
fn is_animating(&self, at: Instant) -> bool {
match self {
IndicatorStatus::Fixed { .. } => false,
IndicatorStatus::Widening { animation, .. } => animation.is_animating(at),
IndicatorStatus::Hovered { .. } => false,
IndicatorStatus::Narrowing { animation, .. } => animation.is_animating(at),
IndicatorStatus::Moving { animation, .. } => animation.is_animating(at),
}
}
fn current_id(&self) -> Id {
match self {
IndicatorStatus::Fixed { current }
| IndicatorStatus::Widening { current, .. }
| IndicatorStatus::Hovered { current }
| IndicatorStatus::Narrowing { current, .. }
| IndicatorStatus::Moving { to: current, .. } => current.clone(),
}
}
fn widen(&mut self, from: f32, at: Instant) {
*self = IndicatorStatus::Widening {
current: self.current_id(),
animation: Animation::new(from)
.slow()
.easing(Easing::EaseOutQuart)
.go(1.0, at),
};
}
fn narrow(&mut self, from: f32, at: Instant) {
*self = IndicatorStatus::Narrowing {
current: self.current_id(),
animation: Animation::new(from)
.slow()
.easing(Easing::EaseOutQuart)
.go(1.0, at),
};
}
fn move_to(&mut self, from: Id, to: Id, at: Instant) {
*self = IndicatorStatus::Moving {
from,
to,
animation: Animation::new(false)
.very_quick()
.easing(Easing::EaseOutQuart)
.go(true, at),
};
}
}
#[derive(Debug)]
struct State<Id> {
now: Option<Instant>,
pressed_item: Option<Id>,
indicator_status: IndicatorStatus<Id>,
}
impl<Id> State<Id> {
fn new(current: Id) -> Self {
Self {
now: None,
pressed_item: None,
indicator_status: IndicatorStatus::Fixed { current },
}
}
}
impl<'a, Id, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for SelectorBar<'a, Id, Message, Theme, Renderer>
where
Id: 'static + Clone + Default + PartialEq,
Theme: Catalog + item::Catalog,
Renderer: iced_core::Renderer,
{
fn tag(&self) -> Tag {
Tag::of::<State<Id>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::<Id>::new(self.selected_id.clone()))
}
fn children(&self) -> Vec<Tree> {
self.items
.iter()
.map(|tab| Tree {
tag: tab.tag(),
state: tab.state(),
children: tab.children(),
})
.collect()
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children_custom(
&self.items[..],
|tree, item| item.diff(tree),
|item| Tree {
tag: item.tag(),
state: item.state(),
children: item.children(),
},
);
}
fn size(&self) -> Size<Length> {
Size::new(self.width, self.height)
}
fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
layout::padded(limits, self.width, self.height, self.padding, |limits| {
let (nodes, items_size) = self.items.iter_mut().zip(&mut tree.children).fold(
(vec![], Size::ZERO),
|(mut nodes, total_size), (tab, tree)| {
let node = tab
.layout(tree, renderer, limits)
.move_to([total_size.width, 0.0]);
let width = total_size.width + node.size().width + self.spacing.0;
let height = total_size.height.max(node.size().height);
let total_size = Size::new(width, height);
nodes.push(node);
(nodes, total_size)
},
);
Node::with_children(items_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.items
.iter_mut()
.zip(&mut tree.children)
.zip(layout.child(0).children())
.for_each(|((item, state), layout)| {
item.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,
) {
let state = tree.state.downcast_mut::<State<Id>>();
for ((item, tree), layout) in &mut self
.items
.iter_mut()
.zip(&mut tree.children)
.zip(layout.child(0).children())
{
item.update(
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
);
if shell.is_event_captured() {
return;
}
}
if let Some(item) = self.items.iter().find(|item| item.is_pressed()) {
state.pressed_item = Some(item.id.clone());
} else if let Some(item) = self.items.iter().find(|item| item.is_hovered()) {
if state.pressed_item.as_ref().is_some_and(|id| *id == item.id) {
shell.publish((self.on_select)(item.id.clone()));
}
state.pressed_item = None;
} else {
state.pressed_item = None;
}
if !self
.items
.iter()
.any(|item| item.id == state.indicator_status.current_id())
{
state.indicator_status = IndicatorStatus::Fixed {
current: self.selected_id.clone(),
};
return;
}
if let Event::Window(window::Event::RedrawRequested(now)) = event {
let hovered_item = self.items.iter().find(|item| {
item.status
.is_some_and(|status| matches!(status, item::Status::Hovered))
});
let previous_id = state.indicator_status.current_id();
if previous_id != self.selected_id {
state
.indicator_status
.move_to(previous_id.clone(), self.selected_id.clone(), *now);
} else {
match &mut state.indicator_status {
IndicatorStatus::Fixed { current } => {
if hovered_item.is_some_and(|item| item.id == *current) {
state.indicator_status.widen(0.0, *now);
}
}
IndicatorStatus::Widening { current, animation } => {
if hovered_item.is_none_or(|item| item.id != *current) {
let value_now = animation.interpolate_with(|v| v, *now);
let from = 1.0 - value_now;
state.indicator_status.narrow(from, *now);
} else if !animation.is_animating(*now) {
state.indicator_status = IndicatorStatus::Hovered {
current: current.clone(),
};
}
}
IndicatorStatus::Hovered { current } => {
if hovered_item.is_none_or(|item| item.id != *current) {
state.indicator_status.narrow(0.0, *now);
}
}
IndicatorStatus::Narrowing { current, animation } => {
if hovered_item.is_some_and(|item| item.id == *current) {
let value_now = animation.interpolate_with(|v| v, *now);
let from = 1.0 - value_now;
state.indicator_status.widen(from, *now);
} else if !animation.is_animating(*now) {
state.indicator_status = IndicatorStatus::Fixed {
current: current.clone(),
};
}
}
IndicatorStatus::Moving { animation, .. } => {
if !animation.is_animating(*now) {
state.indicator_status.widen(0.0, *now);
}
}
}
}
if state.indicator_status.is_animating(*now) {
shell.request_redraw();
}
state.now = Some(*now);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> Interaction {
self.items
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((item, tree), layout)| {
item.mouse_interaction(tree, layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced_core::renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
let bounds = layout.bounds();
let selector_bar_style = <Theme as Catalog>::style(theme, &self.class);
renderer.fill_quad(
Quad {
bounds,
border: selector_bar_style.border,
shadow: selector_bar_style.shadow,
snap: selector_bar_style.snap,
},
selector_bar_style
.background
.unwrap_or(Color::TRANSPARENT.into()),
);
for ((item, tree), layout) in self
.items
.iter()
.zip(&tree.children)
.zip(layout.child(0).children())
{
item.draw(tree, renderer, theme, style, layout, cursor, viewport);
}
let item = |id| {
self.items
.iter()
.zip(layout.child(0).children())
.find(|(item, _)| item.id == id)
.unwrap()
};
let state = tree.state.downcast_ref::<State<Id>>();
let Some(now) = state.now else {
return;
};
let (selected_item, selected_layout) = item(self.selected_id.clone());
let padding = selected_item.padding;
let base_x = selected_layout.position().x;
let outer_width = selected_layout.bounds().width;
let inner_width = outer_width - padding.left - padding.right;
let (x, width) = match &state.indicator_status {
IndicatorStatus::Fixed { .. } => (base_x + padding.left, inner_width),
IndicatorStatus::Widening { animation, .. } => {
let value = animation.interpolate_with(|v| v, now);
let x = base_x + padding.left * (1.0 - value);
let width = inner_width + 2.0 * padding.left * value;
(x, width)
}
IndicatorStatus::Hovered { .. } => (base_x, inner_width + padding.left + padding.right),
IndicatorStatus::Narrowing { animation, .. } => {
let value = animation.interpolate_with(|v| v, now);
let x = base_x + padding.left * value;
let width = outer_width - 2.0 * padding.left * value;
(x, width)
}
IndicatorStatus::Moving {
from, animation, ..
} => {
let from_x = item(from.clone()).1.position().x;
let to_x = selected_layout.bounds().x;
let x = animation.interpolate(from_x, to_x, now);
(x, inner_width)
}
};
let bounds = Rectangle {
x,
y: bounds.y + bounds.height - self.padding.top - INDICATOR_HEIGHT,
width,
height: INDICATOR_HEIGHT,
};
let indicator_style = <Theme as item::Catalog>::style(
theme,
&selected_item.class,
selected_item.status.unwrap_or_default(),
)
.active_indicator;
renderer.fill_quad(
Quad {
bounds,
border: indicator_style.border,
shadow: indicator_style.shadow,
snap: indicator_style.snap,
},
indicator_style
.background
.unwrap_or(Color::TRANSPARENT.into()),
);
}
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
.items
.iter_mut()
.zip(&mut tree.children)
.zip(layout.child(0).children())
.flat_map(|((item, tree), layout)| {
item.overlay(tree, layout, renderer, viewport, translation)
})
.collect();
Some(overlay::Group::with_children(overlays).overlay())
}
}
impl<'a, Id, Message, Theme, Renderer> From<SelectorBar<'a, Id, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Id: 'static + Clone + std::fmt::Debug + Default + PartialEq,
Message: 'a,
Theme: 'a + Catalog + item::Catalog,
Renderer: 'a + iced_core::Renderer,
{
fn from(bar: SelectorBar<'a, Id, Message, Theme, Renderer>) -> Self {
Element::new(bar)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Style {
pub background: Option<Background>,
pub border: Border,
pub shadow: Shadow,
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
}
}
}
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 Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(transparent)
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
pub fn transparent(_theme: &Theme) -> Style {
Style {
background: None,
border: Border::default(),
shadow: Shadow::default(),
snap: true,
}
}
pub fn item<'a, Id, Message, Theme, Renderer>(
id: Id,
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Item<'a, Id, Message, Theme, Renderer>
where
Theme: item::Catalog,
{
Item::new(id, content)
}