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 )
}