use std::borrow::Cow;
use iced::advanced::layout::{Layout, Limits, Node};
use iced::advanced::renderer;
use iced::advanced::widget::{tree, Tree, Widget};
use iced::advanced::{Clipboard, Shell};
use iced::{event, mouse, Border, Color, Element, Event, Length, Point, Rectangle, Size};
#[derive(Clone)]
pub struct Tab<'a> {
pub label: Cow<'a, str>,
pub icon: Option<Cow<'a, str>>,
}
impl<'a> Tab<'a> {
#[must_use]
pub fn new(label: impl Into<Cow<'a, str>>) -> Self {
Self {
label: label.into(),
icon: None,
}
}
#[must_use]
pub fn icon(mut self, icon: impl Into<Cow<'a, str>>) -> Self {
self.icon = Some(icon.into());
self
}
}
pub struct Tabs<'a, Message> {
tabs: Vec<Tab<'a>>,
active: usize,
on_select: Box<dyn Fn(usize) -> Message + 'a>,
tab_width: TabWidth,
height: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum TabWidth {
#[default]
Shrink,
Equal,
Fixed(f32),
}
impl<'a, Message> Tabs<'a, Message> {
pub fn new<F>(active: usize, on_select: F) -> Self
where
F: Fn(usize) -> Message + 'a,
{
Self {
tabs: Vec::new(),
active,
on_select: Box::new(on_select),
tab_width: TabWidth::default(),
height: 40.0,
}
}
#[must_use]
pub fn push(mut self, tab: Tab<'a>) -> Self {
self.tabs.push(tab);
self
}
#[must_use]
pub fn tab_width(mut self, width: TabWidth) -> Self {
self.tab_width = width;
self
}
#[must_use]
pub fn height(mut self, height: f32) -> Self {
self.height = height;
self
}
fn tab_bounds(&self, total_width: f32) -> Vec<(f32, f32)> {
let tab_count = self.tabs.len();
if tab_count == 0 {
return Vec::new();
}
match self.tab_width {
TabWidth::Equal => {
let w = total_width / tab_count as f32;
(0..tab_count).map(|i| (i as f32 * w, w)).collect()
}
TabWidth::Fixed(w) => (0..tab_count).map(|i| (i as f32 * w, w)).collect(),
TabWidth::Shrink => {
let mut x = 0.0;
self.tabs
.iter()
.map(|tab| {
let w = (tab.label.len() as f32 * 10.0 + 48.0).max(80.0);
let result = (x, w);
x += w;
result
})
.collect()
}
}
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Tabs<'a, Message>
where
Message: Clone,
Renderer: renderer::Renderer + iced::advanced::text::Renderer<Font = iced::Font>,
{
fn size(&self) -> Size<Length> {
Size::new(Length::Fill, Length::Fixed(self.height))
}
fn tag(&self) -> tree::Tag {
tree::Tag::stateless()
}
fn state(&self) -> tree::State {
tree::State::None
}
fn children(&self) -> Vec<Tree> {
Vec::new()
}
fn diff(&self, _tree: &mut Tree) {}
fn layout(&self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node {
let width = limits.max().width;
Node::new(Size::new(width, self.height))
}
fn on_event(
&mut self,
_tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = event {
if let Some(position) = cursor.position() {
let bounds = layout.bounds();
if bounds.contains(position) {
let tab_bounds = self.tab_bounds(bounds.width);
let rel_x = position.x - bounds.x;
for (i, (x, w)) in tab_bounds.iter().enumerate() {
if rel_x >= *x && rel_x < x + w {
if i != self.active {
shell.publish((self.on_select)(i));
}
return event::Status::Captured;
}
}
}
}
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let tab_bounds = self.tab_bounds(bounds.width);
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border {
color: Color::from_rgba(0.0, 0.0, 0.0, 0.1),
width: 0.0,
radius: 0.0.into(),
},
shadow: iced::Shadow::default(),
},
Color::from_rgba(0.95, 0.95, 0.97, 1.0),
);
for (i, (tab, (x, w))) in self.tabs.iter().zip(tab_bounds.iter()).enumerate() {
let tab_rect = Rectangle {
x: bounds.x + x,
y: bounds.y,
width: *w,
height: bounds.height,
};
let is_active = i == self.active;
let is_hovered = cursor
.position()
.map(|p| tab_rect.contains(p))
.unwrap_or(false);
let bg_color = if is_active {
Color::WHITE
} else if is_hovered {
Color::from_rgba(1.0, 1.0, 1.0, 0.5)
} else {
Color::TRANSPARENT
};
renderer.fill_quad(
renderer::Quad {
bounds: tab_rect,
border: Border {
radius: 6.0.into(),
..Default::default()
},
shadow: iced::Shadow::default(),
},
bg_color,
);
if is_active {
let indicator = Rectangle {
x: tab_rect.x,
y: tab_rect.y + tab_rect.height - 2.0,
width: tab_rect.width,
height: 2.0,
};
renderer.fill_quad(
renderer::Quad {
bounds: indicator,
border: Border::default(),
shadow: iced::Shadow::default(),
},
Color::from_rgb(0.22, 0.47, 0.87),
);
}
let text_color = if is_active {
Color::from_rgb(0.1, 0.1, 0.1)
} else {
Color::from_rgb(0.4, 0.4, 0.4)
};
let label: String = tab.label.clone().into_owned();
renderer.fill_text(
iced::advanced::text::Text {
content: label,
bounds: Size::new(tab_rect.width, tab_rect.height),
size: iced::Pixels(14.0),
line_height: iced::advanced::text::LineHeight::default(),
font: iced::Font::default(),
horizontal_alignment: iced::alignment::Horizontal::Center,
vertical_alignment: iced::alignment::Vertical::Center,
shaping: iced::advanced::text::Shaping::Basic,
wrapping: iced::advanced::text::Wrapping::None,
},
Point::new(tab_rect.x, tab_rect.y),
text_color,
tab_rect,
);
}
let border_line = Rectangle {
x: bounds.x,
y: bounds.y + bounds.height - 1.0,
width: bounds.width,
height: 1.0,
};
renderer.fill_quad(
renderer::Quad {
bounds: border_line,
border: Border::default(),
shadow: iced::Shadow::default(),
},
Color::from_rgba(0.0, 0.0, 0.0, 0.1),
);
}
}
impl<'a, Message, Theme, Renderer> From<Tabs<'a, Message>> for Element<'a, Message, Theme, Renderer>
where
Message: Clone + 'a,
Theme: 'a,
Renderer: renderer::Renderer + iced::advanced::text::Renderer<Font = iced::Font> + 'a,
{
fn from(tabs: Tabs<'a, Message>) -> Self {
Element::new(tabs)
}
}