use std::cell::Cell;
use std::rc::Rc;
use std::time::{Duration, Instant};
use teksilo_canvas::{Rect, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::build_context::BuildContext;
use teksilo_core::frame_tick_scheduler::FrameTickSubscription;
use teksilo_core::signal::Signal;
use teksilo_core::widget::{LayoutContext, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
use crate::primitives::Switcher;
const DEFAULT_PERIOD: Duration = Duration::from_secs(3);
pub struct Cycle {
period: Duration,
deferred_children: Vec<Box<dyn Widget>>,
root_child_id: Option<WidgetId>,
frame_tick_sub: Option<FrameTickSubscription>,
}
impl Cycle {
pub fn new() -> Self {
Self {
period: DEFAULT_PERIOD,
deferred_children: Vec::new(),
root_child_id: None,
frame_tick_sub: None,
}
}
pub fn period(mut self, period: Duration) -> Self {
self.period = period;
self
}
pub fn child(mut self, widget: impl Widget + 'static) -> Self {
self.deferred_children.push(Box::new(widget));
self
}
pub fn child_boxed(mut self, widget: Box<dyn Widget>) -> Self {
self.deferred_children.push(widget);
self
}
pub fn children(mut self, iter: impl IntoIterator<Item = impl Widget + 'static>) -> Self {
for w in iter {
self.deferred_children.push(Box::new(w));
}
self
}
}
impl Default for Cycle {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for Cycle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cycle")
.field("period", &self.period)
.field("num_children", &self.deferred_children.len())
.finish()
}
}
impl Widget for Cycle {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let children = std::mem::take(&mut self.deferred_children);
let n = children.len();
let selected = Signal::new(0_usize);
let mut switcher = Switcher::new(selected.clone());
for child in children {
switcher = switcher.child_boxed(child);
}
let root = ctx.add(switcher);
self.root_child_id = Some(root);
if ctx.prefers_reduced_motion() || n <= 1 {
return vec![root];
}
let period = self.period;
let last_advance: Rc<Cell<Option<Instant>>> = Rc::new(Cell::new(None));
let selected_for_tick = selected;
ctx.effect(&ctx.frame_tick(), move |_delta| {
let now = Instant::now();
match last_advance.get() {
None => last_advance.set(Some(now)),
Some(prev) if now.duration_since(prev) >= period => {
let next = (selected_for_tick.get() + 1) % n;
selected_for_tick.set(next);
last_advance.set(Some(now));
}
Some(_) => {}
}
});
self.frame_tick_sub = None;
self.frame_tick_sub = Some(ctx.subscribe_frame_tick_throttled(period));
vec![root]
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::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) {
}
fn children(&self) -> Vec<WidgetId> {
self.root_child_id.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::TextWidget;
use teksilo_core::widget_tree::WidgetTree;
use teksilo_i18n::lit;
#[test]
fn cycle_builds_with_children() {
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(
Cycle::new()
.child(TextWidget::new(lit!("A")))
.child(TextWidget::new(lit!("B")))
.child(TextWidget::new(lit!("C"))),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let b = tree.bounds(id);
assert!(b.width > 0.0);
}
#[test]
fn empty_cycle_is_safe() {
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
tree.add(Cycle::new());
tree.layout(SizeProposal::exact(100.0, 50.0));
let _ = tree.render();
}
#[test]
fn single_child_cycle_does_not_animate() {
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
tree.add(Cycle::new().child(TextWidget::new(lit!("only"))));
tree.layout(SizeProposal::exact(200.0, 100.0));
let _ = tree.render();
assert!(
!tree.has_active_animations(),
"single-child cycle should not start a timer"
);
}
#[test]
fn reduced_motion_pins_first_child() {
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
tree.set_accessibility_preferences(false, true, 1.0);
tree.add(
Cycle::new()
.child(TextWidget::new(lit!("A")))
.child(TextWidget::new(lit!("B"))),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let _ = tree.render();
assert!(
!tree.has_active_animations(),
"reduced-motion path must not register animations"
);
}
}