use std::cell::Cell;
use std::rc::Rc;
use std::time::{Duration, Instant};
use teksilo_canvas::{Rect, SizeProposal};
use teksilo_core::build_context::BuildContext;
use teksilo_core::event::{EventResponse, PointerButton, WidgetEvent};
use teksilo_core::signal::Signal;
use teksilo_core::widget::{CursorIcon, EventContext, LayoutContext, Widget, WidgetPlacement};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::{CornerRadius, SurfaceRole, TextRole};
use crate::button::InteractionState;
use crate::primitives::icon_widget::IconWidget;
use crate::primitives::{Center, RectWidget, ZStack};
const INITIAL_DELAY_SECS: f32 = 0.400;
const MIN_INTERVAL_SECS: f32 = 0.045;
const START_INTERVAL_SECS: f32 = 0.120;
const ACCELERATION: f32 = 0.88;
#[derive(Debug, Clone, Copy)]
enum RepeatState {
Idle,
Held {
next_fire: Instant,
interval: f32,
},
}
pub(super) struct StepButton {
icon: IconWidget,
on_tap: Rc<dyn Fn(&mut EventContext)>,
on_auto_repeat: Option<Rc<dyn Fn()>>,
enabled_signal: Signal<bool>,
width: f32,
height: f32,
corner_radius: CornerRadius,
interaction: Signal<InteractionState>,
root_child_id: Option<WidgetId>,
}
impl std::fmt::Debug for StepButton {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StepButton")
.field("width", &self.width)
.field("height", &self.height)
.field("auto_repeat", &self.on_auto_repeat.is_some())
.finish_non_exhaustive()
}
}
impl StepButton {
pub(super) fn new(
icon: IconWidget,
enabled_signal: Signal<bool>,
on_tap: impl Fn(&mut EventContext) + 'static,
) -> Self {
Self {
icon,
on_tap: Rc::new(on_tap),
on_auto_repeat: None,
enabled_signal,
width: 18.0,
height: 12.0,
corner_radius: CornerRadius::uniform(0.0),
interaction: Signal::new(InteractionState::Idle),
root_child_id: None,
}
}
pub(super) fn size(mut self, width: f32, height: f32) -> Self {
self.width = width;
self.height = height;
self
}
pub(super) fn corner_radius(mut self, radius: CornerRadius) -> Self {
self.corner_radius = radius;
self
}
pub(super) fn on_auto_repeat(mut self, f: impl Fn() + 'static) -> Self {
self.on_auto_repeat = Some(Rc::new(f));
self
}
}
impl Widget for StepButton {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let interaction = self.interaction.clone();
let self_id = ctx.self_id();
let registry = ctx.binding_registry();
self.enabled_signal.bind_to(
self_id,
registry,
teksilo_core::binding::BindingLevel::RepaintOnly,
);
let enabled_signal_bg = self.enabled_signal.clone();
let bg_role =
interaction.map(move |state| resolve_bg_role(*state, enabled_signal_bg.get()));
let enabled_signal_icon = self.enabled_signal.clone();
let icon_role =
interaction.map(move |state| resolve_icon_role(*state, enabled_signal_icon.get()));
let bg = RectWidget::new()
.background(bg_role)
.corner_radius(self.corner_radius);
let bg_id = ctx.add(bg);
let sized_icon = std::mem::replace(
&mut self.icon,
IconWidget::from_path(teksilo_canvas::Path::new(), 0.0),
)
.color(icon_role);
let icon_id = ctx.add(Center::new().child(sized_icon));
let zstack_id = ctx.add(ZStack::new().add_child(bg_id).add_child(icon_id));
let root_id = ctx.add(
crate::primitives::FixedSize::new()
.width(self.width)
.height(self.height)
.child_id(zstack_id),
);
let repeat_state = Rc::new(Cell::new(RepeatState::Idle));
let hovered = Rc::new(Cell::new(false));
let frame_req = ctx.frame_request_handle();
let wake_at = ctx.wake_at_handle();
if let Some(auto_fn) = self.on_auto_repeat.clone() {
let state_for_tick = repeat_state.clone();
let frame_for_tick = frame_req.clone();
let wake_for_tick = wake_at.clone();
let tick = ctx.frame_tick();
ctx.effect(&tick, move |_delta| {
if let RepeatState::Held {
next_fire,
interval,
} = state_for_tick.get()
{
let now = Instant::now();
if now >= next_fire {
auto_fn();
let new_interval = (interval * ACCELERATION).max(MIN_INTERVAL_SECS);
let new_next = now + Duration::from_secs_f32(new_interval);
state_for_tick.set(RepeatState::Held {
next_fire: new_next,
interval: new_interval,
});
wake_for_tick.set(Some(new_next));
frame_for_tick.set(true);
} else {
let current = wake_for_tick.get();
let merged = match current {
Some(existing) if existing <= next_fire => existing,
_ => next_fire,
};
wake_for_tick.set(Some(merged));
}
}
});
}
let on_tap_for_press = self.on_tap.clone();
let on_auto = self.on_auto_repeat.clone();
let int_pointer = interaction.clone();
let int_enter = interaction.clone();
let int_leave = interaction.clone();
let enabled_pointer = self.enabled_signal.clone();
let enabled_hover = self.enabled_signal.clone();
let repeat_for_pointer = repeat_state.clone();
let hovered_for_pointer = hovered.clone();
let hovered_for_hover = hovered.clone();
let frame_for_pointer = frame_req.clone();
let wake_for_pointer = wake_at.clone();
let handlers = HandlerSet::new()
.focusable(false)
.cursor(CursorIcon::Pointer)
.on_pointer_event(move |event, ctx| {
match event {
WidgetEvent::PointerDown { button, .. }
if *button == PointerButton::Primary =>
{
if !enabled_pointer.get() {
return EventResponse::Ignored;
}
(on_tap_for_press)(ctx);
int_pointer.set(InteractionState::Pressed);
if on_auto.is_some() {
let first_fire =
Instant::now() + Duration::from_secs_f32(INITIAL_DELAY_SECS);
repeat_for_pointer.set(RepeatState::Held {
next_fire: first_fire,
interval: START_INTERVAL_SECS,
});
let current = wake_for_pointer.get();
let merged = match current {
Some(existing) if existing <= first_fire => existing,
_ => first_fire,
};
wake_for_pointer.set(Some(merged));
frame_for_pointer.set(true);
}
EventResponse::Handled
}
WidgetEvent::PointerUp { button, .. } if *button == PointerButton::Primary => {
repeat_for_pointer.set(RepeatState::Idle);
int_pointer.set(if hovered_for_pointer.get() {
InteractionState::Hovered
} else {
InteractionState::Idle
});
EventResponse::Handled
}
_ => EventResponse::Ignored,
}
})
.on_hover(move |entered, _ctx| {
hovered_for_hover.set(entered);
if !enabled_hover.get() {
return;
}
let current = int_enter.get();
if matches!(current, InteractionState::Pressed) {
return;
}
if entered {
int_enter.set(InteractionState::Hovered);
} else {
int_leave.set(InteractionState::Idle);
}
});
ctx.apply_self_handlers(handlers);
self.root_child_id = Some(root_id);
vec![root_id]
}
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(self.width, self.height))
.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 children(&self) -> Vec<WidgetId> {
self.root_child_id.into_iter().collect()
}
fn accessibility(&self, builder: &mut teksilo_core::accessibility::AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
}
}
fn resolve_bg_role(state: InteractionState, enabled: bool) -> SurfaceRole {
if !enabled {
return SurfaceRole::Transparent;
}
match state {
InteractionState::Pressed => SurfaceRole::Pressed,
InteractionState::Hovered => SurfaceRole::Hover,
_ => SurfaceRole::Transparent,
}
}
fn resolve_icon_role(_state: InteractionState, enabled: bool) -> TextRole {
if enabled {
TextRole::Primary
} else {
TextRole::Disabled
}
}