teksilo-widgets 0.9.0

Widget library for Teksilo — over a hundred widgets and layout primitives, from Button to TreeTableView.
Documentation
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech

//! Per-tab metadata extraction for `TabBar<T>` and `TabWidget<T>`.
//!
//! `TabDelegate<T>` is a struct of closures the tab bar invokes against
//! each item to obtain its label, icon, slots, tooltip, and capability
//! flags (closable / pinned / enabled). Mirrors `ListView`'s
//! `Fn(usize, &T, bool) -> Box<dyn Widget>` delegate pattern, but split
//! into per-aspect callbacks so callers don't have to compose every
//! affordance into one giant builder.
//!
//! Closures are called at build time. Mutating an item via
//! `ListModel::set(i, …)` fires `DataChange::ItemUpdated` which
//! triggers a rebuild of the bar — closures re-run, labels and icons
//! re-resolve. Locale changes propagate through the same path because
//! `LocalizedString` already carries reactive resolution semantics.

use std::rc::Rc;

use teksilo_canvas::Point;
use teksilo_core::widget::{EventContext, Widget};
use teksilo_i18n::LocalizedString;

use crate::IconWidget;
use crate::tooltip::{RichTooltipSource, TooltipContent};

/// A reusable widget factory the framework calls every time a context
/// menu opens. Returns a fresh widget instance each call (the
/// framework can't reuse a single widget across multiple openings).
///
/// Same shape as the framework's
/// [`teksilo_core::widget_builder::ContextMenuFactory`] — receives the
/// click position (in tab-local coords) and a full
/// [`EventContext`], and returns `Some(menu)` to mount or `None` to
/// decline. The `Rc` wrapping is a tab-widget convenience: the
/// delegate clones the factory per-tab without reallocating.
pub type ContextMenuFactory = Rc<dyn Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>>>;

/// Bar orientation. Selects between a horizontal row of tabs (default
/// for browser-style document tabs) and a vertical column of pills
/// (sidebar / IDE perspective convention).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TabBarOrientation {
    /// Tabs flow left-to-right in a horizontal row. Scroll axis is
    /// horizontal; a vertical wheel maps to horizontal scroll
    /// (Firefox / Chrome convention) when
    /// `vertical_wheel_scrolls_horizontally` is on.
    #[default]
    Horizontal,
    /// Tabs flow top-to-bottom in a vertical column. Scroll axis is
    /// vertical; vertical wheel scrolls vertically. Pinned tabs
    /// render in a non-scrolling strip at the top of the column.
    Vertical,
}

impl From<TabBarOrientation> for teksilo_core::styles::TabBarOrientation {
    fn from(o: TabBarOrientation) -> Self {
        match o {
            TabBarOrientation::Horizontal => teksilo_core::styles::TabBarOrientation::Horizontal,
            TabBarOrientation::Vertical => teksilo_core::styles::TabBarOrientation::Vertical,
        }
    }
}

/// How wide each tab is: shared across all unpinned tabs, chosen
/// per-tab from content, or stretched to fill the bar.
///
/// `Shared` and `Independent` size the **layout axis** (width in
/// horizontal bars, height in vertical bars); `Fill` sizes the tab's
/// **width** in both orientations — see each variant. See the module
/// docs of [`crate::tab_widget`] for how this is applied per
/// orientation. In wrap (multi-line horizontal) mode `Independent` is
/// forced regardless of this setting — equal-width tabs in a wrapping
/// row look like a tile grid and lose the bookmark-bar / pill-strip
/// aesthetic.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabSizing {
    /// All non-pinned tabs share the same extent on the layout axis.
    /// The available region is divided equally across the unpinned
    /// count, then clamped to `[min_tab_extent, max_tab_extent]`.
    /// Below the min, content overflows into scroll. Above the max,
    /// slack is left as empty space at the trailing edge.
    ///
    /// In a **vertical** bar the layout axis is the pill *height*, so
    /// this yields uniform pills whose width fits the widest label
    /// (clamped to `[min_tab_width, max_tab_width]`).
    Shared,
    /// Each tab sizes to its content (icon + label + slots), clamped
    /// to `[min_tab_extent, max_tab_extent]`. Truncation via ellipsis
    /// when content hits `max`.
    Independent,
    /// Tabs stretch to the full width the bar is offered — no slack
    /// left over, no fit-to-content shrinking. The nav-rail /
    /// segmented-control look (VS Code's settings sidebar, a
    /// full-bleed tab strip).
    ///
    /// - **Horizontal:** the viewport width is divided equally across
    ///   the unpinned tabs and `max_tab_width` is *not* applied, so
    ///   the strip is filled edge to edge instead of leaving trailing
    ///   slack. `min_tab_width` still holds — below it the headers
    ///   overflow into scroll rather than squeezing to nothing.
    /// - **Vertical:** every pill takes the bar's full proposed width
    ///   (the widest-label clamp is bypassed), so the tabs span the
    ///   sidebar. Pill height is unchanged (the intrinsic
    ///   `editor_tab_height`, or the `tab_bar_height` override).
    ///
    /// With no width proposed at all (an unbounded measure — a
    /// `Center`, an `HStack` asking for the natural size), there is
    /// nothing to fill: a vertical bar falls back to the `Shared`
    /// fit-to-widest-label width. Give the bar a bounded width (a
    /// `FixedSize`, an `Expand` in a sized parent) for `Fill` to have
    /// any effect.
    Fill,
}

/// Bar-level control over what each tab shows — its icon, its label, or both.
///
/// Each tab still declares both a title and (optionally) an icon; this mode
/// decides which are painted, so a caller can offer a "tab size" toggle
/// (VS Code's activity-bar / panel convention) without rebuilding the tabs by
/// hand. Icon-only tabs size to their icon (they don't pad out to a text
/// width), and the full title is promoted to the hover tooltip.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TabDisplayMode {
    /// Render each tab exactly as its [`TabInfo`](super::TabInfo) declares —
    /// the title if set, the icon if set. The default; preserves per-tab
    /// `no_title()` control.
    #[default]
    Auto,
    /// Title only — icons are hidden even when present.
    Text,
    /// Icon only — the title becomes the hover tooltip. A tab with no icon
    /// falls back to its title's initial letter so the mode is never blank.
    Icon,
    /// Icon + title.
    IconText,
}

/// When the trailing "show all tabs" overflow dropdown button appears.
///
/// The dropdown is a chevron-down `PopoverIconButton` whose popover lists every
/// tab (a jump-to menu for tabs scrolled out of view). This mode governs *when*
/// the button itself is shown — independent of whether the tabs actually
/// overflow the viewport (which is what drives the scroll arrows).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TabOverflowButton {
    /// Show the button **only when the tab headers overflow** the bar's
    /// viewport — i.e. exactly when there is something scrolled out of view, the
    /// same condition that auto-reveals the scroll arrows. The default: the
    /// button stays out of the way until it is useful.
    #[default]
    Auto,
    /// Always show the button whenever the bar has at least one tab, even when
    /// every tab is already visible (a persistent jump-to affordance).
    Always,
    /// Never show the button.
    Never,
}

/// Type alias for label-resolving callbacks.
type LabelFn<T> = Box<dyn Fn(usize, &T) -> LocalizedString>;
/// Type alias for icon-resolving callbacks.
type IconFn<T> = Box<dyn Fn(usize, &T) -> Option<IconWidget>>;
/// Type alias for slot-widget-resolving callbacks.
type SlotFn<T> = Box<dyn Fn(usize, &T) -> Option<Box<dyn Widget>>>;
/// Type alias for context-menu-factory-resolving callbacks. The
/// returned factory is callable many times (once per right-click).
type ContextMenuFn<T> = Box<dyn Fn(usize, &T) -> Option<ContextMenuFactory>>;
/// Type alias for tooltip callbacks.
type TooltipFn<T> = Box<dyn Fn(usize, &T) -> Option<LocalizedString>>;
/// Type alias for rich-tooltip-key callbacks (returns a registry key per tab).
type RichTooltipKeyFn<T> = Box<dyn Fn(usize, &T) -> Option<String>>;
/// Type alias for inline rich-tooltip-content callbacks.
type RichTooltipContentFn<T> = Box<dyn Fn(usize, &T) -> Option<TooltipContent>>;
/// Type alias for composite-tooltip callbacks. Returns a boxed widget
/// because closure-returning-`impl Trait` isn't object-safe.
type CompositeTooltipFn<T> = Box<dyn Fn(usize, &T) -> Option<Box<dyn Widget>>>;
/// Type alias for boolean capability callbacks.
type FlagFn<T> = Box<dyn Fn(usize, &T) -> bool>;

/// Resolves per-tab UI from a model item.
///
/// Required: a `label` callback. Everything else is optional and
/// defaults to "no leading icon, no slots, no tooltip, not closable,
/// not pinned, enabled".
pub struct TabDelegate<T: 'static> {
    pub(crate) label: LabelFn<T>,
    pub(crate) icon: Option<IconFn<T>>,
    pub(crate) leading: Option<SlotFn<T>>,
    pub(crate) trailing: Option<SlotFn<T>>,
    pub(crate) context_menu: Option<ContextMenuFn<T>>,
    pub(crate) closable: Option<FlagFn<T>>,
    pub(crate) pinned: Option<FlagFn<T>>,
    pub(crate) enabled: Option<FlagFn<T>>,
    pub(crate) tooltip: Option<TooltipFn<T>>,
    pub(crate) rich_tooltip_key: Option<RichTooltipKeyFn<T>>,
    pub(crate) rich_tooltip_content: Option<RichTooltipContentFn<T>>,
    pub(crate) composite_tooltip: Option<CompositeTooltipFn<T>>,
}

impl<T: 'static> std::fmt::Debug for TabDelegate<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TabDelegate")
            .field("has_icon", &self.icon.is_some())
            .field("has_leading", &self.leading.is_some())
            .field("has_trailing", &self.trailing.is_some())
            .field("has_context_menu", &self.context_menu.is_some())
            .field("has_closable", &self.closable.is_some())
            .field("has_pinned", &self.pinned.is_some())
            .field("has_enabled", &self.enabled.is_some())
            .field("has_tooltip", &self.tooltip.is_some())
            .field("has_rich_tooltip_key", &self.rich_tooltip_key.is_some())
            .field(
                "has_rich_tooltip_content",
                &self.rich_tooltip_content.is_some(),
            )
            .field("has_composite_tooltip", &self.composite_tooltip.is_some())
            .finish()
    }
}

impl<T: 'static> TabDelegate<T> {
    /// Construct from the label callback. Every other field defaults
    /// to its identity behavior.
    pub fn new(label: impl Fn(usize, &T) -> LocalizedString + 'static) -> Self {
        Self {
            label: Box::new(label),
            icon: None,
            leading: None,
            trailing: None,
            context_menu: None,
            closable: None,
            pinned: None,
            enabled: None,
            tooltip: None,
            rich_tooltip_key: None,
            rich_tooltip_content: None,
            composite_tooltip: None,
        }
    }

    /// Per-tab leading icon (rendered before the label).
    pub fn icon(mut self, f: impl Fn(usize, &T) -> Option<IconWidget> + 'static) -> Self {
        self.icon = Some(Box::new(f));
        self
    }

    /// Per-tab leading slot (between the icon and label, or before
    /// the label when no icon is present).
    pub fn leading(mut self, f: impl Fn(usize, &T) -> Option<Box<dyn Widget>> + 'static) -> Self {
        self.leading = Some(Box::new(f));
        self
    }

    /// Per-tab trailing slot (between the label and the close button,
    /// or at the trailing edge when no close button is present).
    pub fn trailing(mut self, f: impl Fn(usize, &T) -> Option<Box<dyn Widget>> + 'static) -> Self {
        self.trailing = Some(Box::new(f));
        self
    }

    /// Per-tab context menu factory. Activated by right-click /
    /// long-press / `accesskit::Action::ShowContextMenu`.
    ///
    /// The closure runs once per build and returns an optional
    /// [`ContextMenuFactory`]. The factory itself is called every
    /// time the menu opens, returning a fresh menu widget each call —
    /// the framework cannot reuse a single widget instance across
    /// multiple openings.
    pub fn context_menu(
        mut self,
        f: impl Fn(usize, &T) -> Option<ContextMenuFactory> + 'static,
    ) -> Self {
        self.context_menu = Some(Box::new(f));
        self
    }

    /// Per-tab closable flag. When `true`, the tab gets a trailing
    /// close button and middle-click / `Ctrl+W` close affordances.
    /// Pinned tabs suppress the close button regardless of this flag
    /// (pinned tabs only close via the context menu — Firefox
    /// convention).
    pub fn closable(mut self, f: impl Fn(usize, &T) -> bool + 'static) -> Self {
        self.closable = Some(Box::new(f));
        self
    }

    /// Per-tab pinned flag. Pinned tabs render in a leading
    /// non-scrolling region with a fixed icon-only width.
    pub fn pinned(mut self, f: impl Fn(usize, &T) -> bool + 'static) -> Self {
        self.pinned = Some(Box::new(f));
        self
    }

    /// Per-tab enabled flag. Disabled tabs are visible but not
    /// activatable, skipped by keyboard navigation, and excluded from
    /// the close / pin / context-menu affordances.
    pub fn enabled(mut self, f: impl Fn(usize, &T) -> bool + 'static) -> Self {
        self.enabled = Some(Box::new(f));
        self
    }

    /// Per-tab tooltip text. Shown on hover via the existing
    /// `WidgetBuilder::tooltip` mechanism.
    pub fn tooltip(mut self, f: impl Fn(usize, &T) -> Option<LocalizedString> + 'static) -> Self {
        self.tooltip = Some(Box::new(f));
        self.rich_tooltip_key = None;
        self.rich_tooltip_content = None;
        self.composite_tooltip = None;
        self
    }

    /// Per-tab rich-tooltip registry key. Returning `Some(key)` makes
    /// the tab show a rich tooltip resolved against
    /// [`TooltipRegistry`](crate::tooltip::TooltipRegistry).
    pub fn rich_tooltip_key(mut self, f: impl Fn(usize, &T) -> Option<String> + 'static) -> Self {
        self.rich_tooltip_key = Some(Box::new(f));
        self.tooltip = None;
        self.rich_tooltip_content = None;
        self.composite_tooltip = None;
        self
    }

    /// Per-tab inline rich-tooltip content. Skips the registry — useful
    /// for tooltips whose body depends on `T`'s state.
    pub fn rich_tooltip_content_with(
        mut self,
        f: impl Fn(usize, &T) -> Option<TooltipContent> + 'static,
    ) -> Self {
        self.rich_tooltip_content = Some(Box::new(f));
        self.tooltip = None;
        self.rich_tooltip_key = None;
        self.composite_tooltip = None;
        self
    }

    /// Per-tab composite-tooltip body factory. Returning
    /// `Some(boxed_widget)` makes the tab show a composite tooltip
    /// containing that subtree. The closure runs at tab-header build
    /// time (and on every rebuild after data changes), so the body
    /// can carry per-tab dynamic state.
    pub fn composite_tooltip_with(
        mut self,
        f: impl Fn(usize, &T) -> Option<Box<dyn Widget>> + 'static,
    ) -> Self {
        self.composite_tooltip = Some(Box::new(f));
        self.tooltip = None;
        self.rich_tooltip_key = None;
        self.rich_tooltip_content = None;
        self
    }

    pub(crate) fn resolve_label(&self, index: usize, item: &T) -> LocalizedString {
        (self.label)(index, item)
    }

    pub(crate) fn resolve_icon(&self, index: usize, item: &T) -> Option<IconWidget> {
        self.icon.as_ref().and_then(|f| f(index, item))
    }

    pub(crate) fn resolve_leading(&self, index: usize, item: &T) -> Option<Box<dyn Widget>> {
        self.leading.as_ref().and_then(|f| f(index, item))
    }

    pub(crate) fn resolve_trailing(&self, index: usize, item: &T) -> Option<Box<dyn Widget>> {
        self.trailing.as_ref().and_then(|f| f(index, item))
    }

    pub(crate) fn resolve_context_menu(
        &self,
        index: usize,
        item: &T,
    ) -> Option<ContextMenuFactory> {
        self.context_menu.as_ref().and_then(|f| f(index, item))
    }

    pub(crate) fn resolve_closable(&self, index: usize, item: &T) -> bool {
        self.closable
            .as_ref()
            .map(|f| f(index, item))
            .unwrap_or(false)
    }

    pub(crate) fn resolve_pinned(&self, index: usize, item: &T) -> bool {
        self.pinned
            .as_ref()
            .map(|f| f(index, item))
            .unwrap_or(false)
    }

    pub(crate) fn resolve_enabled(&self, index: usize, item: &T) -> bool {
        self.enabled
            .as_ref()
            .map(|f| f(index, item))
            .unwrap_or(true)
    }

    pub(crate) fn resolve_tooltip(&self, index: usize, item: &T) -> Option<LocalizedString> {
        self.tooltip.as_ref().and_then(|f| f(index, item))
    }

    pub(crate) fn resolve_rich_tooltip(&self, index: usize, item: &T) -> Option<RichTooltipSource> {
        if let Some(ref f) = self.rich_tooltip_key
            && let Some(k) = f(index, item)
        {
            return Some(RichTooltipSource::Key(k));
        }
        if let Some(ref f) = self.rich_tooltip_content
            && let Some(c) = f(index, item)
        {
            return Some(RichTooltipSource::Content(c));
        }
        None
    }

    pub(crate) fn resolve_composite_tooltip(
        &self,
        index: usize,
        item: &T,
    ) -> Option<Box<dyn Widget>> {
        self.composite_tooltip.as_ref().and_then(|f| f(index, item))
    }
}