teksilo_core/styles/table_style.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Tier-3 style protocol for `TableView` and `TreeTableView`. See
5//! `docs/styling-system.md`.
6//!
7//! Multi-method trait shared by both data-grid widgets. Chrome is split
8//! between *composed* widgets (one widget per header cell, sort indicator,
9//! and row band) and a *batched paint pass* for grid lines + frozen-column
10//! shadow. Grid lines genuinely need the batched path — composing one
11//! `RectWidget` per line on a 1000-row virtualized viewport would defeat
12//! the virtualization budget. The "recipe describes, widget paints the
13//! batched case" split applies here for specialty widgets.
14//!
15//! ## Wiring status
16//!
17//! The trait surface, the `TableGridRecipe`, and the
18//! `style_slots.table` slot are in place. Wiring `TableView` /
19//! `TreeTableView` through `make_*` is intentionally deferred. The
20//! widgets currently still own their cell / row / header / grid-line
21//! chrome directly; every dimension lives on
22//! `teksilo_widgets::styles::recipe_table_style` as `pub const`s.
23
24use std::rc::Rc;
25
26use teksilo_tokens::BorderRole;
27
28use crate::build_context::BuildContext;
29use crate::signal::Signal;
30use crate::widget_id::WidgetId;
31
32/// Sort direction for header cells.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum SortDirection {
35 Ascending,
36 Descending,
37}
38
39pub struct TableHeaderCellConfig {
40 pub label: WidgetId,
41 pub sort: Option<SortDirection>,
42 pub is_hovered: Signal<bool>,
43 pub is_resizing: Signal<bool>,
44}
45
46pub struct TableRowConfig {
47 pub index: usize,
48 pub is_selected: Signal<bool>,
49 pub is_hovered: Signal<bool>,
50 pub is_alt: bool,
51 /// Whether the view holds keyboard focus. `None` = "treat as always
52 /// focused" (the stock `TableView` paints its selection band directly and
53 /// passes `None` here). A custom style that paints row backgrounds reactively
54 /// can supply this — combined with [`is_window_active`](Self::is_window_active)
55 /// — to desaturate the selection (`SelectedInactive`) when focus is elsewhere.
56 pub is_focused: Option<Signal<bool>>,
57 /// Whether the host window is active (`focused AND not occluded`). `None` =
58 /// "treat as always active". Custom styles combine this with
59 /// [`is_focused`](Self::is_focused) so a selected row desaturates in a
60 /// background window, matching the stock views.
61 pub is_window_active: Option<Signal<bool>>,
62}
63
64/// Recipe — non-widget data describing the batched paint pass for
65/// grid lines and the frozen-column shadow. Consumed by
66/// `TableView::paint` / `TreeTableView::paint` directly. Custom styles
67/// override the entire recipe via `TableStyle::grid()`.
68#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct TableGridRecipe {
70 /// Vertical and horizontal grid-line stroke width.
71 pub line_thickness: f32,
72 /// Border role for grid lines. Defaults to `Divider`.
73 pub line_role: BorderRole,
74 /// Width of the shadow drawn at the frozen-column boundary. `0.0`
75 /// disables the shadow.
76 pub frozen_shadow_width: f32,
77}
78
79impl Default for TableGridRecipe {
80 fn default() -> Self {
81 Self {
82 line_thickness: 1.0,
83 line_role: BorderRole::Divider,
84 frozen_shadow_width: 4.0,
85 }
86 }
87}
88
89pub trait TableStyle: 'static {
90 fn make_header_cell(&self, cfg: &TableHeaderCellConfig, ctx: &mut BuildContext) -> WidgetId;
91 fn make_sort_indicator(&self, direction: SortDirection, ctx: &mut BuildContext) -> WidgetId;
92 /// Row-band chrome (selection / hover / alt) — composed *behind*
93 /// the cells.
94 fn make_row_background(&self, cfg: &TableRowConfig, ctx: &mut BuildContext) -> WidgetId;
95 /// Grid-line + frozen-column-shadow recipe — the table's own paint
96 /// pass batches over the virtualized viewport using this data.
97 fn grid(&self) -> TableGridRecipe;
98}
99
100pub type SharedTableStyle = Rc<dyn TableStyle>;