Skip to main content

teksilo_widgets/tab_widget/
info.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Per-tab presentation metadata.
5//!
6//! [`TabInfo`] is the bundle of "what does this tab look like?"
7//! values: title, icon, tooltip, capability flags. Decoupled from
8//! [`crate::tab_widget::TabHandle`] so the same struct is reusable
9//! by both static and dynamic tab construction paths.
10//!
11//! Title and tooltip are [`LocalizedString`] — they
12//! accept `tr!(...)` (locale-reactive) and convert from raw
13//! strings via `LocalizedString::literal`. Icon is a factory
14//! closure (no `IconWidget: Clone` requirement) so the same
15//! `TabInfo` can be cloned cheaply and the icon is rebuilt each
16//! TabHeader build, picking up theme / state changes naturally.
17
18use std::rc::Rc;
19
20use teksilo_canvas::Point;
21use teksilo_core::widget::{EventContext, Widget};
22use teksilo_i18n::LocalizedString;
23
24use super::delegate::ContextMenuFactory;
25use crate::IconWidget;
26use crate::tooltip::RichTooltipSource;
27
28/// Reusable factory for an [`IconWidget`]. Boxed in `Rc` so
29/// [`TabInfo`] is `Clone` without forcing `IconWidget: Clone`.
30pub type IconFactory = Rc<dyn Fn() -> IconWidget>;
31
32/// Reusable factory for a composite-tooltip body widget. Boxed in
33/// `Rc` so [`TabInfo`] is `Clone` without forcing the body's type to
34/// be `Clone`. The factory is called each time the tab's header
35/// builds — typically once per tab lifetime, plus rebuilds triggered
36/// by data-source mutations.
37pub type CompositeTooltipFactory = Rc<dyn Fn() -> Box<dyn Widget>>;
38
39/// Per-tab presentation metadata. Build with [`TabInfo::new`] and
40/// fluent setters.
41///
42/// ```rust
43/// # use teksilo_widgets::tab_widget::TabInfo;
44/// # use teksilo_widgets::primitives::IconWidget;
45/// # use teksilo_i18n::lit;
46/// let _info = TabInfo::new()
47///     .title(lit!("Welcome"))
48///     .icon(|| IconWidget::checkmark(16.0))
49///     .closable(true);
50/// ```
51#[derive(Clone)]
52pub struct TabInfo {
53    pub(crate) title: Option<LocalizedString>,
54    pub(crate) icon: Option<IconFactory>,
55    pub(crate) tooltip: Option<LocalizedString>,
56    /// Optional rich tooltip — registry key or inline content.
57    /// Mutually exclusive with `tooltip` and `composite_tooltip`.
58    pub(crate) rich_tooltip: Option<RichTooltipSource>,
59    /// Optional composite tooltip body factory. Mutually exclusive
60    /// with the other two tooltip slots.
61    pub(crate) composite_tooltip: Option<CompositeTooltipFactory>,
62    pub(crate) closable: bool,
63    pub(crate) pinned: bool,
64    /// Initial-enabled hint. Forwarded into the arena at build time
65    /// via `ctx.enabled_when(header_id, false)` when `false`; the
66    /// arena is then the single source of truth and ANDs with
67    /// ancestors. A disabled `TabBar` ancestor disables every tab
68    /// regardless of this flag.
69    pub(crate) initial_enabled: teksilo_core::signal::Prop<bool>,
70    /// Mark the tab's content pane as focusable so keyboard users can
71    /// reach it. ARIA: a `tabpanel` with no focusable content must
72    /// itself be focusable (`tabindex="0"`). Opt-in because the
73    /// framework can't reliably auto-detect at build time (children
74    /// are built lazily). Default: `false`.
75    pub(crate) focusable_panel: bool,
76    /// Optional per-tab context menu (right-click the tab header). Same
77    /// shape as the delegate's `context_menu`; for dynamic tabs this is
78    /// the per-handle way to attach one.
79    pub(crate) context_menu: Option<ContextMenuFactory>,
80}
81
82impl Default for TabInfo {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl std::fmt::Debug for TabInfo {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("TabInfo")
91            .field("title", &self.title)
92            .field("has_icon", &self.icon.is_some())
93            .field("tooltip", &self.tooltip)
94            .field("has_rich_tooltip", &self.rich_tooltip.is_some())
95            .field("has_composite_tooltip", &self.composite_tooltip.is_some())
96            .field("closable", &self.closable)
97            .field("pinned", &self.pinned)
98            .field("initial_enabled", &self.initial_enabled.get())
99            .field("focusable_panel", &self.focusable_panel)
100            .field("has_context_menu", &self.context_menu.is_some())
101            .finish()
102    }
103}
104
105impl TabInfo {
106    /// Empty defaults: no title, no icon, no tooltip, not closable,
107    /// not pinned, enabled.
108    pub fn new() -> Self {
109        Self {
110            title: None,
111            icon: None,
112            tooltip: None,
113            rich_tooltip: None,
114            composite_tooltip: None,
115            closable: false,
116            pinned: false,
117            initial_enabled: teksilo_core::signal::Prop::Static(true),
118            focusable_panel: false,
119            context_menu: None,
120        }
121    }
122
123    /// Attach a per-tab context menu (right-click the tab header). The
124    /// factory receives the click position (tab-local) and a full
125    /// [`EventContext`], and returns `Some(menu)` to mount or `None` to
126    /// decline (falling through to an ancestor). Cloned per header build.
127    pub fn context_menu(
128        mut self,
129        f: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
130    ) -> Self {
131        self.context_menu = Some(Rc::new(f));
132        self
133    }
134
135    /// Set the tab's title. Accepts `tr!(...)`, a literal string,
136    /// or any value implementing `Into<LocalizedString>`.
137    /// `None` means icon-only (the pinned-tab presentation).
138    pub fn title(mut self, t: impl Into<LocalizedString>) -> Self {
139        self.title = Some(t.into());
140        self
141    }
142
143    /// Untitled — useful for icon-only tabs even when not pinned.
144    pub fn no_title(mut self) -> Self {
145        self.title = None;
146        self
147    }
148
149    /// Set the leading icon via a factory closure. The closure is
150    /// called each time the `TabHeader`
151    /// is built — typically once per tab lifetime, plus any rebuild
152    /// triggered by data-source mutations.
153    pub fn icon(mut self, factory: impl Fn() -> IconWidget + 'static) -> Self {
154        self.icon = Some(Rc::new(factory));
155        self
156    }
157
158    /// Tooltip text shown on hover. If unset and the tab is
159    /// [pinned](Self::pinned), the framework promotes [title](Self::title)
160    /// to the tooltip — pinned tabs render icon-only and otherwise
161    /// have no way for the user to identify them.
162    pub fn tooltip(mut self, t: impl Into<LocalizedString>) -> Self {
163        self.tooltip = Some(t.into());
164        self.rich_tooltip = None;
165        self.composite_tooltip = None;
166        self
167    }
168
169    /// Attach a rich tooltip resolved from the app-wide tooltip
170    /// registry. See [`Button::rich_tooltip`](crate::button::Button::rich_tooltip).
171    pub fn rich_tooltip(mut self, key: impl Into<String>) -> Self {
172        self.rich_tooltip = Some(RichTooltipSource::Key(key.into()));
173        self.tooltip = None;
174        self.composite_tooltip = None;
175        self
176    }
177
178    /// Attach a rich tooltip driven by inline `TooltipContent`.
179    pub fn rich_tooltip_content(mut self, content: crate::tooltip::TooltipContent) -> Self {
180        self.rich_tooltip = Some(RichTooltipSource::Content(content));
181        self.tooltip = None;
182        self.composite_tooltip = None;
183        self
184    }
185
186    /// Attach a composite tooltip — third tier, hosting an arbitrary
187    /// widget tree. The `factory` closure is called each time the
188    /// tab's header rebuilds, so the body picks up theme / locale
189    /// changes naturally without retaining state across rebuilds.
190    pub fn composite_tooltip<W>(mut self, factory: impl Fn() -> W + 'static) -> Self
191    where
192        W: Widget + 'static,
193    {
194        self.composite_tooltip = Some(Rc::new(move || -> Box<dyn Widget> { Box::new(factory()) }));
195        self.tooltip = None;
196        self.rich_tooltip = None;
197        self
198    }
199
200    /// Whether the tab shows a close button + responds to
201    /// middle-click. Default: `false`.
202    pub fn closable(mut self, b: bool) -> Self {
203        self.closable = b;
204        self
205    }
206
207    /// Whether the tab renders in the leading pinned strip
208    /// (icon-only, fixed-width, no close button — Firefox / Chrome
209    /// convention). Default: `false`.
210    pub fn pinned(mut self, b: bool) -> Self {
211        self.pinned = b;
212        self
213    }
214
215    /// Whether the tab can be activated. Disabled tabs render but
216    /// are skipped by keyboard navigation, can't be clicked, and
217    /// don't get the close button. Default: `true`.
218    ///
219    /// Forwarded to the arena via `ctx.enabled_when(header_id, false)`
220    /// at build time when `false`. Ancestor-driven disable (e.g. a
221    /// disabled `TabBar`) ANDs with this flag automatically.
222    pub fn enabled(mut self, enabled: impl Into<teksilo_core::signal::Prop<bool>>) -> Self {
223        self.initial_enabled = enabled.into();
224        self
225    }
226
227    /// Make the tab's content pane itself focusable, so keyboard users
228    /// can press `Tab` from the selected tab header and land inside
229    /// the panel.
230    ///
231    /// Opt in for panels you know contain no focusable descendants —
232    /// a static text-only "About" tab, a chart-only metrics tab.
233    /// Panels that already host a `Button`, `TextInput`, `ListView`,
234    /// or any other interactive widget don't need this: focus will
235    /// flow naturally into the descendant.
236    ///
237    /// ARIA: this implements the `tabindex="0"` requirement that an
238    /// empty `tabpanel` must be focusable so its content can be read
239    /// by screen readers in browse mode. AccessKit has no `tabindex`
240    /// field; the framework advertises `Action::Focus` on the panel
241    /// node to signal focusability to AT. Default: `false`.
242    pub fn focusable_panel(mut self, b: bool) -> Self {
243        self.focusable_panel = b;
244        self
245    }
246}