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
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech
//! Tier-3 style protocol for `Avatar`. See `docs/styling-system.md`.
//!
//! The style owns everything *around* the inner content: the shape's
//! background fill (with the hash-derived palette pick when no caller
//! override is supplied), the border ring, the keyboard focus ring,
//! and the presence indicator dot. The `Avatar` widget builds the
//! inner content (`InitialsLeaf` or an `ImageWidget`) and passes it
//! in as a pre-built `content` id; the style composes its chrome
//! around that content.
//!
//! Avatar's domain enums (`AvatarShape`, `AvatarSize`, `AvatarPresence`,
//! `AvatarCorner`) live here so the config can carry them and custom
//! `AvatarStyle` implementations can branch on them.
use std::rc::Rc;
use teksilo_tokens::Color;
use crate::build_context::BuildContext;
use crate::color_prop::ColorProp;
use crate::signal::Signal;
use crate::widget_id::WidgetId;
/// Discrete avatar size variants. `Custom(px)` accepts an arbitrary
/// logical-pixel side length. The default size resolution table lives
/// in `teksilo_widgets::styles::recipe_avatar_style::avatar_pixel_size`.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum AvatarSize {
/// Small — list rows, mention chips.
Small,
/// Medium — comment threads, sidebars (default).
#[default]
Medium,
/// Large — profile cards.
Large,
/// X-large — settings, "your account" headers.
XLarge,
/// Arbitrary side length.
Custom(f32),
}
/// Outer outline.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum AvatarShape {
#[default]
Circle,
RoundedSquare,
Square,
}
/// Presence indicator dot drawn at one corner of the avatar.
#[derive(Debug, Clone)]
pub enum AvatarPresence {
Online,
Offline,
Away,
Busy,
Custom { color: ColorProp, label: String },
}
impl AvatarPresence {
/// Resolve the dot's fill colour against the active theme.
pub fn color(&self, theme: &crate::styles::Theme) -> Color {
match self {
AvatarPresence::Online => theme.colors.status_success_fg,
AvatarPresence::Offline => theme.colors.text_disabled,
AvatarPresence::Away => theme.colors.status_warning_fg,
AvatarPresence::Busy => theme.colors.status_error_fg,
// The Avatar widget calls this helper from its paint() but
// doesn't currently thread `effective_enabled` here. When
// the Avatar composite migrates (commit 4 of the
// enabled-state refactor) this signature widens to take
// `enabled: bool` and the Custom presence respects it.
AvatarPresence::Custom { color, .. } => color.resolve(theme, true),
}
}
/// Accessible label for screen readers.
pub fn label(&self) -> String {
match self {
AvatarPresence::Online => "Online".to_string(),
AvatarPresence::Offline => "Offline".to_string(),
AvatarPresence::Away => "Away".to_string(),
AvatarPresence::Busy => "Busy".to_string(),
AvatarPresence::Custom { label, .. } => label.clone(),
}
}
}
/// Where the presence dot is rendered relative to the avatar bounds.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum AvatarCorner {
#[default]
BottomTrailing,
BottomLeading,
TopTrailing,
TopLeading,
}
impl AvatarCorner {
/// `(x_factor, y_factor)` in `{-1, 1}` — `-1` = leading/top, `1` =
/// trailing/bottom. Used by the recipe to position the presence
/// dot.
pub fn offset(self) -> (f32, f32) {
match self {
AvatarCorner::BottomTrailing => (1.0, 1.0),
AvatarCorner::BottomLeading => (-1.0, 1.0),
AvatarCorner::TopTrailing => (1.0, -1.0),
AvatarCorner::TopLeading => (-1.0, -1.0),
}
}
}
#[derive(Clone, Debug)]
pub struct AvatarStyleConfig {
pub shape: AvatarShape,
pub size: AvatarSize,
/// Pre-built content subtree (`InitialsLeaf` or `ImageWidget`).
pub content: WidgetId,
/// Current presence (if any). The widget passes the live value
/// resolved from any bound signal at build time; reactive presence
/// changes re-run `Avatar::build` so the chrome rebuilds with the
/// new value.
pub presence: Option<AvatarPresence>,
pub presence_corner: AvatarCorner,
/// `true` while the avatar holds keyboard focus — drives the
/// outer focus ring.
pub is_focused: Signal<bool>,
/// Caller override for the background fill. `None` lets the
/// recipe pick a colour from the chart palette using `seed`.
pub background_override: Option<ColorProp>,
/// Caller override for the border ring colour. `None` lets the
/// recipe use `theme.colors.surface_main`.
pub border_color_override: Option<ColorProp>,
/// Caller override for the border ring width. `None` = no border.
pub border_width_override: Option<f32>,
/// Seed string for the hash-derived background palette pick
/// (typically the avatar's name or initials).
pub seed: String,
}
pub trait AvatarStyle: 'static {
fn make_body(&self, cfg: &AvatarStyleConfig, ctx: &mut BuildContext) -> WidgetId;
}
pub type SharedAvatarStyle = Rc<dyn AvatarStyle>;