use ribir_core::prelude::*;
use crate::prelude::*;
#[derive(Declare, Clone)]
pub struct Tabs {
#[declare(default)]
active: usize,
#[declare(skip)]
tabs_cnt: usize,
}
class_names! {
TAB_ICON,
TAB_LABEL,
TAB_HEADER,
TAB_HEADERS_VIEW,
TAB_HEADERS_CONTAINER,
TAB_PANE,
TABS
}
#[derive(Template)]
pub struct Tab<'t> {
icon: Option<PairOf<'t, Icon>>,
label: Option<TextValue>,
pane: Option<GenWidget>,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum TabPos {
#[default]
Top,
Bottom,
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum TabType {
#[default]
Primary,
Secondary,
Tertiary,
Quaternary,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TabsInlineIcon(pub bool);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TabInfo {
pub idx: usize,
pub has_icon: bool,
pub has_label: bool,
}
impl TabInfo {
pub fn is_icon_only(&self) -> bool { self.has_icon && !self.has_label }
pub fn is_label_only(&self) -> bool { self.has_label && !self.has_icon }
pub fn has_icon_and_label(&self) -> bool { self.has_icon && self.has_label }
}
impl Tabs {
pub fn active_idx(&self) -> usize { self.active }
pub fn set_active(&mut self, idx: usize) {
if idx < self.tabs_cnt {
self.active = idx;
}
}
}
impl<'c> ComposeChild<'c> for Tabs {
type Child = Vec<Tab<'c>>;
fn compose_child(this: impl StateWriter<Value = Self>, child: Self::Child) -> Widget<'c> {
this.silent().tabs_cnt = child.len();
fn_widget! {
let position = Variant::<TabPos>::new_or_default(BuildCtx::get());
let (headers, panes): (Vec<_>, Vec<_>) = child
.into_iter()
.enumerate()
.map(|(idx, tab)| tab.into_header_and_pane(idx))
.unzip();
@Flex {
providers: [Provider::writer(this.clone_writer(), None)],
class: TABS,
direction: position.clone().map(TabPos::main_dir),
reverse: position.clone().map(TabPos::main_reverse),
align_items: Align::Stretch,
@ScrollableWidget {
class: TAB_HEADERS_VIEW,
scrollable: position.clone().map(TabPos::headers_scroll_dir),
@Flex {
align_items: Align::Center,
direction: position.map(TabPos::headers_dir),
class: TAB_HEADERS_CONTAINER,
@ { headers }
}
}
@Expanded {
defer_alloc: true,
@pipe! { panes[$read(this).active].clone() }
}
}
}
.into_widget()
}
}
impl<'w> Tab<'w> {
fn into_header_and_pane(mut self, idx: usize) -> (Widget<'w>, GenWidget) {
let pane = self.take_pane();
let header = self.tab_header(idx);
(header, pane)
}
fn tab_header(self, idx: usize) -> Widget<'w> {
let tab_info = self.info(idx);
fn_widget! {
let ctx = BuildCtx::get();
let inline = Variant::<TabsInlineIcon>::new_or_default(ctx);
let line = match inline {
Variant::Value(inline) => inline.align_header_widget(),
Variant::Watcher(w) => pipe!($read(w).align_header_widget()).into_multi_child()
};
let header = @Class {
class: TAB_HEADER,
on_tap: move |e| {
let prev = Provider::of::<Tabs>(e).unwrap().active;
if prev != idx {
Provider::write_of::<Tabs>(e).unwrap().set_active(idx);
}
},
@(line) {
@ { self.icon.map(|icon| icon! { class: TAB_ICON, @{ icon } }) }
@ { self.label.map(|label| text! { text: label, class: TAB_LABEL }) }
}
};
@Expanded {
defer_alloc: false,
@Providers {
providers: [Provider::new(tab_info)],
@ { header }
}
}
}
.into_widget()
}
pub fn take_pane(&mut self) -> GenWidget {
let pane = self.pane.take();
GenWidget::from_fn_widget(fat_obj! {
class: TAB_PANE,
@ {
pane.as_ref()
.map_or_else(|| Void::default().into_widget(), GenWidget::gen_widget)
}
})
}
pub fn info(&self, idx: usize) -> TabInfo {
TabInfo { has_icon: self.icon.is_some(), has_label: self.label.is_some(), idx }
}
}
impl TabPos {
pub fn is_horizontal(self) -> bool { matches!(self, TabPos::Top | TabPos::Bottom) }
fn main_dir(&self) -> Direction {
match self {
TabPos::Top | TabPos::Bottom => Direction::Vertical,
TabPos::Left | TabPos::Right => Direction::Horizontal,
}
}
fn main_reverse(&self) -> bool {
match self {
TabPos::Top | TabPos::Left => false,
TabPos::Bottom | TabPos::Right => true,
}
}
fn headers_dir(&self) -> Direction {
match self {
TabPos::Top | TabPos::Bottom => Direction::Horizontal,
TabPos::Left | TabPos::Right => Direction::Vertical,
}
}
fn headers_scroll_dir(&self) -> Scrollable {
match self {
TabPos::Top | TabPos::Bottom => Scrollable::X,
TabPos::Left | TabPos::Right => Scrollable::Y,
}
}
}
impl TabsInlineIcon {
fn align_header_widget(self) -> XMultiChild<'static> {
if self.0 {
Row { align_items: Align::Center, justify_content: JustifyContent::Start }.into_multi_child()
} else {
Column { align_items: Align::Center, justify_content: JustifyContent::Start }
.into_multi_child()
}
}
}
impl Default for TabsInlineIcon {
fn default() -> Self { TabsInlineIcon(true) }
}
#[cfg(test)]
mod tests {
use ribir_core::test_helper::*;
use ribir_dev_helper::*;
use smallvec::smallvec;
use super::*;
fn tabs_tester(tab_type: TabType, pos: TabPos) -> WidgetTester {
WidgetTester::new(tabs! {
providers: smallvec![Provider::new(tab_type), Provider::new(pos)],
clamp: BoxClamp::EXPAND_BOTH,
@Tab {
@{ "Tab 1" }
@text! { text: "Only label" }
}
@Tab {
@Icon { @svg_registry::default_svg() }
}
@Tab {
@ { "Tab 3" }
@Icon { @svg_registry::default_svg() }
@text! { text: "Label and icon" }
}
})
.with_wnd_size(Size::new(256., 128.))
}
widget_image_tests!(primary_left, tabs_tester(TabType::Primary, TabPos::Left),);
widget_image_tests!(primary_right, tabs_tester(TabType::Primary, TabPos::Right),);
widget_image_tests!(primary_top, tabs_tester(TabType::Primary, TabPos::Top),);
widget_image_tests!(primary_bottom, tabs_tester(TabType::Primary, TabPos::Bottom),);
widget_image_tests!(secondary_left, tabs_tester(TabType::Secondary, TabPos::Left),);
widget_image_tests!(secondary_right, tabs_tester(TabType::Secondary, TabPos::Right),);
widget_image_tests!(secondary_top, tabs_tester(TabType::Secondary, TabPos::Top),);
widget_image_tests!(secondary_bottom, tabs_tester(TabType::Secondary, TabPos::Bottom),);
}