use fission::core::op::AlignItems;
use fission::icons::material;
use fission::prelude::*;
use crate::{OnboardingLabels, OnboardingStep, OnboardingStyle};
#[derive(Clone)]
pub(crate) struct OnboardingCard {
pub id: String,
pub step: OnboardingStep,
pub labels: OnboardingLabels,
pub style: OnboardingStyle,
pub first_step: bool,
pub last_step: bool,
pub busy: bool,
pub continue_enabled: bool,
pub contextual_action: Option<(TextContent, ActionEnvelope)>,
pub contextual_status: Option<TextContent>,
pub on_back: Option<ActionEnvelope>,
pub on_continue: ActionEnvelope,
pub on_skip: Option<ActionEnvelope>,
pub on_complete: ActionEnvelope,
}
impl From<OnboardingCard> for Widget {
fn from(card: OnboardingCard) -> Self {
let (_, view) = fission::build::current::<()>();
let back_label = resolve_text(view.env(), &card.labels.back);
let continue_label = resolve_text(
view.env(),
if card.last_step {
&card.labels.finish
} else {
&card.labels.continue_label
},
);
let skip_label = resolve_text(view.env(), &card.labels.skip);
let back: Widget = if card.first_step {
Spacer::default().into()
} else {
Button {
child: Some(
Row {
gap: Some(card.style.control_gap),
children: vec![
Icon::svg(material::navigation::arrow_back::regular())
.size(card.style.icon_size)
.color(card.style.text)
.into(),
Text::new(card.labels.back.clone())
.size(card.style.body_size)
.color(card.style.text)
.into(),
],
..Default::default()
}
.into(),
),
on_press: (!card.busy).then(|| card.on_back.clone()).flatten(),
semantics: Some(button_semantics(
format!("{}-back", card.id),
back_label,
card.busy,
)),
variant: ButtonVariant::SecondaryGray,
size: ComponentSize::Sm,
disabled: card.busy,
..Default::default()
}
.into()
};
let continue_action = if card.last_step {
card.on_complete.clone()
} else {
card.on_continue.clone()
};
let continue_disabled = card.busy || !card.continue_enabled;
let next: Widget = Button {
child: Some(
Row {
gap: Some(card.style.control_gap),
children: vec![
Text::new(if card.last_step {
card.labels.finish.clone()
} else {
card.labels.continue_label.clone()
})
.size(card.style.body_size)
.color(card.style.primary_text)
.into(),
Icon::svg(if card.last_step {
material::action::check_circle::outlined()
} else {
material::navigation::arrow_forward::regular()
})
.size(card.style.icon_size)
.color(card.style.primary_text)
.into(),
],
..Default::default()
}
.into(),
),
on_press: (!continue_disabled).then_some(continue_action),
semantics: Some(button_semantics(
format!("{}-continue", card.id),
continue_label.clone(),
continue_disabled,
)),
variant: ButtonVariant::Primary,
size: ComponentSize::Sm,
disabled: continue_disabled,
..Default::default()
}
.into();
let skip: Widget = if let Some(action) = card.on_skip {
Button {
child: Some(
Text::new(card.labels.skip.clone())
.size(card.style.body_size)
.color(card.style.text_muted)
.into(),
),
on_press: (!card.busy).then_some(action),
semantics: Some(button_semantics(
format!("{}-skip", card.id),
skip_label,
card.busy,
)),
variant: ButtonVariant::TertiaryGray,
size: ComponentSize::Sm,
disabled: card.busy,
..Default::default()
}
.into()
} else {
Spacer::default().into()
};
let busy_indicator = |suffix: &str| -> Widget {
if card.busy {
Spinner {
id: WidgetId::explicit(&format!("{}-{suffix}-saving", card.id)),
color: Some(card.style.focus_ring),
motion: Some(SpinnerMotion::Default),
label: Some(continue_label.clone()),
}
.into()
} else {
Spacer::default().into()
}
};
let busy_wide = busy_indicator("wide");
let busy_compact = busy_indicator("compact");
let wide_actions: Widget = Row {
gap: Some(card.style.control_gap),
align_items: AlignItems::Center,
children: vec![
busy_wide,
skip.clone(),
Spacer {
flex_grow: 1.0,
..Default::default()
}
.into(),
back.clone(),
next.clone(),
],
..Default::default()
}
.into();
let compact_actions: Widget = Column {
gap: Some(card.style.control_gap),
children: vec![
Row {
gap: Some(card.style.control_gap),
align_items: AlignItems::Center,
children: vec![
busy_compact,
skip,
Spacer {
flex_grow: 1.0,
..Default::default()
}
.into(),
],
..Default::default()
}
.into(),
Row {
gap: Some(card.style.control_gap),
align_items: AlignItems::Center,
children: vec![
Spacer {
flex_grow: 1.0,
..Default::default()
}
.into(),
back,
next,
],
..Default::default()
}
.into(),
],
..Default::default()
}
.into();
let actions = if view.viewport_size().width <= card.style.compact_actions_below {
compact_actions
} else {
wide_actions
};
let mut content_children = vec![
Text::new(card.labels.progress.clone())
.size(card.style.progress_size)
.color(card.style.text_muted)
.semantics_identifier(format!("{}-progress", card.id))
.into(),
Text::new(card.step.title)
.size(card.style.title_size)
.weight(card.style.title_weight)
.color(card.style.text)
.into(),
Text::new(card.step.body)
.size(card.style.body_size)
.color(card.style.text_muted)
.into(),
];
if let Some(status) = card.contextual_status {
content_children.push(
Text::new(status)
.size(card.style.body_size)
.weight(card.style.title_weight)
.color(card.style.text)
.semantics_identifier(format!("{}-status", card.id))
.into(),
);
}
if let Some((label, action)) = card.contextual_action {
let resolved_label = resolve_text(view.env(), &label);
content_children.push(
Button {
child: Some(
Text::new(label)
.size(card.style.body_size)
.color(card.style.primary_text)
.into(),
),
on_press: (!card.busy).then_some(action),
semantics: Some(button_semantics(
format!("{}-context-action", card.id),
resolved_label,
card.busy,
)),
variant: ButtonVariant::Primary,
size: ComponentSize::Sm,
disabled: card.busy,
..Default::default()
}
.into(),
);
}
content_children.push(actions);
let content = Column {
gap: Some(card.style.card_gap),
children: content_children,
..Default::default()
};
let mut surface = Container::new(content)
.width_length(card.style.card_width)
.bg(card.style.surface)
.border(card.style.border, 1.0)
.border_radius(card.style.card_radius)
.padding_all(card.style.card_padding);
if let Some(shadow) = card.style.shadow {
surface = surface.shadow(shadow);
}
SemanticsRegion::new(surface)
.identifier(card.id)
.label(card.labels.dialog_label)
.role(Role::Dialog)
.into()
}
}
fn button_semantics(identifier: String, label: String, disabled: bool) -> Semantics {
Semantics {
role: Role::Button,
identifier: Some(identifier),
label: Some(label),
focusable: !disabled,
disabled,
..Default::default()
}
}
fn resolve_text(env: &Env, content: &TextContent) -> String {
match content {
TextContent::Literal(value) => value.clone(),
TextContent::Key(key) => env
.i18n
.get(&env.locale, key)
.unwrap_or(key.as_str())
.to_string(),
TextContent::KeyWithFallback { key, fallback } => env
.i18n
.get(&env.locale, key)
.unwrap_or(fallback.as_str())
.to_string(),
}
}