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::default())
165 }
166
167 fn id(&self) -> Option<Id> {
168 Some(self.id.clone())
169 }
170
171 fn set_id(&mut self, id: Id) {
172 self.id = id;
173 }
174
175 fn layout(
176 &mut self,
177 tree: &mut Tree,
178 renderer: &crate::Renderer,
179 limits: &layout::Limits,
180 ) -> layout::Node {
181 let limits = limits.width(self.width);
182
183 let res = next_to_each_other(
184 &limits,
185 self.spacing,
186 |limits| {
187 if let Some(label) = self.label.as_deref() {
188 let state = tree.state.downcast_mut::<State>();
189 let node = iced_core::widget::text::layout(
190 &mut state.text,
191 renderer,
192 limits,
193 label,
194 widget::text::Format {
195 width: self.width,
196 height: Length::Shrink,
197 line_height: self.text_line_height,
198 size: self.text_size.map(iced::Pixels),
199 font: self.font,
200 align_x: self.text_alignment,
201 align_y: alignment::Vertical::Top,
202 shaping: self.text_shaping,
203 wrapping: iced_core::text::Wrapping::default(),
204 ellipsize: self.ellipsize,
205 },
206 );
207 match self.width {
208 Length::Fill => {
209 let size = node.size();
210 layout::Node::with_children(
211 Size::new(limits.width(Length::Fill).max().width, size.height),
212 vec![node],
213 )
214 }
215 _ => node,
216 }
217 } else {
218 layout::Node::new(iced_core::Size::ZERO)
219 }
220 },
221 |_| layout::Node::new(Size::new(48., 24.)),
222 );
223 res
224 }
225
226 fn update(
227 &mut self,
228 tree: &mut Tree,
229 event: &Event,
230 layout: Layout<'_>,
231 cursor_position: mouse::Cursor,
232 _renderer: &crate::Renderer,
233 _clipboard: &mut dyn Clipboard,
234 shell: &mut Shell<'_, Message>,
235 _viewport: &Rectangle,
236 ) {
237 let Some(on_toggle) = self.on_toggle.as_ref() else {
238 return;
239 };
240 let state = tree.state.downcast_mut::<State>();
241 match event {
242 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
243 | Event::Touch(touch::Event::FingerPressed { .. }) => {
244 let mouse_over = cursor_position.is_over(layout.bounds());
245
246 if mouse_over {
247 shell.publish((on_toggle)(!self.is_toggled));
248 state.anim.changed(self.duration);
249 shell.capture_event();
250 }
251 }
252 Event::Window(window::Event::RedrawRequested(now)) => {
253 state.anim.anim_done(self.duration);
254 if state.anim.last_change.is_some() {
255 shell.request_redraw();
256 }
257 }
258 _ => {}
259 }
260 }
261
262 fn mouse_interaction(
263 &self,
264 _state: &Tree,
265 layout: Layout<'_>,
266 cursor_position: mouse::Cursor,
267 _viewport: &Rectangle,
268 _renderer: &crate::Renderer,
269 ) -> mouse::Interaction {
270 if cursor_position.is_over(layout.bounds()) {
271 mouse::Interaction::Pointer
272 } else {
273 mouse::Interaction::default()
274 }
275 }
276
277 fn draw(
278 &self,
279 tree: &Tree,
280 renderer: &mut crate::Renderer,
281 theme: &crate::Theme,
282 style: &renderer::Style,
283 layout: Layout<'_>,
284 cursor_position: mouse::Cursor,
285 viewport: &Rectangle,
286 ) {
287 let state = tree.state.downcast_ref::<State>();
288
289 let mut children = layout.children();
290 let label_layout = children.next().unwrap();
291
292 if let Some(_label) = &self.label {
293 let state: &State = tree.state.downcast_ref();
294 iced_widget::text::draw(
295 renderer,
296 style,
297 label_layout.bounds(),
298 state.text.raw(),
299 iced_widget::text::Style::default(),
300 viewport,
301 );
302 }
303
304 let toggler_layout = children.next().unwrap();
305 let bounds = toggler_layout.bounds();
306
307 let is_mouse_over = cursor_position.is_over(bounds);
308
309 let style = theme.style(
330 &(),
331 if is_mouse_over {
332 Status::Hovered {
333 is_toggled: self.is_toggled,
334 }
335 } else {
336 Status::Active {
337 is_toggled: self.is_toggled,
338 }
339 },
340 );
341
342 let space = style.handle_margin;
343
344 let toggler_background_bounds = Rectangle {
345 x: bounds.x,
346 y: bounds.y,
347 width: bounds.width,
348 height: bounds.height,
349 };
350
351 renderer.fill_quad(
352 renderer::Quad {
353 bounds: toggler_background_bounds,
354 border: Border {
355 radius: style.border_radius,
356 ..Default::default()
357 },
358 ..renderer::Quad::default()
359 },
360 style.background,
361 );
362 let mut t = state.anim.t(self.duration, self.is_toggled);
363
364 let toggler_foreground_bounds = Rectangle {
365 x: bounds.x
366 + anim::slerp(
367 space,
368 bounds.width - space - (bounds.height - (2.0 * space)),
369 t,
370 ),
371
372 y: bounds.y + space,
373 width: bounds.height - (2.0 * space),
374 height: bounds.height - (2.0 * space),
375 };
376
377 renderer.fill_quad(
378 renderer::Quad {
379 bounds: toggler_foreground_bounds,
380 border: Border {
381 radius: style.handle_radius,
382 ..Default::default()
383 },
384 ..renderer::Quad::default()
385 },
386 style.foreground,
387 );
388 }
389}
390
391impl<'a, Message: 'static> From<Toggler<'a, Message>> for Element<'a, Message> {
392 fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> {
393 Element::new(toggler)
394 }
395}
396
397pub fn next_to_each_other(
399 limits: &iced::Limits,
400 spacing: f32,
401 left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
402 right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
403) -> iced_core::layout::Node {
404 let mut right_node = right(limits);
405 let right_size = right_node.size();
406
407 let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0));
408 let mut left_node = left(&left_limits);
409 let left_size = left_node.size();
410
411 let (left_y, right_y) = if left_size.height > right_size.height {
412 (0.0, (left_size.height - right_size.height) / 2.0)
413 } else {
414 ((right_size.height - left_size.height) / 2.0, 0.0)
415 };
416
417 left_node = left_node.move_to(iced::Point::new(0.0, left_y));
418 right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y));
419
420 iced_core::layout::Node::with_children(
421 Size::new(
422 left_size.width + spacing + right_size.width,
423 left_size.height.max(right_size.height),
424 ),
425 vec![left_node, right_node],
426 )
427}
428
429#[derive(Debug, Default)]
430pub struct State {
431 text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
432 anim: anim::State,
433}