use std::rc::Rc;
use teksilo_canvas::{Rect, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::build_context::BuildContext;
use teksilo_core::signal::Signal;
use teksilo_core::widget::{EventContext, LayoutContext, LayoutResponse, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
use teksilo_i18n::LocalizedString;
use super::controller::StepperController;
use super::nav::StepNav;
use crate::button::{Button, ButtonVariant};
use crate::primitives::{HStack, Spacer};
pub(crate) type FooterAction = Rc<dyn Fn(&mut EventContext, &StepperController)>;
pub(crate) struct StepperFooter {
nav: Rc<StepNav>,
optional_flags: Vec<bool>,
back_label: LocalizedString,
next_label: LocalizedString,
finish_label: LocalizedString,
skip_label: LocalizedString,
help_label: Option<LocalizedString>,
cancel_label: Option<LocalizedString>,
help_action: Option<FooterAction>,
cancel_action: Option<FooterAction>,
root_child_id: Option<WidgetId>,
}
impl StepperFooter {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
nav: Rc<StepNav>,
optional_flags: Vec<bool>,
back_label: LocalizedString,
next_label: LocalizedString,
finish_label: LocalizedString,
skip_label: LocalizedString,
help_label: Option<LocalizedString>,
cancel_label: Option<LocalizedString>,
help_action: Option<FooterAction>,
cancel_action: Option<FooterAction>,
) -> Self {
Self {
nav,
optional_flags,
back_label,
next_label,
finish_label,
skip_label,
help_label,
cancel_label,
help_action,
cancel_action,
root_child_id: None,
}
}
fn controller(&self) -> StepperController {
self.nav.controller().clone()
}
}
impl std::fmt::Debug for StepperFooter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StepperFooter")
.field("steps", &self.optional_flags.len())
.finish()
}
}
impl Widget for StepperFooter {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
if self.optional_flags.is_empty() {
return Vec::new();
}
let controller = self.controller();
let current = controller.current_step_signal();
let version = controller.version_signal();
let completion = self.nav.completion_signals();
let next_enabled = current.flat_map(move |i| {
completion
.get(*i)
.cloned()
.unwrap_or_else(|| Signal::new(true))
});
let back_id = ctx.add(
Button::new(self.back_label.clone())
.variant(ButtonVariant::Plain)
.on_activate_fn({
let nav = self.nav.clone();
move |ctx| {
nav.controller().back();
nav.focus_primary(ctx);
}
}),
);
let next_id = ctx.add(
Button::new(self.next_label.clone())
.variant(ButtonVariant::Filled)
.on_activate_fn({
let nav = self.nav.clone();
move |ctx| {
nav.advance(ctx);
}
}),
);
let finish_id = ctx.add(
Button::new(self.finish_label.clone())
.variant(ButtonVariant::Filled)
.on_activate_fn({
let nav = self.nav.clone();
move |ctx| {
nav.finish(ctx);
}
}),
);
let skip_id = ctx.add(
Button::new(self.skip_label.clone())
.variant(ButtonVariant::Ghost)
.on_activate_fn({
let nav = self.nav.clone();
move |ctx| {
nav.controller().skip();
nav.focus_primary(ctx);
}
}),
);
self.nav.set_focus_targets(next_id, finish_id);
ctx.visible_when(back_id, {
let c = controller.clone();
version.zip(¤t).map(move |_| c.can_back())
});
ctx.visible_when(next_id, {
let c = controller.clone();
version.zip(¤t).map(move |_| c.has_next())
});
ctx.visible_when(finish_id, {
let c = controller.clone();
version.zip(¤t).map(move |_| !c.has_next())
});
ctx.enabled_when(next_id, next_enabled.clone());
ctx.enabled_when(finish_id, next_enabled);
let optional_flags = self.optional_flags.clone();
ctx.visible_when(
skip_id,
current.map(move |i| optional_flags.get(*i).copied().unwrap_or(false)),
);
let help_id = self.help_label.clone().map(|label| {
ctx.add(
Button::new(label)
.variant(ButtonVariant::Plain)
.on_activate_fn({
let controller = controller.clone();
let help_action = self.help_action.clone();
move |ctx| {
if let Some(action) = &help_action {
action(ctx, &controller);
}
}
}),
)
});
let cancel_id = self.cancel_label.clone().map(|label| {
ctx.add(
Button::new(label)
.variant(ButtonVariant::Ghost)
.on_activate_fn({
let controller = controller.clone();
let cancel_action = self.cancel_action.clone();
move |ctx| {
if let Some(action) = &cancel_action {
action(ctx, &controller);
}
}
}),
)
});
let mut row = HStack::new()
.spacing(12.0)
.add_child(back_id)
.child(Spacer::new());
if let Some(id) = help_id {
row = row.add_child(id);
}
if let Some(id) = cancel_id {
row = row.add_child(id);
}
row = row
.add_child(skip_id)
.add_child(next_id)
.add_child(finish_id);
let root = ctx.add(row);
self.root_child_id = Some(root);
vec![root]
}
fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
self.root_child_id
.and_then(|id| ctx.child_size(id, proposal))
.unwrap_or_else(|| proposal.resolve(0.0, 0.0))
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::Group);
}
fn children(&self) -> Vec<WidgetId> {
self.root_child_id.into_iter().collect()
}
}