Skip to main content

snora_core/
tab.rs

1//! Tab bar — horizontal selector for switching between sibling views.
2//!
3//! Like [`crate::menu`] and [`crate::sidebar`], the tab bar is described
4//! as plain data here and rendered by `snora-widgets::app_tab_bar`. The
5//! engine never inspects these types directly; they exist so that
6//! applications and widget engines speak the same shape.
7//!
8//! # Choosing between tabs and a sidebar
9//!
10//! Both let users switch among sibling views, but they imply different
11//! information density and depth:
12//!
13//! * **Tabs** — flat, horizontal, label-first. Three to seven peer views
14//!   that the user expects to switch among frequently. Sits below the
15//!   header.
16//! * **Sidebar** ([`crate::SideBar`]) — vertical, icon-first, scales to
17//!   more entries. Use when the navigation is the primary structural
18//!   element of the app.
19//!
20//! Use both at once when tabs subdivide a sidebar-selected workspace.
21
22use crate::Icon;
23
24/// One tab in a [`TabBar`]. Carries an application-defined `TabId` so
25/// that the application is the source of truth for which tab is which.
26///
27/// # Example
28///
29/// ```
30/// use snora_core::Tab;
31///
32/// #[derive(Clone, PartialEq, Eq, Debug)]
33/// enum WorkspaceTab { Library, Editor, Settings }
34///
35/// let editor = Tab {
36///     id: WorkspaceTab::Editor,
37///     label: "Editor".into(),
38///     icon: None,
39/// };
40/// assert_eq!(editor.label, "Editor");
41/// assert_eq!(editor.id, WorkspaceTab::Editor);
42/// ```
43#[derive(Debug, Clone)]
44pub struct Tab<TabId: Clone + PartialEq> {
45    /// Application-defined identifier. Compared against [`TabBar::active`]
46    /// to decide which tab to highlight.
47    pub id: TabId,
48    /// Visible label.
49    pub label: String,
50    /// Optional leading icon.
51    pub icon: Option<Icon>,
52}
53
54/// A horizontal tab strip.
55///
56/// `TabBar` is generic over `TabId` so that applications can use any
57/// `Clone + PartialEq` type — typically a small enum.
58///
59/// # Example
60///
61/// ```
62/// use snora_core::{Tab, TabBar};
63///
64/// #[derive(Clone, PartialEq, Eq, Debug)]
65/// enum WorkspaceTab { Library, Editor }
66///
67/// let bar = TabBar {
68///     tabs: vec![
69///         Tab { id: WorkspaceTab::Library, label: "Library".into(), icon: None },
70///         Tab { id: WorkspaceTab::Editor,  label: "Editor".into(),  icon: None },
71///     ],
72///     active: WorkspaceTab::Library,
73/// };
74/// assert_eq!(bar.tabs.len(), 2);
75/// assert_eq!(bar.active, WorkspaceTab::Library);
76/// ```
77#[derive(Debug, Clone)]
78pub struct TabBar<TabId: Clone + PartialEq> {
79    /// Tabs in display order. The widget engine respects this order;
80    /// horizontal mirroring under [`crate::LayoutDirection::Rtl`] is
81    /// the engine's responsibility.
82    pub tabs: Vec<Tab<TabId>>,
83    /// The currently selected tab id. The widget renders this tab with
84    /// an active treatment (typically an underline). If `active` does
85    /// not match any tab in `tabs`, no tab is highlighted.
86    pub active: TabId,
87}
88
89/// What happens when the user interacts with a tab.
90///
91/// Tab bars only emit one kind of event — a tab being pressed. We
92/// still wrap it in an enum (rather than a bare `TabId`) so that
93/// future extensions (close button on a tab, drag-to-reorder) can be
94/// added without breaking the existing handler shape.
95///
96/// # Example
97///
98/// ```
99/// use snora_core::TabAction;
100///
101/// #[derive(Clone, Debug, PartialEq, Eq)]
102/// enum WorkspaceTab { Library, Editor }
103///
104/// // Application code typically maps `TabAction` into its own message
105/// // enum; here we just match on it directly.
106/// let received: TabAction<WorkspaceTab> = TabAction::Pressed(WorkspaceTab::Editor);
107/// match received {
108///     TabAction::Pressed(id) => assert_eq!(id, WorkspaceTab::Editor),
109/// }
110/// ```
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum TabAction<TabId> {
113    /// The user pressed a tab. The application typically responds by
114    /// updating its `active` state and re-rendering.
115    Pressed(TabId),
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[derive(Clone, PartialEq, Eq, Debug)]
123    enum DemoTab {
124        A,
125        B,
126        C,
127    }
128
129    #[test]
130    fn tab_bar_is_constructible() {
131        let bar = TabBar {
132            tabs: vec![
133                Tab {
134                    id: DemoTab::A,
135                    label: "A".into(),
136                    icon: None,
137                },
138                Tab {
139                    id: DemoTab::B,
140                    label: "B".into(),
141                    icon: None,
142                },
143            ],
144            active: DemoTab::A,
145        };
146        assert_eq!(bar.tabs.len(), 2);
147        assert_eq!(bar.active, DemoTab::A);
148    }
149
150    #[test]
151    fn tab_action_carries_id() {
152        let action = TabAction::Pressed(DemoTab::C);
153        assert_eq!(action, TabAction::Pressed(DemoTab::C));
154    }
155}