ltk/widget/tab_bar/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! TabBar — segmented selector built as a composition over existing
//! widgets. The widget itself is stateless: the application owns the
//! current selection (`usize`) and calls back through
//! [`TabBar::on_select`] when the user taps another tab.
//!
//! Returns an [`Element`] directly via [`TabBar::build`] / `Into`, so it
//! drops into any layout that accepts a child widget.
//!
//! ```rust,no_run
//! # use ltk::{ tabs, TabBar };
//! # #[ derive( Clone ) ] enum Msg { SelectTab( usize ) }
//! # struct App { tab: usize }
//! # impl App { fn _ex( &self ) -> TabBar<Msg> {
//! tabs( [ "General", "Network", "Audio" ] )
//! .selected( self.tab )
//! .on_select( Msg::SelectTab )
//! # }}
//! ```
use crate::types::Color;
use super::Element;
mod theme;
#[ cfg( test ) ]
mod tests;
/// Segmented horizontal tab selector. One row of pressable cells with
/// the active cell painted as a filled pill and inactive cells as plain
/// text. Build it from a slice of labels; produce an [`Element`] with
/// [`Self::build`] (or by `.into()`-ing it where an `Element` is
/// expected).
pub struct TabBar<Msg: Clone>
{
pub labels: Vec<String>,
pub selected: usize,
pub on_select: Option<std::sync::Arc<dyn Fn( usize ) -> Msg>>,
/// Background colour of the strip itself. `None` paints no
/// background, letting the parent surface show through.
pub strip_bg: Option<Color>,
}
impl<Msg: Clone + 'static> TabBar<Msg>
{
/// Build a tab bar from an iterable of labels.
pub fn new<I, S>( labels: I ) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self
{
labels: labels.into_iter().map( Into::into ).collect(),
selected: 0,
on_select: None,
strip_bg: Some( crate::theme::palette().surface_alt ),
}
}
/// Index of the currently selected tab. Out-of-range values are
/// drawn as "no tab active".
pub fn selected( mut self, idx: usize ) -> Self
{
self.selected = idx;
self
}
/// Callback invoked with the index of the tapped tab.
pub fn on_select( mut self, f: impl Fn( usize ) -> Msg + 'static ) -> Self
{
self.on_select = Some( std::sync::Arc::new( f ) );
self
}
/// Override the strip background colour. Pass `None` (via
/// [`Self::strip_bg_none`]) to disable the background entirely.
pub fn strip_bg( mut self, c: Color ) -> Self
{
self.strip_bg = Some( c );
self
}
/// Paint no strip background. Useful inside containers that already
/// provide their own surface chrome.
pub fn strip_bg_none( mut self ) -> Self
{
self.strip_bg = None;
self
}
/// Build the [`Element`] tree representing this tab bar. Equivalent
/// to `Element::from( self )`.
pub fn build( self ) -> Element<Msg>
{
use super::{ button, container };
use super::button::ButtonVariant;
use crate::layout::row::row;
use crate::layout::spacer::spacer;
let selected = self.selected;
let cb = self.on_select.clone();
let labels = self.labels;
let mut r = row::<Msg>().padding( theme::PADDING ).spacing( theme::SPACING );
for ( i, label ) in labels.into_iter().enumerate()
{
let is_active = i == selected;
let mut btn = button( label );
if is_active
{
btn = btn.variant( ButtonVariant::Primary );
} else {
btn = btn.variant( ButtonVariant::Tertiary );
}
if let Some( ref f ) = cb
{
let f = f.clone();
let msg = f( i );
btn = btn.on_press( msg );
}
r = r.push( btn );
}
// Trailing spacer so the strip claims the full row width and the
// chips left-align inside it. Callers that want full-width tabs
// can wrap the result in a Row of `flex` children themselves.
r = r.push( spacer() );
match self.strip_bg
{
Some( bg ) => container( r ).background( bg ).radius( theme::RADIUS ).into(),
None => Element::Row( r ),
}
}
}
impl<Msg: Clone + 'static> From<TabBar<Msg>> for Element<Msg>
{
fn from( t: TabBar<Msg> ) -> Self
{
t.build()
}
}
/// Create a [`TabBar`] from any iterable of label-likes.
///
/// ```rust,no_run
/// # use ltk::{ tabs, TabBar };
/// # #[ derive( Clone ) ] enum Msg { SelectTab( usize ) }
/// # struct App { tab: usize }
/// # impl App { fn _ex( &self ) -> TabBar<Msg> {
/// tabs( [ "Inbox", "Sent", "Drafts" ] )
/// .selected( self.tab )
/// .on_select( Msg::SelectTab )
/// # }}
/// ```
pub fn tabs<Msg, I, S>( labels: I ) -> TabBar<Msg>
where
Msg: Clone + 'static,
I: IntoIterator<Item = S>,
S: Into<String>,
{
TabBar::new( labels )
}