frus_widgets/
breadcrumb.rs1use frus_core::{Point, Rect, ResolvedTextStyle, Scene, TextStyle};
5use frus_layout::{Align, Dimension, FlexDirection, Style};
6
7use crate::interaction::Status;
8use crate::theme::Theme;
9use crate::widget::Widget;
10
11fn label_style(over: Option<TextStyle>, theme: Option<&Theme>) -> ResolvedTextStyle {
22 over.or(theme.and_then(|t| t.widgets.breadcrumb.text_style))
23 .unwrap_or_else(|| crate::theme::type_scale(theme).body_medium)
24 .resolved()
25}
26
27struct Crumb<Msg> {
29 label: String,
30 message: Option<Msg>,
32 separator: bool,
33 text_style: Option<TextStyle>,
34}
35
36impl<Msg> Crumb<Msg> {
37 fn sizing(&self, theme: Option<&Theme>) -> Style {
38 let measured =
39 frus_text::measure_resolved(&self.label, &label_style(self.text_style, theme));
40 Style {
41 width: Dimension::Length(measured.width.ceil()),
42 height: Dimension::Length(measured.height.ceil()),
43 ..Default::default()
44 }
45 }
46}
47
48impl<Msg: Clone> Widget<Msg> for Crumb<Msg> {
49 fn style(&self) -> Style {
50 self.sizing(None)
51 }
52
53 fn style_themed(&self, theme: &Theme) -> Style {
54 self.sizing(Some(theme))
55 }
56
57 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
58 &[]
59 }
60
61 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
62 let o = status.opacity;
63 let color = if self.separator {
64 theme.muted
65 } else if self.message.is_some() {
66 theme.muted.lerp(theme.on_surface, status.hover_progress)
68 } else {
69 theme.on_surface
71 };
72 scene.text(
73 Point::new(bounds.x, bounds.y),
74 self.label.clone(),
75 &label_style(self.text_style, Some(theme)),
76 color.fade(o),
77 );
78 }
79
80 fn on_click(&self) -> Option<Msg> {
81 self.message.clone()
82 }
83
84 fn focusable(&self) -> bool {
85 self.message.is_some()
86 }
87}
88
89pub struct Breadcrumb<Msg = crate::callback::Callback> {
91 on_select: Box<dyn Fn(usize) -> Msg>,
92 labels: Vec<String>,
93 text_style: Option<TextStyle>,
94 children: Vec<Box<dyn Widget<Msg>>>,
95}
96
97impl<Msg: Clone + 'static> Breadcrumb<Msg> {
98 pub fn new<R: crate::callback::IntoMsg<Msg>>(on_select: impl Fn(usize) -> R + 'static) -> Self {
100 Self {
101 on_select: Box::new(crate::callback::handler1(on_select)),
102 labels: Vec::new(),
103 text_style: None,
104 children: Vec::new(),
105 }
106 }
107
108 #[must_use]
110 pub fn text_style(mut self, style: TextStyle) -> Self {
111 self.text_style = Some(style);
112 self.rebuild();
113 self
114 }
115
116 pub fn crumb(mut self, label: impl Into<String>) -> Self {
118 self.labels.push(label.into());
119 self.rebuild();
120 self
121 }
122
123 fn rebuild(&mut self) {
124 let last = self.labels.len().saturating_sub(1);
125 let mut children: Vec<Box<dyn Widget<Msg>>> = Vec::new();
126 for (i, label) in self.labels.iter().enumerate() {
127 if i > 0 {
128 children.push(Box::new(Crumb {
129 label: "›".to_string(),
130 message: None,
131 separator: true,
132 text_style: self.text_style,
133 }));
134 }
135 children.push(Box::new(Crumb {
136 label: label.clone(),
137 message: if i == last {
138 None
139 } else {
140 Some((self.on_select)(i))
141 },
142 separator: false,
143 text_style: self.text_style,
144 }));
145 }
146 self.children = children;
147 }
148}
149
150impl<Msg: Clone> Widget<Msg> for Breadcrumb<Msg> {
151 fn style(&self) -> Style {
152 Style {
153 flex_direction: FlexDirection::Row,
154 align: Align::Center,
155 gap: 6.0,
156 ..Default::default()
157 }
158 }
159
160 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
161 &self.children
162 }
163
164 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
165
166 fn on_click(&self) -> Option<Msg> {
167 None
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[derive(Clone, Debug, PartialEq)]
176 enum Msg {
177 Go(usize),
178 }
179
180 #[test]
181 fn links_click_but_current_does_not() {
182 let bc = Breadcrumb::new(Msg::Go)
183 .crumb("Home")
184 .crumb("Settings")
185 .crumb("Advanced");
186 let children = Widget::<Msg>::children(&bc);
187 assert_eq!(children.len(), 5);
189 assert_eq!(children[0].on_click(), Some(Msg::Go(0)));
191 assert_eq!(children[4].on_click(), None);
193 }
194}