1use crate::alignment;
3use crate::event;
4use crate::layout;
5use crate::mouse;
6use crate::renderer;
7use crate::text;
8use crate::widget::{self, Row, Text, Tree};
9use crate::{
10 Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point,
11 Rectangle, Shell, Widget,
12};
13
14pub use iced_style::toggler::{Appearance, StyleSheet};
15
16#[allow(missing_debug_implementations)]
32pub struct Toggler<'a, Message, Renderer>
33where
34 Renderer: text::Renderer,
35 Renderer::Theme: StyleSheet,
36{
37 is_toggled: bool,
38 on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
39 label: Option<String>,
40 width: Length,
41 size: f32,
42 text_size: Option<f32>,
43 text_alignment: alignment::Horizontal,
44 spacing: f32,
45 font: Renderer::Font,
46 style: <Renderer::Theme as StyleSheet>::Style,
47}
48
49impl<'a, Message, Renderer> Toggler<'a, Message, Renderer>
50where
51 Renderer: text::Renderer,
52 Renderer::Theme: StyleSheet,
53{
54 pub const DEFAULT_SIZE: f32 = 20.0;
56
57 pub fn new<F>(
66 label: impl Into<Option<String>>,
67 is_toggled: bool,
68 f: F,
69 ) -> Self
70 where
71 F: 'a + Fn(bool) -> Message,
72 {
73 Toggler {
74 is_toggled,
75 on_toggle: Box::new(f),
76 label: label.into(),
77 width: Length::Fill,
78 size: Self::DEFAULT_SIZE,
79 text_size: None,
80 text_alignment: alignment::Horizontal::Left,
81 spacing: 0.0,
82 font: Renderer::Font::default(),
83 style: Default::default(),
84 }
85 }
86
87 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
89 self.size = size.into().0;
90 self
91 }
92
93 pub fn width(mut self, width: impl Into<Length>) -> Self {
95 self.width = width.into();
96 self
97 }
98
99 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
101 self.text_size = Some(text_size.into().0);
102 self
103 }
104
105 pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
107 self.text_alignment = alignment;
108 self
109 }
110
111 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
113 self.spacing = spacing.into().0;
114 self
115 }
116
117 pub fn font(mut self, font: Renderer::Font) -> Self {
121 self.font = font;
122 self
123 }
124
125 pub fn style(
127 mut self,
128 style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
129 ) -> Self {
130 self.style = style.into();
131 self
132 }
133}
134
135impl<'a, Message, Renderer> Widget<Message, Renderer>
136 for Toggler<'a, Message, Renderer>
137where
138 Renderer: text::Renderer,
139 Renderer::Theme: StyleSheet + widget::text::StyleSheet,
140{
141 fn width(&self) -> Length {
142 self.width
143 }
144
145 fn height(&self) -> Length {
146 Length::Shrink
147 }
148
149 fn layout(
150 &self,
151 renderer: &Renderer,
152 limits: &layout::Limits,
153 ) -> layout::Node {
154 let mut row = Row::<(), Renderer>::new()
155 .width(self.width)
156 .spacing(self.spacing)
157 .align_items(Alignment::Center);
158
159 if let Some(label) = &self.label {
160 row = row.push(
161 Text::new(label)
162 .horizontal_alignment(self.text_alignment)
163 .font(self.font.clone())
164 .width(self.width)
165 .size(
166 self.text_size
167 .unwrap_or_else(|| renderer.default_size()),
168 ),
169 );
170 }
171
172 row = row.push(Row::new().width(2.0 * self.size).height(self.size));
173
174 row.layout(renderer, limits)
175 }
176
177 fn on_event(
178 &mut self,
179 _state: &mut Tree,
180 event: Event,
181 layout: Layout<'_>,
182 cursor_position: Point,
183 _renderer: &Renderer,
184 _clipboard: &mut dyn Clipboard,
185 shell: &mut Shell<'_, Message>,
186 ) -> event::Status {
187 match event {
188 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
189 let mouse_over = layout.bounds().contains(cursor_position);
190
191 if mouse_over {
192 shell.publish((self.on_toggle)(!self.is_toggled));
193
194 event::Status::Captured
195 } else {
196 event::Status::Ignored
197 }
198 }
199 _ => event::Status::Ignored,
200 }
201 }
202
203 fn mouse_interaction(
204 &self,
205 _state: &Tree,
206 layout: Layout<'_>,
207 cursor_position: Point,
208 _viewport: &Rectangle,
209 _renderer: &Renderer,
210 ) -> mouse::Interaction {
211 if layout.bounds().contains(cursor_position) {
212 mouse::Interaction::Pointer
213 } else {
214 mouse::Interaction::default()
215 }
216 }
217
218 fn draw(
219 &self,
220 _state: &Tree,
221 renderer: &mut Renderer,
222 theme: &Renderer::Theme,
223 style: &renderer::Style,
224 layout: Layout<'_>,
225 cursor_position: Point,
226 _viewport: &Rectangle,
227 ) {
228 const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
230
231 const SPACE_RATIO: f32 = 0.05;
234
235 let mut children = layout.children();
236
237 if let Some(label) = &self.label {
238 let label_layout = children.next().unwrap();
239
240 crate::widget::text::draw(
241 renderer,
242 style,
243 label_layout,
244 label,
245 self.text_size,
246 self.font.clone(),
247 Default::default(),
248 self.text_alignment,
249 alignment::Vertical::Center,
250 );
251 }
252
253 let toggler_layout = children.next().unwrap();
254 let bounds = toggler_layout.bounds();
255
256 let is_mouse_over = bounds.contains(cursor_position);
257
258 let style = if is_mouse_over {
259 theme.hovered(&self.style, self.is_toggled)
260 } else {
261 theme.active(&self.style, self.is_toggled)
262 };
263
264 let border_radius = bounds.height / BORDER_RADIUS_RATIO;
265 let space = SPACE_RATIO * bounds.height;
266
267 let toggler_background_bounds = Rectangle {
268 x: bounds.x + space,
269 y: bounds.y + space,
270 width: bounds.width - (2.0 * space),
271 height: bounds.height - (2.0 * space),
272 };
273
274 renderer.fill_quad(
275 renderer::Quad {
276 bounds: toggler_background_bounds,
277 border_radius: border_radius.into(),
278 border_width: 1.0,
279 border_color: style
280 .background_border
281 .unwrap_or(style.background),
282 },
283 style.background,
284 );
285
286 let toggler_foreground_bounds = Rectangle {
287 x: bounds.x
288 + if self.is_toggled {
289 bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
290 } else {
291 2.0 * space
292 },
293 y: bounds.y + (2.0 * space),
294 width: bounds.height - (4.0 * space),
295 height: bounds.height - (4.0 * space),
296 };
297
298 renderer.fill_quad(
299 renderer::Quad {
300 bounds: toggler_foreground_bounds,
301 border_radius: border_radius.into(),
302 border_width: 1.0,
303 border_color: style
304 .foreground_border
305 .unwrap_or(style.foreground),
306 },
307 style.foreground,
308 );
309 }
310}
311
312impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>>
313 for Element<'a, Message, Renderer>
314where
315 Message: 'a,
316 Renderer: 'a + text::Renderer,
317 Renderer::Theme: StyleSheet + widget::text::StyleSheet,
318{
319 fn from(
320 toggler: Toggler<'a, Message, Renderer>,
321 ) -> Element<'a, Message, Renderer> {
322 Element::new(toggler)
323 }
324}