1use std::time::{Duration, Instant};
4
5use crate::{Element, anim};
6use iced_core::renderer::{self, Renderer};
7use iced_core::widget::{self, Tree, tree};
8use iced_core::{
9 Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment,
10 event, layout, mouse, text, touch, window,
11};
12use iced_widget::Id;
13use iced_widget::toggler::Status;
14
15pub use iced_widget::toggler::{Catalog, Style};
16
17pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> {
18 Toggler::new(is_checked)
19}
20#[allow(missing_debug_implementations)]
22pub struct Toggler<'a, Message> {
23 id: Id,
24 is_toggled: bool,
25 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
26 label: Option<String>,
27 width: Length,
28 size: f32,
29 text_size: Option<f32>,
30 text_line_height: text::LineHeight,
31 text_alignment: text::Alignment,
32 text_shaping: text::Shaping,
33 spacing: f32,
34 font: Option<crate::font::Font>,
35 duration: Duration,
36 ellipsize: text::Ellipsize,
37}
38
39impl<'a, Message> Toggler<'a, Message> {
40 pub const DEFAULT_SIZE: f32 = 24.0;
42
43 pub fn new(is_toggled: bool) -> Self {
52 Toggler {
53 id: Id::unique(),
54 is_toggled,
55 on_toggle: None,
56 label: None,
57 width: Length::Shrink,
58 size: Self::DEFAULT_SIZE,
59 text_size: None,
60 text_line_height: text::LineHeight::default(),
61 text_alignment: text::Alignment::Left,
62 text_shaping: text::Shaping::Advanced,
63 spacing: 0.0,
64 font: None,
65 duration: Duration::from_millis(200),
66 ellipsize: text::Ellipsize::None,
67 }
68 }
69
70 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
72 self.size = size.into().0;
73 self
74 }
75
76 pub fn width(mut self, width: impl Into<Length>) -> Self {
78 self.width = width.into();
79 self
80 }
81
82 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
84 self.text_size = Some(text_size.into().0);
85 self
86 }
87
88 pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
90 self.text_line_height = line_height.into();
91 self
92 }
93
94 pub fn text_alignment(mut self, alignment: text::Alignment) -> Self {
96 self.text_alignment = alignment;
97 self
98 }
99
100 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
102 self.text_shaping = shaping;
103 self
104 }
105
106 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
108 self.spacing = spacing.into().0;
109 self
110 }
111
112 pub fn ellipsize(mut self, ellipsize: text::Ellipsize) -> Self {
114 self.ellipsize = ellipsize;
115 self
116 }
117
118 pub fn font(mut self, font: impl Into<crate::font::Font>) -> Self {
122 self.font = Some(font.into());
123 self
124 }
125
126 pub fn id(mut self, id: Id) -> Self {
127 self.id = id;
128 self
129 }
130
131 pub fn duration(mut self, dur: Duration) -> Self {
132 self.duration = dur;
133 self
134 }
135
136 pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
137 self.on_toggle = Some(Box::new(on_toggle));
138 self
139 }
140
141 pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
142 self.on_toggle = on_toggle.map(|t| Box::new(t) as _);
143 self
144 }
145
146 pub fn label(mut self, label: impl Into<Option<String>>) -> Self {
148 self.label = label.into();
149 self
150 }
151}
152
153impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a, Message> {
154 fn size(&self) -> Size<Length> {
155 Size::new(self.width, Length::Shrink)
156 }
157
158 fn tag(&self) -> tree::Tag {
159 tree::Tag::of::<State>()
160 }
161
162 fn state(&self) -> tree::State {
163 tree::State::new(State {
164 prev_toggled: self.is_toggled,
165 ..State::default()
166 })
167 }
168
169 fn id(&self) -> Option<Id> {
170 Some(self.id.clone())
171 }
172
173 fn set_id(&mut self, id: Id) {
174 self.id = id;
175 }
176
177 fn layout(
178 &mut self,
179 tree: &mut Tree,
180 renderer: &crate::Renderer,
181 limits: &layout::Limits,
182 ) -> layout::Node {
183 let limits = limits.width(self.width);
184
185 let res = next_to_each_other(
186 &limits,
187 self.spacing,
188 |limits| {
189 if let Some(label) = self.label.as_deref() {
190 let state = tree.state.downcast_mut::<State>();
191 let node = iced_core::widget::text::layout(
192 &mut state.text,
193 renderer,
194 limits,
195 label,
196 widget::text::Format {
197 width: self.width,
198 height: Length::Shrink,
199 line_height: self.text_line_height,
200 size: self.text_size.map(iced::Pixels),
201 font: self.font,
202 align_x: self.text_alignment,
203 align_y: alignment::Vertical::Top,
204 shaping: self.text_shaping,
205 wrapping: iced_core::text::Wrapping::default(),
206 ellipsize: self.ellipsize,
207 },
208 );
209 match self.width {
210 Length::Fill => {
211 let size = node.size();
212 layout::Node::with_children(
213 Size::new(limits.width(Length::Fill).max().width, size.height),
214 vec![node],
215 )
216 }
217 _ => node,
218 }
219 } else {
220 layout::Node::new(iced_core::Size::ZERO)
221 }
222 },
223 |_| layout::Node::new(Size::new(48., 24.)),
224 );
225 res
226 }
227
228 fn update(
229 &mut self,
230 tree: &mut Tree,
231 event: &Event,
232 layout: Layout<'_>,
233 cursor_position: mouse::Cursor,
234 _renderer: &crate::Renderer,
235 _clipboard: &mut dyn Clipboard,
236 shell: &mut Shell<'_, Message>,
237 _viewport: &Rectangle,
238 ) {
239 let Some(on_toggle) = self.on_toggle.as_ref() else {
240 return;
241 };
242 let state = tree.state.downcast_mut::<State>();
243
244 if state.prev_toggled != self.is_toggled {
246 state.anim.changed(self.duration);
247 shell.request_redraw();
248 state.prev_toggled = self.is_toggled;
249 }
250
251 match event {
252 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
253 | Event::Touch(touch::Event::FingerPressed { .. }) => {
254 let mouse_over = cursor_position.is_over(layout.bounds());
255
256 if mouse_over {
257 shell.publish((on_toggle)(!self.is_toggled));
258 state.anim.changed(self.duration);
259 state.prev_toggled = !self.is_toggled;
260 shell.capture_event();
261 }
262 }
263 Event::Window(window::Event::RedrawRequested(now)) => {
264 state.anim.anim_done(self.duration);
265 if state.anim.last_change.is_some() {
266 shell.request_redraw();
267 }
268 }
269 _ => {}
270 }
271 }
272
273 fn mouse_interaction(
274 &self,
275 _state: &Tree,
276 layout: Layout<'_>,
277 cursor_position: mouse::Cursor,
278 _viewport: &Rectangle,
279 _renderer: &crate::Renderer,
280 ) -> mouse::Interaction {
281 if cursor_position.is_over(layout.bounds()) {
282 mouse::Interaction::Pointer
283 } else {
284 mouse::Interaction::default()
285 }
286 }
287
288 fn draw(
289 &self,
290 tree: &Tree,
291 renderer: &mut crate::Renderer,
292 theme: &crate::Theme,
293 style: &renderer::Style,
294 layout: Layout<'_>,
295 cursor_position: mouse::Cursor,
296 viewport: &Rectangle,
297 ) {
298 let state = tree.state.downcast_ref::<State>();
299
300 let mut children = layout.children();
301 let label_layout = children.next().unwrap();
302
303 if let Some(_label) = &self.label {
304 let state: &State = tree.state.downcast_ref();
305 iced_widget::text::draw(
306 renderer,
307 style,
308 label_layout.bounds(),
309 state.text.raw(),
310 iced_widget::text::Style::default(),
311 viewport,
312 );
313 }
314
315 let toggler_layout = children.next().unwrap();
316 let bounds = toggler_layout.bounds();
317
318 let is_mouse_over = cursor_position.is_over(bounds);
319
320 let style = theme.style(
341 &(),
342 if is_mouse_over {
343 Status::Hovered {
344 is_toggled: self.is_toggled,
345 }
346 } else {
347 Status::Active {
348 is_toggled: self.is_toggled,
349 }
350 },
351 );
352
353 let space = style.handle_margin;
354
355 let toggler_background_bounds = Rectangle {
356 x: bounds.x,
357 y: bounds.y,
358 width: bounds.width,
359 height: bounds.height,
360 };
361
362 renderer.fill_quad(
363 renderer::Quad {
364 bounds: toggler_background_bounds,
365 border: Border {
366 radius: style.border_radius,
367 ..Default::default()
368 },
369 ..renderer::Quad::default()
370 },
371 style.background,
372 );
373 let mut t = state.anim.t(self.duration, self.is_toggled);
374
375 let toggler_foreground_bounds = Rectangle {
376 x: bounds.x
377 + anim::slerp(
378 space,
379 bounds.width - space - (bounds.height - (2.0 * space)),
380 t,
381 ),
382
383 y: bounds.y + space,
384 width: bounds.height - (2.0 * space),
385 height: bounds.height - (2.0 * space),
386 };
387
388 renderer.fill_quad(
389 renderer::Quad {
390 bounds: toggler_foreground_bounds,
391 border: Border {
392 radius: style.handle_radius,
393 ..Default::default()
394 },
395 ..renderer::Quad::default()
396 },
397 style.foreground,
398 );
399 }
400}
401
402impl<'a, Message: 'static> From<Toggler<'a, Message>> for Element<'a, Message> {
403 fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> {
404 Element::new(toggler)
405 }
406}
407
408pub fn next_to_each_other(
410 limits: &iced::Limits,
411 spacing: f32,
412 left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
413 right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
414) -> iced_core::layout::Node {
415 let mut right_node = right(limits);
416 let right_size = right_node.size();
417
418 let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0));
419 let mut left_node = left(&left_limits);
420 let left_size = left_node.size();
421
422 let (left_y, right_y) = if left_size.height > right_size.height {
423 (0.0, (left_size.height - right_size.height) / 2.0)
424 } else {
425 ((right_size.height - left_size.height) / 2.0, 0.0)
426 };
427
428 left_node = left_node.move_to(iced::Point::new(0.0, left_y));
429 right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y));
430
431 iced_core::layout::Node::with_children(
432 Size::new(
433 left_size.width + spacing + right_size.width,
434 left_size.height.max(right_size.height),
435 ),
436 vec![left_node, right_node],
437 )
438}
439
440#[derive(Debug, Default)]
441pub struct State {
442 text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
443 anim: anim::State,
444 prev_toggled: bool,
445}