1use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
22use teksilo_core::accessibility::AccessNodeBuilder;
23use teksilo_core::binding::BindingLevel;
24use teksilo_core::build_context::BuildContext;
25use teksilo_core::focus::FocusOrigin;
26use teksilo_core::signal::Signal;
27use teksilo_core::styles::{ToggleStyle, ToggleStyleConfig, ToggleVariant};
28use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement};
29use teksilo_core::widget_id::WidgetId;
30use teksilo_tokens::{Color, CornerRadius};
31
32pub const TOGGLE_TRACK_WIDTH: f32 = 28.0;
37pub const TOGGLE_TRACK_HEIGHT: f32 = 16.0;
38pub const TOGGLE_THUMB_DIAMETER: f32 = 12.0;
39pub const TOGGLE_THUMB_INSET: f32 = 2.0;
40
41#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct ToggleRecipe {
45 pub track_width: f32,
46 pub track_height: f32,
47 pub thumb_diameter: f32,
48 pub thumb_inset: f32,
49}
50
51impl Default for ToggleRecipe {
52 fn default() -> Self {
53 Self {
54 track_width: TOGGLE_TRACK_WIDTH,
55 track_height: TOGGLE_TRACK_HEIGHT,
56 thumb_diameter: TOGGLE_THUMB_DIAMETER,
57 thumb_inset: TOGGLE_THUMB_INSET,
58 }
59 }
60}
61
62#[derive(Debug, Default, Clone, Copy)]
66pub struct RecipeToggleStyle {
67 pub recipe: ToggleRecipe,
68}
69
70impl RecipeToggleStyle {
71 pub fn new(recipe: ToggleRecipe) -> Self {
73 Self { recipe }
74 }
75}
76
77impl ToggleStyle for RecipeToggleStyle {
78 fn make_body(&self, cfg: &ToggleStyleConfig, ctx: &mut BuildContext) -> WidgetId {
79 let initial = if cfg.is_on.get() { 1.0 } else { 0.0 };
82 let knob_position = ctx.animated_signal(initial);
83 let knob_anim = ctx.animate().fast().standard();
84
85 {
89 let knob_position = knob_position.clone();
90 let knob_anim = knob_anim.clone();
91 ctx.effect(&cfg.is_on, move |on| {
92 let target = if *on { 1.0 } else { 0.0 };
93 knob_anim.to_or_snap(&knob_position, target);
94 });
95 }
96
97 let focus_origin = cfg
102 .is_focused
103 .zip(&cfg.is_focus_visible)
104 .map(|(focused, visible)| {
105 if *focused {
106 Some(if *visible {
107 FocusOrigin::Keyboard
108 } else {
109 FocusOrigin::Pointer
110 })
111 } else {
112 None
113 }
114 });
115
116 ctx.add(ToggleBody {
117 knob_position,
118 is_disabled: cfg.is_disabled.clone(),
119 focus_origin,
120 variant: cfg.variant,
121 recipe: self.recipe,
122 })
123 }
124}
125
126struct ToggleBody {
130 knob_position: Signal<f32>,
131 is_disabled: Signal<bool>,
132 focus_origin: Signal<Option<FocusOrigin>>,
133 variant: ToggleVariant,
134 recipe: ToggleRecipe,
135}
136
137impl std::fmt::Debug for ToggleBody {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.debug_struct("ToggleBody")
140 .field("variant", &self.variant)
141 .finish()
142 }
143}
144
145impl Widget for ToggleBody {
146 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
147 let id = ctx.self_id();
148 let registry = ctx.binding_registry();
149 self.knob_position
150 .bind_to(id, registry, BindingLevel::RepaintOnly);
151 self.is_disabled
152 .bind_to(id, registry, BindingLevel::RepaintOnly);
153 self.focus_origin
154 .bind_to(id, registry, BindingLevel::RepaintOnly);
155 vec![]
156 }
157
158 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
159 let row_h = self.recipe.track_height.max(24.0);
160 Size::new(self.recipe.track_width, row_h).into()
161 }
162
163 fn place_children(
164 &self,
165 _bounds: Rect,
166 _proposal: SizeProposal,
167 _children: &mut [WidgetPlacement],
168 _ctx: &LayoutContext,
169 ) {
170 }
171
172 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
173 let colors = &ctx.theme.colors;
174 let track_w = self.recipe.track_width;
175 let track_h = self.recipe.track_height;
176 let knob_size = self.recipe.thumb_diameter;
177 let knob_inset = self.recipe.thumb_inset;
178 let enabled = !self.is_disabled.get();
179
180 let track_x = bounds.x;
182 let track_y = bounds.y + (bounds.height - track_h) / 2.0;
183 let track_rect = Rect::new(track_x, track_y, track_w, track_h);
184
185 let t = self.knob_position.get();
188 let track_color = if !enabled {
189 colors.accent_disabled
190 } else {
191 let off = colors.surface_sunken;
192 let on = colors.accent;
193 Color::new(
194 teksilo_tokens::lerp(off.r(), on.r(), t),
195 teksilo_tokens::lerp(off.g(), on.g(), t),
196 teksilo_tokens::lerp(off.b(), on.b(), t),
197 teksilo_tokens::lerp(off.a(), on.a(), t),
198 )
199 };
200
201 let track_corner = match self.variant {
204 ToggleVariant::Switch | ToggleVariant::Pill => CornerRadius::uniform(track_h / 2.0),
205 ToggleVariant::Square => CornerRadius::uniform(0.0),
206 ToggleVariant::Inset => CornerRadius::uniform(4.0),
207 };
208 canvas.fill_rounded_rect(track_rect, track_corner, track_color);
209
210 if self.focus_origin.get() == Some(FocusOrigin::Keyboard) {
213 let offset = ctx.theme.shape.focus_ring_offset + ctx.theme.shape.focus_ring_width / 2.0;
214 let ring_rect = Rect::new(
215 track_rect.x - offset,
216 track_rect.y - offset,
217 track_rect.width + offset * 2.0,
218 track_rect.height + offset * 2.0,
219 );
220 let ring_corner = match self.variant {
221 ToggleVariant::Switch | ToggleVariant::Pill => {
222 CornerRadius::uniform(track_h / 2.0 + offset)
223 }
224 ToggleVariant::Square => CornerRadius::uniform(offset),
225 ToggleVariant::Inset => CornerRadius::uniform(4.0 + offset),
226 };
227 canvas.stroke_rounded_rect(
228 ring_rect,
229 ring_corner,
230 colors.focus_ring,
231 ctx.theme.shape.focus_ring_width,
232 );
233 }
234
235 if matches!(self.variant, ToggleVariant::Switch | ToggleVariant::Square) {
237 let min_x = track_x + knob_inset;
238 let max_x = track_x + track_w - knob_size - knob_inset;
239 let knob_x = teksilo_tokens::lerp(min_x, max_x, t.clamp(0.0, 1.0));
240 let knob_y = track_y + (track_h - knob_size) / 2.0;
241 let knob_rect = Rect::new(knob_x, knob_y, knob_size, knob_size);
242 let knob_color = if !enabled {
243 colors.text_disabled
244 } else {
245 Color::WHITE
246 };
247 let knob_corner = match self.variant {
248 ToggleVariant::Switch => CornerRadius::uniform(knob_size / 2.0),
249 ToggleVariant::Square => CornerRadius::uniform(0.0),
250 _ => unreachable!(),
251 };
252 canvas.fill_rounded_rect(knob_rect, knob_corner, knob_color);
253 }
254 }
255
256 fn accessibility(&self, _builder: &mut AccessNodeBuilder) {
257 }
261}