1use crate::alignment;
3use crate::event::{self, Event};
4use crate::layout;
5use crate::mouse;
6use crate::renderer;
7use crate::text;
8use crate::touch;
9use crate::widget::{self, Row, Text, Tree};
10use crate::{
11 Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point,
12 Rectangle, Shell, Widget,
13};
14
15pub use iced_style::radio::{Appearance, StyleSheet};
16
17#[allow(missing_debug_implementations)]
71pub struct Radio<Message, Renderer>
72where
73 Renderer: text::Renderer,
74 Renderer::Theme: StyleSheet,
75{
76 is_selected: bool,
77 on_click: Message,
78 label: String,
79 width: Length,
80 size: f32,
81 spacing: f32,
82 text_size: Option<f32>,
83 font: Renderer::Font,
84 style: <Renderer::Theme as StyleSheet>::Style,
85}
86
87impl<Message, Renderer> Radio<Message, Renderer>
88where
89 Message: Clone,
90 Renderer: text::Renderer,
91 Renderer::Theme: StyleSheet,
92{
93 pub const DEFAULT_SIZE: f32 = 28.0;
95
96 pub const DEFAULT_SPACING: f32 = 15.0;
98
99 pub fn new<F, V>(
108 label: impl Into<String>,
109 value: V,
110 selected: Option<V>,
111 f: F,
112 ) -> Self
113 where
114 V: Eq + Copy,
115 F: FnOnce(V) -> Message,
116 {
117 Radio {
118 is_selected: Some(value) == selected,
119 on_click: f(value),
120 label: label.into(),
121 width: Length::Shrink,
122 size: Self::DEFAULT_SIZE,
123 spacing: Self::DEFAULT_SPACING, text_size: None,
125 font: Default::default(),
126 style: Default::default(),
127 }
128 }
129
130 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
132 self.size = size.into().0;
133 self
134 }
135
136 pub fn width(mut self, width: impl Into<Length>) -> Self {
138 self.width = width.into();
139 self
140 }
141
142 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
144 self.spacing = spacing.into().0;
145 self
146 }
147
148 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
150 self.text_size = Some(text_size.into().0);
151 self
152 }
153
154 pub fn font(mut self, font: Renderer::Font) -> Self {
156 self.font = font;
157 self
158 }
159
160 pub fn style(
162 mut self,
163 style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
164 ) -> Self {
165 self.style = style.into();
166 self
167 }
168}
169
170impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer>
171where
172 Message: Clone,
173 Renderer: text::Renderer,
174 Renderer::Theme: StyleSheet + widget::text::StyleSheet,
175{
176 fn width(&self) -> Length {
177 self.width
178 }
179
180 fn height(&self) -> Length {
181 Length::Shrink
182 }
183
184 fn layout(
185 &self,
186 renderer: &Renderer,
187 limits: &layout::Limits,
188 ) -> layout::Node {
189 Row::<(), Renderer>::new()
190 .width(self.width)
191 .spacing(self.spacing)
192 .align_items(Alignment::Center)
193 .push(Row::new().width(self.size).height(self.size))
194 .push(Text::new(&self.label).width(self.width).size(
195 self.text_size.unwrap_or_else(|| renderer.default_size()),
196 ))
197 .layout(renderer, limits)
198 }
199
200 fn on_event(
201 &mut self,
202 _state: &mut Tree,
203 event: Event,
204 layout: Layout<'_>,
205 cursor_position: Point,
206 _renderer: &Renderer,
207 _clipboard: &mut dyn Clipboard,
208 shell: &mut Shell<'_, Message>,
209 ) -> event::Status {
210 match event {
211 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
212 | Event::Touch(touch::Event::FingerPressed { .. }) => {
213 if layout.bounds().contains(cursor_position) {
214 shell.publish(self.on_click.clone());
215
216 return event::Status::Captured;
217 }
218 }
219 _ => {}
220 }
221
222 event::Status::Ignored
223 }
224
225 fn mouse_interaction(
226 &self,
227 _state: &Tree,
228 layout: Layout<'_>,
229 cursor_position: Point,
230 _viewport: &Rectangle,
231 _renderer: &Renderer,
232 ) -> mouse::Interaction {
233 if layout.bounds().contains(cursor_position) {
234 mouse::Interaction::Pointer
235 } else {
236 mouse::Interaction::default()
237 }
238 }
239
240 fn draw(
241 &self,
242 _state: &Tree,
243 renderer: &mut Renderer,
244 theme: &Renderer::Theme,
245 style: &renderer::Style,
246 layout: Layout<'_>,
247 cursor_position: Point,
248 _viewport: &Rectangle,
249 ) {
250 let bounds = layout.bounds();
251 let is_mouse_over = bounds.contains(cursor_position);
252
253 let mut children = layout.children();
254
255 let custom_style = if is_mouse_over {
256 theme.hovered(&self.style, self.is_selected)
257 } else {
258 theme.active(&self.style, self.is_selected)
259 };
260
261 {
262 let layout = children.next().unwrap();
263 let bounds = layout.bounds();
264
265 let size = bounds.width;
266 let dot_size = size / 2.0;
267
268 renderer.fill_quad(
269 renderer::Quad {
270 bounds,
271 border_radius: (size / 2.0).into(),
272 border_width: custom_style.border_width,
273 border_color: custom_style.border_color,
274 },
275 custom_style.background,
276 );
277
278 if self.is_selected {
279 renderer.fill_quad(
280 renderer::Quad {
281 bounds: Rectangle {
282 x: bounds.x + dot_size / 2.0,
283 y: bounds.y + dot_size / 2.0,
284 width: bounds.width - dot_size,
285 height: bounds.height - dot_size,
286 },
287 border_radius: (dot_size / 2.0).into(),
288 border_width: 0.0,
289 border_color: Color::TRANSPARENT,
290 },
291 custom_style.dot_color,
292 );
293 }
294 }
295
296 {
297 let label_layout = children.next().unwrap();
298
299 widget::text::draw(
300 renderer,
301 style,
302 label_layout,
303 &self.label,
304 self.text_size,
305 self.font.clone(),
306 widget::text::Appearance {
307 color: custom_style.text_color,
308 },
309 alignment::Horizontal::Left,
310 alignment::Vertical::Center,
311 );
312 }
313 }
314}
315
316impl<'a, Message, Renderer> From<Radio<Message, Renderer>>
317 for Element<'a, Message, Renderer>
318where
319 Message: 'a + Clone,
320 Renderer: 'a + text::Renderer,
321 Renderer::Theme: StyleSheet + widget::text::StyleSheet,
322{
323 fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> {
324 Element::new(radio)
325 }
326}