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
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech
//! Tier-3 style protocol for `StandardListItem` / `StandardTreeItem`.
//! See `docs/styling-system.md`.
use std::rc::Rc;
use teksilo_tokens::{InputTokens, TargetRole, TextRole};
use crate::build_context::BuildContext;
use crate::signal::Signal;
use crate::styles::density::dp;
use crate::widget_id::WidgetId;
#[derive(Clone, Debug)]
pub struct StandardItemStyleConfig {
/// Pre-built row content — typically an HStack of `[checkbox?]
/// [leading?] [center?] [label / VStack { label, subtitle row }]
/// [Spacer] [trailing?]` composed by the host
/// `StandardListItem` / `StandardTreeItem`. The style is
/// responsible for the chrome (selection background, corner
/// radius, padding) but not for row-internal layout — the
/// per-slot composition is StandardItem-specific (subtitle has
/// its own sub-row, the checkbox carries `labelled_externally` AT
/// metadata, etc.) and would force every custom style to
/// reimplement it.
pub content: WidgetId,
pub is_selected: Signal<bool>,
pub is_hovered: Signal<bool>,
pub is_pressed: Signal<bool>,
pub is_focused: Signal<bool>,
/// Input-modality "focus-visible": `true` after keyboard input. A focus ring
/// should render only when this and `is_focused` are both true, so a mouse
/// click selects without a ring while keyboard navigation reveals one.
pub is_focus_visible: Signal<bool>,
pub is_disabled: Signal<bool>,
/// Whether the host window is currently active (`focused AND not occluded`).
/// Composed with `is_focused` so a selected row shows the vivid `Selected`
/// surface only while the view holds keyboard focus **and** the window is
/// active; otherwise it falls back to the muted `SelectedInactive` — the
/// same desaturation a view-focus loss produces (macOS "unemphasized"
/// selection serves both states). Populated from
/// `BuildContext::window_active_signal`.
///
/// Which of the two signals a style composes is its
/// [`StandardItemStyle::selection_emphasis`]; build the condition with
/// [`SelectionEmphasis::emphasised`] rather than by hand, so the chrome
/// and the row's label (which the row composes from the same hook) agree.
pub is_window_active: Signal<bool>,
}
/// When a selected row is drawn **emphasised**: the vivid `Selected`
/// surface (and [`StandardItemStyle::selected_label_role`] on its
/// foregrounds) rather than the muted `SelectedInactive` one.
///
/// Design languages disagree on what mutes a selection. macOS, IntUI and
/// Fluent mute it whenever the view loses keyboard focus, as well as when
/// the window does. Qt picks a palette colour group per *window*: every
/// view in an active window paints its selection with the active
/// `Highlight`, whichever of them holds focus, so KDE's Breeze mutes a
/// selection only when the window goes inactive.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum SelectionEmphasis {
/// Emphasised while the row's view holds keyboard focus **and** its
/// window is active. The default, and the rule every shipped style
/// followed before the choice existed.
#[default]
ViewFocusedAndWindowActive,
/// Emphasised while the window is active, whether or not the view holds
/// keyboard focus: Qt's per-window colour groups.
WindowActive,
}
impl SelectionEmphasis {
/// The emphasis condition as a signal, from the view-focus signal (a
/// row's [`StandardItemStyleConfig::is_focused`]) and the window-active
/// signal ([`StandardItemStyleConfig::is_window_active`]).
///
/// A selected row is emphasised while this reads `true`. Selection
/// itself is not part of it: the caller combines that, since the chrome
/// and the label each already hold it.
pub fn emphasised(
self,
view_focused: &Signal<bool>,
window_active: &Signal<bool>,
) -> Signal<bool> {
match self {
Self::ViewFocusedAndWindowActive => view_focused.and(window_active),
Self::WindowActive => window_active.clone(),
}
}
}
pub trait StandardItemStyle: 'static {
fn make_body(&self, cfg: &StandardItemStyleConfig, ctx: &mut BuildContext) -> WidgetId;
/// The text role a row's label takes while the row is **emphasised**
/// — selected, with its view focused and its window active. `None`
/// (the default) keeps the row's own mapping, `TextRole::Primary`.
///
/// The row builds its label before a style ever sees it, so a style
/// that fills the selection with a saturated colour cannot recolour
/// the text on top of it. Design languages whose selected row is a
/// solid accent fill with a light label — macOS's
/// `alternateSelectedControlTextColor` is the canonical case — return
/// [`TextRole::OnAccent`] here; ones whose selection is a pale wash
/// (IntUI, Fluent) leave it `None` and the label keeps its contrast
/// against the wash.
///
/// Same shape as
/// [`ButtonStyle::label_text_role`](crate::styles::ButtonStyle::label_text_role),
/// and defaulted for the same reason: an existing style needs no
/// change.
fn selected_label_role(&self) -> Option<TextRole> {
None
}
/// When a selected row is drawn emphasised; see [`SelectionEmphasis`].
///
/// The row reads this for its foregrounds (the label, the subtitle and
/// the tree chevron take [`selected_label_role`](Self::selected_label_role)
/// only while the row is emphasised), and a style reads it for its own
/// chrome through [`SelectionEmphasis::emphasised`], so a solid
/// selection fill and the text on it stay in step.
///
/// **Defaulted** to [`SelectionEmphasis::ViewFocusedAndWindowActive`],
/// the rule every style followed before this existed, so an existing
/// style needs no change. The shipped `RecipeStandardItemStyle` consults
/// its own value, which is always the default: a style that returns
/// another value and delegates its chrome to the recipe gets the
/// recipe's muting, and has to paint its selection itself to follow its
/// own rule.
fn selection_emphasis(&self) -> SelectionEmphasis {
SelectionEmphasis::ViewFocusedAndWindowActive
}
/// The height floor of a row with no subtitle, in logical pixels, at the
/// density `tokens` describe.
///
/// A metrics accessor rather than a `make_body` concern: the row measures
/// itself in `layout_response`, long after the chrome is built, and holds
/// an `Rc<dyn StandardItemStyle>` that cannot reach a recipe field.
/// Without it a preset's row height is written and never rendered.
///
/// **Defaulted**, so a `StandardItemStyle` implemented outside this
/// workspace keeps compiling and keeps the ladder it had. The default is
/// `teksilo_widgets::styles::recipe_standard_item_style::STANDARD_ITEM_MIN_HEIGHT_SINGLE_LINE`
/// put through [`dp`] as a target, restated as a literal because
/// `teksilo-core` cannot name a `teksilo-widgets` constant, and pinned
/// equal to it by
/// `the_standard_item_trait_defaults_restate_the_module_constants`.
fn min_height_single_line(&self, tokens: &InputTokens) -> f32 {
dp(28.0, TargetRole::Target, tokens)
}
/// The height floor of a row with no subtitle that carries a leading
/// decoration, a check box or a leading slot (an icon, typically), in
/// logical pixels, at the density `tokens` describe.
///
/// Some design languages make such a row taller than a row of text
/// alone: Qt adds 2 px for an item's icon, so a KDE Breeze row is 24 px
/// with text only and 26 px with an icon or a check box.
///
/// **Defaulted** to [`min_height_single_line`](Self::min_height_single_line),
/// so a style that does not tell the two apart keeps one floor and an
/// existing style needs no change.
fn min_height_decorated(&self, tokens: &InputTokens) -> f32 {
self.min_height_single_line(tokens)
}
/// The height floor of a row carrying a subtitle, in logical pixels.
/// Defaulted on the same terms as
/// [`min_height_single_line`](Self::min_height_single_line), from
/// `STANDARD_ITEM_MIN_HEIGHT_TWO_LINE`.
fn min_height_two_line(&self, tokens: &InputTokens) -> f32 {
dp(44.0, TargetRole::Target, tokens)
}
}
pub type SharedStandardItemStyle = Rc<dyn StandardItemStyle>;