1use iced_native::alignment;
3use iced_native::event;
4use iced_native::layout;
5use iced_native::mouse;
6use iced_native::renderer;
7use iced_native::text;
8use iced_native::time::Duration;
9use iced_native::widget::{self, Row, Text, Tree};
10use iced_native::{
11 color, Alignment, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, Rectangle,
12 Shell, Widget,
13};
14
15use crate::keyframes::{self, toggler::Chain};
16use crate::lerp;
17
18pub use iced_style::toggler::{Appearance, StyleSheet};
19
20const ANIM_DURATION: f32 = 100.;
22
23#[allow(missing_debug_implementations)]
39pub struct Toggler<'a, Message, Renderer>
40where
41 Renderer: text::Renderer,
42 Renderer::Theme: StyleSheet,
43{
44 id: crate::keyframes::toggler::Id,
45 is_toggled: bool,
46 on_toggle: Box<dyn Fn(Chain, bool) -> Message + 'a>,
47 label: Option<String>,
48 width: Length,
49 size: f32,
50 text_size: Option<f32>,
51 text_alignment: alignment::Horizontal,
52 spacing: f32,
53 font: Renderer::Font,
54 style: <Renderer::Theme as StyleSheet>::Style,
55 percent: f32,
56 anim_multiplier: f32,
57}
58
59impl<'a, Message, Renderer> Toggler<'a, Message, Renderer>
60where
61 Renderer: text::Renderer,
62 Renderer::Theme: StyleSheet,
63{
64 pub const DEFAULT_SIZE: f32 = 20.0;
66
67 pub fn new<F>(
76 id: crate::keyframes::toggler::Id,
77 label: impl Into<Option<String>>,
78 is_toggled: bool,
79 f: F,
80 ) -> Self
81 where
82 F: 'a + Fn(Chain, bool) -> Message,
83 {
84 Toggler {
85 id,
86 is_toggled,
87 on_toggle: Box::new(f),
88 label: label.into(),
89 width: Length::Fill,
90 size: Self::DEFAULT_SIZE,
91 text_size: None,
92 text_alignment: alignment::Horizontal::Left,
93 spacing: 0.0,
94 font: Renderer::Font::default(),
95 style: Default::default(),
96 percent: if is_toggled { 1.0 } else { 0.0 },
97 anim_multiplier: 1.0,
98 }
99 }
100
101 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
103 self.size = size.into().0;
104 self
105 }
106
107 pub fn width(mut self, width: impl Into<Length>) -> Self {
109 self.width = width.into();
110 self
111 }
112
113 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
115 self.text_size = Some(text_size.into().0);
116 self
117 }
118
119 pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
121 self.text_alignment = alignment;
122 self
123 }
124
125 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
127 self.spacing = spacing.into().0;
128 self
129 }
130
131 pub fn font(mut self, font: Renderer::Font) -> Self {
135 self.font = font;
136 self
137 }
138
139 pub fn style(mut self, style: impl Into<<Renderer::Theme as StyleSheet>::Style>) -> Self {
141 self.style = style.into();
142 self
143 }
144
145 pub fn percent(mut self, percent: f32) -> Self {
149 self.percent = percent;
150 self
151 }
152
153 pub fn anim_multiplier(mut self, multiplier: f32) -> Self {
157 self.anim_multiplier = multiplier;
158 self
159 }
160}
161
162impl<'a, Message, Renderer> Widget<Message, Renderer> for Toggler<'a, Message, Renderer>
163where
164 Renderer: text::Renderer,
165 Renderer::Theme: StyleSheet + widget::text::StyleSheet,
166{
167 fn width(&self) -> Length {
168 self.width
169 }
170
171 fn height(&self) -> Length {
172 Length::Shrink
173 }
174
175 fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
176 let mut row = Row::<(), Renderer>::new()
177 .width(self.width)
178 .spacing(self.spacing)
179 .align_items(Alignment::Center);
180
181 if let Some(label) = &self.label {
182 row = row.push(
183 Text::new(label)
184 .horizontal_alignment(self.text_alignment)
185 .font(self.font.clone())
186 .width(self.width)
187 .size(self.text_size.unwrap_or_else(|| renderer.default_size())),
188 );
189 }
190
191 row = row.push(Row::new().width(2.0 * self.size).height(self.size));
192
193 row.layout(renderer, limits)
194 }
195
196 fn on_event(
197 &mut self,
198 _state: &mut Tree,
199 event: Event,
200 layout: Layout<'_>,
201 cursor_position: Point,
202 _renderer: &Renderer,
203 _clipboard: &mut dyn Clipboard,
204 shell: &mut Shell<'_, Message>,
205 ) -> event::Status {
206 match event {
207 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
208 let mouse_over = layout.bounds().contains(cursor_position);
209
210 if mouse_over && (self.percent == 0.0 || self.percent == 1.0) {
215 if self.is_toggled {
216 let off_animation = Chain::new(self.id.clone())
217 .link(keyframes::toggler::Toggler::new(Duration::ZERO).percent(1.0))
218 .link(
219 keyframes::toggler::Toggler::new(Duration::from_millis(
220 (ANIM_DURATION * self.anim_multiplier.round()) as u64,
221 ))
222 .percent(0.0),
223 );
224 shell.publish((self.on_toggle)(off_animation, !self.is_toggled));
225 } else {
226 let on_animation = Chain::new(self.id.clone())
227 .link(keyframes::toggler::Toggler::new(Duration::ZERO).percent(0.0))
228 .link(
229 keyframes::toggler::Toggler::new(Duration::from_millis(
230 (ANIM_DURATION * self.anim_multiplier.round()) as u64,
231 ))
232 .percent(1.0),
233 );
234 shell.publish((self.on_toggle)(on_animation, !self.is_toggled));
235 }
236
237 event::Status::Captured
238 } else {
239 event::Status::Ignored
240 }
241 }
242 _ => event::Status::Ignored,
243 }
244 }
245
246 fn mouse_interaction(
247 &self,
248 _state: &Tree,
249 layout: Layout<'_>,
250 cursor_position: Point,
251 _viewport: &Rectangle,
252 _renderer: &Renderer,
253 ) -> mouse::Interaction {
254 if layout.bounds().contains(cursor_position) {
255 mouse::Interaction::Pointer
256 } else {
257 mouse::Interaction::default()
258 }
259 }
260
261 fn draw(
262 &self,
263 _state: &Tree,
264 renderer: &mut Renderer,
265 theme: &Renderer::Theme,
266 style: &renderer::Style,
267 layout: Layout<'_>,
268 cursor_position: Point,
269 _viewport: &Rectangle,
270 ) {
271 const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
273
274 const SPACE_RATIO: f32 = 0.05;
277
278 let mut children = layout.children();
279
280 if let Some(label) = &self.label {
281 let label_layout = children.next().unwrap();
282
283 iced_native::widget::text::draw(
284 renderer,
285 style,
286 label_layout,
287 label,
288 self.text_size,
289 self.font.clone(),
290 Default::default(),
291 self.text_alignment,
292 alignment::Vertical::Center,
293 );
294 }
295
296 let toggler_layout = children.next().unwrap();
297 let bounds = toggler_layout.bounds();
298
299 let is_mouse_over = bounds.contains(cursor_position);
300
301 let style = if is_mouse_over {
302 blend_appearances(
303 theme.hovered(&self.style, false),
304 theme.hovered(&self.style, true),
305 self.percent,
306 )
307 } else {
308 blend_appearances(
309 theme.active(&self.style, false),
310 theme.active(&self.style, true),
311 self.percent,
312 )
313 };
314
315 let border_radius = bounds.height / BORDER_RADIUS_RATIO;
316 let space = SPACE_RATIO * bounds.height;
317
318 let toggler_background_bounds = Rectangle {
319 x: bounds.x + space,
320 y: bounds.y + space,
321 width: bounds.width - (2.0 * space),
322 height: bounds.height - (2.0 * space),
323 };
324
325 renderer.fill_quad(
326 renderer::Quad {
327 bounds: toggler_background_bounds,
328 border_radius: border_radius.into(),
329 border_width: 1.0,
330 border_color: style.background_border.unwrap_or(style.background),
331 },
332 style.background,
333 );
334
335 let toggler_foreground_bounds = Rectangle {
336 x: bounds.x
337 + lerp(
338 2.0 * space,
339 bounds.width - 2.0 * space - (bounds.height - (4.0 * space)),
340 self.percent,
341 ),
342 y: bounds.y + (2.0 * space),
343 width: bounds.height - (4.0 * space),
344 height: bounds.height - (4.0 * space),
345 };
346
347 renderer.fill_quad(
348 renderer::Quad {
349 bounds: toggler_foreground_bounds,
350 border_radius: border_radius.into(),
351 border_width: 1.0,
352 border_color: style.foreground_border.unwrap_or(style.foreground),
353 },
354 style.foreground,
355 );
356 }
357}
358
359impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>> for Element<'a, Message, Renderer>
360where
361 Message: 'a,
362 Renderer: 'a + text::Renderer,
363 Renderer::Theme: StyleSheet + widget::text::StyleSheet,
364{
365 fn from(toggler: Toggler<'a, Message, Renderer>) -> Element<'a, Message, Renderer> {
366 Element::new(toggler)
367 }
368}
369
370fn blend_appearances(
371 one: iced_style::toggler::Appearance,
372 mut two: iced_style::toggler::Appearance,
373 percent: f32,
374) -> iced_style::toggler::Appearance {
375 let background: [f32; 4] = one
376 .background
377 .into_linear()
378 .iter()
379 .zip(two.background.into_linear().iter())
380 .map(|(o, t)| lerp(*o, *t, percent))
381 .collect::<Vec<f32>>()
382 .try_into()
383 .unwrap();
384
385 let border_one: Color = one.background_border.unwrap_or(color!(0, 0, 0));
386 let border_two: Color = two.background_border.unwrap_or(color!(0, 0, 0));
387 let border: [f32; 4] = border_one
388 .into_linear()
389 .iter()
390 .zip(border_two.into_linear().iter())
391 .map(|(o, t)| lerp(*o, *t, percent))
392 .collect::<Vec<f32>>()
393 .try_into()
394 .unwrap();
395 let new_border: Color = border.into();
396
397 let foreground: [f32; 4] = one
398 .foreground
399 .into_linear()
400 .iter()
401 .zip(two.foreground.into_linear().iter())
402 .map(|(o, t)| lerp(*o, *t, percent))
403 .collect::<Vec<f32>>()
404 .try_into()
405 .unwrap();
406
407 let f_border_one: Color = one.foreground_border.unwrap_or(color!(0, 0, 0));
408 let f_border_two: Color = two.foreground_border.unwrap_or(color!(0, 0, 0));
409 let f_border: [f32; 4] = f_border_one
410 .into_linear()
411 .iter()
412 .zip(f_border_two.into_linear().iter())
413 .map(|(o, t)| lerp(*o, *t, percent))
414 .collect::<Vec<f32>>()
415 .try_into()
416 .unwrap();
417 let new_f_border: Color = f_border.into();
418
419 two.background = background.into();
420 two.background_border = Some(new_border);
421 two.foreground = foreground.into();
422 two.foreground_border = Some(new_f_border);
423 two
424}