use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::binding::BindingLevel;
use teksilo_core::build_context::BuildContext;
use teksilo_core::focus::FocusOrigin;
use teksilo_core::signal::Signal;
use teksilo_core::styles::{ToggleStyle, ToggleStyleConfig, ToggleVariant};
use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::{Color, CornerRadius};
pub const TOGGLE_TRACK_WIDTH: f32 = 28.0;
pub const TOGGLE_TRACK_HEIGHT: f32 = 16.0;
pub const TOGGLE_THUMB_DIAMETER: f32 = 12.0;
pub const TOGGLE_THUMB_INSET: f32 = 2.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ToggleRecipe {
pub track_width: f32,
pub track_height: f32,
pub thumb_diameter: f32,
pub thumb_inset: f32,
}
impl Default for ToggleRecipe {
fn default() -> Self {
Self {
track_width: TOGGLE_TRACK_WIDTH,
track_height: TOGGLE_TRACK_HEIGHT,
thumb_diameter: TOGGLE_THUMB_DIAMETER,
thumb_inset: TOGGLE_THUMB_INSET,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RecipeToggleStyle {
pub recipe: ToggleRecipe,
}
impl RecipeToggleStyle {
pub fn new(recipe: ToggleRecipe) -> Self {
Self { recipe }
}
}
impl ToggleStyle for RecipeToggleStyle {
fn make_body(&self, cfg: &ToggleStyleConfig, ctx: &mut BuildContext) -> WidgetId {
let initial = if cfg.is_on.get() { 1.0 } else { 0.0 };
let knob_position = ctx.animated_signal(initial);
let knob_anim = ctx.animate().fast().standard();
{
let knob_position = knob_position.clone();
let knob_anim = knob_anim.clone();
ctx.effect(&cfg.is_on, move |on| {
let target = if *on { 1.0 } else { 0.0 };
knob_anim.to_or_snap(&knob_position, target);
});
}
let focus_origin = cfg
.is_focused
.zip(&cfg.is_focus_visible)
.map(|(focused, visible)| {
if *focused {
Some(if *visible {
FocusOrigin::Keyboard
} else {
FocusOrigin::Pointer
})
} else {
None
}
});
ctx.add(ToggleBody {
knob_position,
is_disabled: cfg.is_disabled.clone(),
focus_origin,
variant: cfg.variant,
recipe: self.recipe,
})
}
}
struct ToggleBody {
knob_position: Signal<f32>,
is_disabled: Signal<bool>,
focus_origin: Signal<Option<FocusOrigin>>,
variant: ToggleVariant,
recipe: ToggleRecipe,
}
impl std::fmt::Debug for ToggleBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToggleBody")
.field("variant", &self.variant)
.finish()
}
}
impl Widget for ToggleBody {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let id = ctx.self_id();
let registry = ctx.binding_registry();
self.knob_position
.bind_to(id, registry, BindingLevel::RepaintOnly);
self.is_disabled
.bind_to(id, registry, BindingLevel::RepaintOnly);
self.focus_origin
.bind_to(id, registry, BindingLevel::RepaintOnly);
vec![]
}
fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
let row_h = self.recipe.track_height.max(24.0);
Size::new(self.recipe.track_width, row_h).into()
}
fn place_children(
&self,
_bounds: Rect,
_proposal: SizeProposal,
_children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let colors = &ctx.theme.colors;
let track_w = self.recipe.track_width;
let track_h = self.recipe.track_height;
let knob_size = self.recipe.thumb_diameter;
let knob_inset = self.recipe.thumb_inset;
let enabled = !self.is_disabled.get();
let track_x = bounds.x;
let track_y = bounds.y + (bounds.height - track_h) / 2.0;
let track_rect = Rect::new(track_x, track_y, track_w, track_h);
let t = self.knob_position.get();
let track_color = if !enabled {
colors.accent_disabled
} else {
let off = colors.surface_sunken;
let on = colors.accent;
Color::new(
teksilo_tokens::lerp(off.r(), on.r(), t),
teksilo_tokens::lerp(off.g(), on.g(), t),
teksilo_tokens::lerp(off.b(), on.b(), t),
teksilo_tokens::lerp(off.a(), on.a(), t),
)
};
let track_corner = match self.variant {
ToggleVariant::Switch | ToggleVariant::Pill => CornerRadius::uniform(track_h / 2.0),
ToggleVariant::Square => CornerRadius::uniform(0.0),
ToggleVariant::Inset => CornerRadius::uniform(4.0),
};
canvas.fill_rounded_rect(track_rect, track_corner, track_color);
if self.focus_origin.get() == Some(FocusOrigin::Keyboard) {
let offset = ctx.theme.shape.focus_ring_offset + ctx.theme.shape.focus_ring_width / 2.0;
let ring_rect = Rect::new(
track_rect.x - offset,
track_rect.y - offset,
track_rect.width + offset * 2.0,
track_rect.height + offset * 2.0,
);
let ring_corner = match self.variant {
ToggleVariant::Switch | ToggleVariant::Pill => {
CornerRadius::uniform(track_h / 2.0 + offset)
}
ToggleVariant::Square => CornerRadius::uniform(offset),
ToggleVariant::Inset => CornerRadius::uniform(4.0 + offset),
};
canvas.stroke_rounded_rect(
ring_rect,
ring_corner,
colors.focus_ring,
ctx.theme.shape.focus_ring_width,
);
}
if matches!(self.variant, ToggleVariant::Switch | ToggleVariant::Square) {
let min_x = track_x + knob_inset;
let max_x = track_x + track_w - knob_size - knob_inset;
let knob_x = teksilo_tokens::lerp(min_x, max_x, t.clamp(0.0, 1.0));
let knob_y = track_y + (track_h - knob_size) / 2.0;
let knob_rect = Rect::new(knob_x, knob_y, knob_size, knob_size);
let knob_color = if !enabled {
colors.text_disabled
} else {
Color::WHITE
};
let knob_corner = match self.variant {
ToggleVariant::Switch => CornerRadius::uniform(knob_size / 2.0),
ToggleVariant::Square => CornerRadius::uniform(0.0),
_ => unreachable!(),
};
canvas.fill_rounded_rect(knob_rect, knob_corner, knob_color);
}
}
fn accessibility(&self, _builder: &mut AccessNodeBuilder) {
}
}