1use std::rc::Rc;
33
34use teksilo_canvas::{Rect, Size, SizeProposal};
35use teksilo_core::accessibility::AccessNodeBuilder;
36use teksilo_core::build_context::BuildContext;
37use teksilo_core::color_prop::ColorProp;
38use teksilo_core::styles::{BadgeStyleConfig, SharedBadgeStyle};
39use teksilo_core::widget::{LayoutContext, Widget, WidgetPlacement};
40use teksilo_core::widget_id::WidgetId;
41use teksilo_tokens::TextStyleRole;
42
43use crate::primitives::TextWidget;
44use teksilo_i18n::LocalizedString;
45
46pub struct Badge {
48 label: LocalizedString,
49 background: Option<ColorProp>,
50 text_role: Option<ColorProp>,
51 text_style: Option<teksilo_core::color_prop::TextStyleProp>,
54 style_override: Option<SharedBadgeStyle>,
56 root_child_id: Option<WidgetId>,
57 tooltip_text: Option<LocalizedString>,
61 rich_tooltip_source: Option<crate::tooltip::RichTooltipSource>,
63 composite_tooltip_content: Option<Box<dyn Widget>>,
65}
66
67impl Badge {
68 pub fn new(label: impl Into<LocalizedString>) -> Self {
70 Self {
71 label: label.into(),
72 background: None,
73 text_role: None,
74 text_style: None,
75 style_override: None,
76 root_child_id: None,
77 tooltip_text: None,
78 rich_tooltip_source: None,
79 composite_tooltip_content: None,
80 }
81 }
82
83 pub fn style(mut self, style: impl teksilo_core::styles::BadgeStyle) -> Self {
86 self.style_override = Some(Rc::new(style));
87 self
88 }
89
90 pub fn background(mut self, color: impl Into<ColorProp>) -> Self {
94 self.background = Some(color.into());
95 self
96 }
97
98 pub fn text_role(mut self, color: impl Into<ColorProp>) -> Self {
101 self.text_role = Some(color.into());
102 self
103 }
104
105 pub fn text_style(mut self, style: impl Into<teksilo_core::color_prop::TextStyleProp>) -> Self {
109 self.text_style = Some(style.into());
110 self
111 }
112
113 pub fn tooltip(mut self, text: impl Into<LocalizedString>) -> Self {
119 self.tooltip_text = Some(text.into());
120 self.rich_tooltip_source = None;
121 self.composite_tooltip_content = None;
122 self
123 }
124
125 pub fn rich_tooltip(mut self, key: impl Into<String>) -> Self {
131 self.rich_tooltip_source = Some(crate::tooltip::RichTooltipSource::Key(key.into()));
132 self.tooltip_text = None;
133 self.composite_tooltip_content = None;
134 self
135 }
136
137 pub fn rich_tooltip_content(mut self, content: crate::tooltip::TooltipContent) -> Self {
143 self.rich_tooltip_source = Some(crate::tooltip::RichTooltipSource::Content(content));
144 self.tooltip_text = None;
145 self.composite_tooltip_content = None;
146 self
147 }
148
149 pub fn composite_tooltip(mut self, content: impl Widget + 'static) -> Self {
155 self.composite_tooltip_content = Some(Box::new(content));
156 self.tooltip_text = None;
157 self.rich_tooltip_source = None;
158 self
159 }
160}
161
162impl std::fmt::Debug for Badge {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct("Badge").field("label", &self.label).finish()
165 }
166}
167
168impl Widget for Badge {
169 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
170 let theme_signal = ctx.theme_signal();
171
172 let text: ColorProp = self
178 .text_role
179 .take()
180 .unwrap_or_else(|| ColorProp::Bound(theme_signal.map(|t| t.colors.status_info_fg)));
181
182 let mut text_widget = TextWidget::new(self.label.clone())
183 .color(text)
184 .single_line()
185 .a11y_hidden();
186 text_widget = match &self.text_style {
187 Some(style) => text_widget.style(style.clone()),
188 None => text_widget.style(TextStyleRole::Tiny),
189 };
190 let content = ctx.add(text_widget);
191
192 let style: SharedBadgeStyle = self
195 .style_override
196 .clone()
197 .or_else(|| ctx.theme().style_slots.badge.clone())
198 .unwrap_or_else(|| Rc::new(crate::styles::RecipeBadgeStyle::default()));
199 let root = style.make_body(
200 &BadgeStyleConfig {
201 content,
202 background_override: self.background.take(),
203 },
204 ctx,
205 );
206 self.root_child_id = Some(root);
207
208 if let Some(content) = self.composite_tooltip_content.take() {
209 let delay = ctx.theme().motion.tooltip_delay_heavy;
210 crate::tooltip::attach_composite_tooltip_boxed(ctx, root, content, delay);
211 } else if let Some(source) = self.rich_tooltip_source.clone() {
212 let delay = ctx.theme().motion.tooltip_delay;
213 crate::tooltip::attach_rich_tooltip_source(ctx, root, source, delay);
214 } else if let Some(text) = self.tooltip_text.clone() {
215 let delay = ctx.theme().motion.tooltip_delay;
216 crate::tooltip::attach_plain_tooltip(ctx, root, text, delay);
217 }
218
219 vec![root]
220 }
221
222 fn layout_response(
223 &self,
224 proposal: SizeProposal,
225 ctx: &LayoutContext,
226 ) -> teksilo_core::widget::LayoutResponse {
227 if let Some(root) = self.root_child_id
229 && let Some(size) = ctx.child_size(root, proposal)
230 {
231 return (size).into();
232 }
233 proposal.resolve(0.0, 0.0).into()
234 }
235
236 fn place_children(
237 &self,
238 bounds: Rect,
239 _proposal: SizeProposal,
240 children: &mut [WidgetPlacement],
241 _ctx: &LayoutContext,
242 ) {
243 for child in children.iter_mut() {
244 child.origin = teksilo_canvas::Point::new(bounds.x, bounds.y);
245 child.size = Size::new(bounds.width, bounds.height);
246 }
247 }
248
249 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
250 builder.set_role(teksilo_core::accesskit::Role::Label);
251 builder.set_name(self.label.resolve_now());
252 }
253
254 fn children(&self) -> Vec<WidgetId> {
255 self.root_child_id.into_iter().collect()
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use teksilo_core::widget_tree::WidgetTree;
263 use teksilo_i18n::lit;
264
265 #[test]
266 fn badge_builds_and_renders() {
267 let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
268 let badge = tree.add(Badge::new(lit!("New")));
269 tree.layout(SizeProposal::exact(200.0, 50.0));
270 let b = tree.bounds(badge);
271 assert!(b.width > 0.0);
272 assert!(b.height > 0.0);
273 }
274
275 #[test]
276 fn badge_accessibility() {
277 let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
278 let badge = tree.add(Badge::new(lit!("3")));
279 tree.layout(SizeProposal::exact(200.0, 50.0));
280 let info = tree.accessibility_node(badge);
281 assert_eq!(info.role(), teksilo_core::accesskit::Role::Label);
282 assert_eq!(info.name(), Some("3"));
283 }
284
285 #[test]
286 fn tooltip_appears_on_hover() {
287 let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
288 let id = tree.add(Badge::new(lit!("New")).tooltip(lit!("Tip")));
289 tree.layout(SizeProposal::exact(300.0, 200.0));
290 tree.pointer_move(tree.bounds(id).center());
291 tree.advance_time(std::time::Duration::from_secs(1));
292 assert_eq!(
293 tree.active_overlays().len(),
294 1,
295 "tooltip should appear on hover"
296 );
297 assert!(tree.find_by_label("Tip").is_some());
298 }
299}