use frus_core::{Point, Rect, ResolvedTextStyle, Scene, TextStyle};
use frus_layout::{Align, Dimension, FlexDirection, Style};
use crate::interaction::Status;
use crate::theme::Theme;
use crate::widget::Widget;
fn label_style(over: Option<TextStyle>, theme: Option<&Theme>) -> ResolvedTextStyle {
over.or(theme.and_then(|t| t.widgets.breadcrumb.text_style))
.unwrap_or_else(|| crate::theme::type_scale(theme).body_medium)
.resolved()
}
struct Crumb<Msg> {
label: String,
message: Option<Msg>,
separator: bool,
text_style: Option<TextStyle>,
}
impl<Msg> Crumb<Msg> {
fn sizing(&self, theme: Option<&Theme>) -> Style {
let measured =
frus_text::measure_resolved(&self.label, &label_style(self.text_style, theme));
Style {
width: Dimension::Length(measured.width.ceil()),
height: Dimension::Length(measured.height.ceil()),
..Default::default()
}
}
}
impl<Msg: Clone> Widget<Msg> for Crumb<Msg> {
fn style(&self) -> Style {
self.sizing(None)
}
fn style_themed(&self, theme: &Theme) -> Style {
self.sizing(Some(theme))
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&[]
}
fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
let o = status.opacity;
let color = if self.separator {
theme.muted
} else if self.message.is_some() {
theme.muted.lerp(theme.on_surface, status.hover_progress)
} else {
theme.on_surface
};
scene.text(
Point::new(bounds.x, bounds.y),
self.label.clone(),
&label_style(self.text_style, Some(theme)),
color.fade(o),
);
}
fn on_click(&self) -> Option<Msg> {
self.message.clone()
}
fn focusable(&self) -> bool {
self.message.is_some()
}
}
pub struct Breadcrumb<Msg = crate::callback::Callback> {
on_select: Box<dyn Fn(usize) -> Msg>,
labels: Vec<String>,
text_style: Option<TextStyle>,
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg: Clone + 'static> Breadcrumb<Msg> {
pub fn new<R: crate::callback::IntoMsg<Msg>>(on_select: impl Fn(usize) -> R + 'static) -> Self {
Self {
on_select: Box::new(crate::callback::handler1(on_select)),
labels: Vec::new(),
text_style: None,
children: Vec::new(),
}
}
#[must_use]
pub fn text_style(mut self, style: TextStyle) -> Self {
self.text_style = Some(style);
self.rebuild();
self
}
pub fn crumb(mut self, label: impl Into<String>) -> Self {
self.labels.push(label.into());
self.rebuild();
self
}
fn rebuild(&mut self) {
let last = self.labels.len().saturating_sub(1);
let mut children: Vec<Box<dyn Widget<Msg>>> = Vec::new();
for (i, label) in self.labels.iter().enumerate() {
if i > 0 {
children.push(Box::new(Crumb {
label: "›".to_string(),
message: None,
separator: true,
text_style: self.text_style,
}));
}
children.push(Box::new(Crumb {
label: label.clone(),
message: if i == last {
None
} else {
Some((self.on_select)(i))
},
separator: false,
text_style: self.text_style,
}));
}
self.children = children;
}
}
impl<Msg: Clone> Widget<Msg> for Breadcrumb<Msg> {
fn style(&self) -> Style {
Style {
flex_direction: FlexDirection::Row,
align: Align::Center,
gap: 6.0,
..Default::default()
}
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug, PartialEq)]
enum Msg {
Go(usize),
}
#[test]
fn links_click_but_current_does_not() {
let bc = Breadcrumb::new(Msg::Go)
.crumb("Home")
.crumb("Settings")
.crumb("Advanced");
let children = Widget::<Msg>::children(&bc);
assert_eq!(children.len(), 5);
assert_eq!(children[0].on_click(), Some(Msg::Go(0)));
assert_eq!(children[4].on_click(), None);
}
}