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