1use iced_widget::button::{Status, Style};
4use iced_widget::core::border::Radius;
5use iced_widget::core::text as core_text;
6use iced_widget::core::time::Instant;
7use iced_widget::core::{Background, Border, Color, Element, Length, Padding, alignment};
8use iced_widget::graphics::geometry;
9use iced_widget::renderer::wgpu::primitive;
10use iced_widget::text;
11use iced_widget::{Container, Row, Text};
12
13use super::absolute_line_height;
14use super::button::Button;
15use super::support::{AnimatedScalar, duration_ms};
16use crate::utils::{mix, state_layer};
17use crate::{Theme, fonts, tokens};
18
19#[derive(Debug, Clone)]
21pub struct State {
22 selected_index: usize,
23 previous_index: usize,
24 progress: AnimatedScalar,
25}
26
27impl State {
28 pub fn new(selected_index: usize) -> Self {
30 Self {
31 selected_index,
32 previous_index: selected_index,
33 progress: AnimatedScalar::new(1.0),
34 }
35 }
36
37 pub const fn selected_index(&self) -> usize {
39 self.selected_index
40 }
41
42 pub fn select(&mut self, selected_index: usize, now: Instant) {
44 if self.selected_index == selected_index {
45 return;
46 }
47
48 self.previous_index = self.selected_index;
49 self.selected_index = selected_index;
50 self.progress = AnimatedScalar::new(0.0);
51 self.progress.set_target(
52 1.0,
53 now,
54 duration_ms(tokens::component::segmented_button::SELECT_TRANSITION_DURATION_MS),
55 tokens::component::segmented_button::SELECT_TRANSITION_EASING,
56 );
57 }
58
59 pub fn advance(&mut self, now: Instant) -> bool {
61 self.progress.advance(now)
62 }
63
64 pub fn is_animating(&self) -> bool {
66 self.progress.is_animating()
67 }
68
69 pub fn progress_for(&self, index: usize) -> f32 {
71 let progress = self.progress.value.clamp(0.0, 1.0);
72
73 if index == self.selected_index {
74 progress
75 } else if index == self.previous_index {
76 1.0 - progress
77 } else {
78 0.0
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum SegmentPosition {
86 Only,
87 First,
88 Middle,
89 Last,
90}
91
92impl SegmentPosition {
93 pub fn for_index(index: usize, len: usize) -> Self {
95 if len <= 1 {
96 Self::Only
97 } else if index == 0 {
98 Self::First
99 } else if index + 1 == len {
100 Self::Last
101 } else {
102 Self::Middle
103 }
104 }
105
106 fn radius(self) -> Radius {
107 let full = tokens::component::segmented_button::CONTAINER_SHAPE;
108
109 match self {
110 Self::Only => Radius::new(full),
111 Self::First => Radius {
112 top_left: full,
113 top_right: 0.0,
114 bottom_right: 0.0,
115 bottom_left: full,
116 },
117 Self::Middle => Radius::default(),
118 Self::Last => Radius {
119 top_left: 0.0,
120 top_right: full,
121 bottom_right: full,
122 bottom_left: 0.0,
123 },
124 }
125 }
126}
127
128fn segment_overlap_spacing() -> f32 {
129 -tokens::component::segmented_button::OUTLINE_WIDTH
130}
131
132pub fn group<'a, Message, Renderer>(
134 segments: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
135) -> Row<'a, Message, Theme, Renderer>
136where
137 Message: 'a,
138 Renderer: iced_widget::core::Renderer + 'a,
139{
140 Row::with_children(segments.into_iter())
141 .spacing(segment_overlap_spacing())
142 .align_y(alignment::Vertical::Center)
143}
144
145pub fn label<'a, Message, Renderer>(
147 label: impl text::IntoFragment<'a>,
148 selected: bool,
149 position: SegmentPosition,
150) -> Button<'a, Message, Renderer>
151where
152 Message: Clone + 'a,
153 Renderer: geometry::Renderer + core_text::Renderer + 'a,
154{
155 segment_button(
156 Text::new(label)
157 .size(tokens::component::segmented_button::LABEL_TEXT.size)
158 .line_height(absolute_line_height(
159 tokens::component::segmented_button::LABEL_TEXT.line_height,
160 ))
161 .into(),
162 selected,
163 position,
164 )
165}
166
167pub fn selectable_label<'a, Message, Renderer>(
169 label_text: impl text::IntoFragment<'a>,
170 selected: bool,
171 position: SegmentPosition,
172) -> Button<'a, Message, Renderer>
173where
174 Message: Clone + 'a,
175 Renderer: geometry::Renderer + core_text::Renderer + 'a,
176 iced_widget::core::Font: Into<Renderer::Font>,
177{
178 animated_selectable_label(label_text, selected.then_some(1.0).unwrap_or(0.0), position)
179}
180
181pub fn animated_selectable_label<'a, Message, Renderer>(
183 label: impl text::IntoFragment<'a>,
184 selected_progress: f32,
185 position: SegmentPosition,
186) -> Button<'a, Message, Renderer>
187where
188 Message: Clone + 'a,
189 Renderer: geometry::Renderer + core_text::Renderer + 'a,
190 iced_widget::core::Font: Into<Renderer::Font>,
191{
192 leading_icon_progress("check", label, selected_progress, position)
193}
194
195pub fn animated_selectable_label_action<'a, Message, Renderer>(
197 label: impl text::IntoFragment<'a>,
198 selected_progress: f32,
199 position: SegmentPosition,
200 on_press: Message,
201) -> Element<'a, Message, Theme, Renderer>
202where
203 Message: Clone + 'a,
204 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
205 iced_widget::core::Font: Into<Renderer::Font>,
206{
207 animated_selectable_label(label, selected_progress, position)
208 .on_press(on_press)
209 .into()
210}
211
212pub fn animated_selectable_label_actions<'a, Message, Renderer, Label>(
214 state: &State,
215 segments: impl IntoIterator<Item = (Label, Message)>,
216) -> Vec<Element<'a, Message, Theme, Renderer>>
217where
218 Label: text::IntoFragment<'a>,
219 Message: Clone + 'a,
220 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
221 iced_widget::core::Font: Into<Renderer::Font>,
222{
223 let segments: Vec<_> = segments.into_iter().collect();
224 let len = segments.len();
225
226 segments
227 .into_iter()
228 .enumerate()
229 .map(|(index, (label, on_press))| {
230 animated_selectable_label_action(
231 label,
232 state.progress_for(index),
233 SegmentPosition::for_index(index, len),
234 on_press,
235 )
236 })
237 .collect()
238}
239
240pub fn leading_icon<'a, Message, Renderer>(
242 icon_name: impl text::IntoFragment<'a>,
243 label: impl text::IntoFragment<'a>,
244 selected: bool,
245 position: SegmentPosition,
246) -> Button<'a, Message, Renderer>
247where
248 Message: Clone + 'a,
249 Renderer: geometry::Renderer + core_text::Renderer + 'a,
250 iced_widget::core::Font: Into<Renderer::Font>,
251{
252 leading_icon_progress(
253 icon_name,
254 label,
255 selected.then_some(1.0).unwrap_or(0.0),
256 position,
257 )
258}
259
260fn leading_icon_progress<'a, Message, Renderer>(
261 icon_name: impl text::IntoFragment<'a>,
262 label: impl text::IntoFragment<'a>,
263 selected_progress: f32,
264 position: SegmentPosition,
265) -> Button<'a, Message, Renderer>
266where
267 Message: Clone + 'a,
268 Renderer: geometry::Renderer + core_text::Renderer + 'a,
269 iced_widget::core::Font: Into<Renderer::Font>,
270{
271 let progress = selected_progress.clamp(0.0, 1.0);
272 let label_text = tokens::component::segmented_button::LABEL_TEXT;
273 let icon = fonts::filled_icon(
274 icon_name,
275 tokens::component::segmented_button::WITH_ICON_ICON_SIZE * progress,
276 )
277 .width(Length::Fixed(
278 tokens::component::segmented_button::WITH_ICON_ICON_SIZE * progress,
279 ))
280 .height(Length::Fixed(
281 tokens::component::segmented_button::WITH_ICON_ICON_SIZE,
282 ));
283
284 let content = Row::<Message, Theme, Renderer>::new()
285 .push(icon)
286 .push(
287 Text::new(label)
288 .size(label_text.size)
289 .line_height(absolute_line_height(label_text.line_height)),
290 )
291 .spacing(tokens::component::segmented_button::ICON_LABEL_SPACE * progress)
292 .align_y(alignment::Vertical::Center);
293
294 segment_button_progress(content.into(), progress, position)
295}
296
297fn segment_button<'a, Message, Renderer>(
298 content: Element<'a, Message, Theme, Renderer>,
299 selected: bool,
300 position: SegmentPosition,
301) -> Button<'a, Message, Renderer>
302where
303 Message: Clone + 'a,
304 Renderer: geometry::Renderer + core_text::Renderer + 'a,
305{
306 segment_button_progress(content, selected.then_some(1.0).unwrap_or(0.0), position)
307}
308
309fn segment_button_progress<'a, Message, Renderer>(
310 content: Element<'a, Message, Theme, Renderer>,
311 selected_progress: f32,
312 position: SegmentPosition,
313) -> Button<'a, Message, Renderer>
314where
315 Message: Clone + 'a,
316 Renderer: geometry::Renderer + core_text::Renderer + 'a,
317{
318 let progress = selected_progress.clamp(0.0, 1.0);
319
320 Button::new(
321 Container::new(content)
322 .height(Length::Fixed(
323 tokens::component::segmented_button::CONTAINER_HEIGHT,
324 ))
325 .padding(Padding {
326 top: 0.0,
327 right: tokens::component::segmented_button::TRAILING_SPACE,
328 bottom: 0.0,
329 left: tokens::component::segmented_button::LEADING_SPACE,
330 })
331 .align_y(alignment::Vertical::Center),
332 )
333 .height(Length::Fixed(
334 tokens::component::segmented_button::CONTAINER_HEIGHT,
335 ))
336 .padding(Padding::ZERO)
337 .style(move |theme, status| segmented_style_progress(theme, status, progress, position))
338}
339
340pub fn segmented_style(
342 theme: &Theme,
343 status: Status,
344 selected: bool,
345 position: SegmentPosition,
346) -> Style {
347 segmented_style_progress(
348 theme,
349 status,
350 selected.then_some(1.0).unwrap_or(0.0),
351 position,
352 )
353}
354
355pub fn segmented_style_progress(
357 theme: &Theme,
358 status: Status,
359 selected_progress: f32,
360 position: SegmentPosition,
361) -> Style {
362 let colors = theme.colors();
363 let radius = position.radius();
364 let progress = selected_progress.clamp(0.0, 1.0);
365 let outline = colors.outline.color;
366 let container = if progress > 0.0 {
367 Some(Color {
368 a: colors.secondary.container.a * progress,
369 ..colors.secondary.container
370 })
371 } else {
372 None
373 };
374 let content = mix(
375 colors.surface.text,
376 colors.secondary.container_text,
377 progress,
378 );
379 let layer = mix(
380 colors.surface.text,
381 colors.secondary.container_text,
382 progress,
383 );
384
385 let border = Border {
386 color: outline,
387 width: tokens::component::segmented_button::OUTLINE_WIDTH,
388 radius,
389 };
390
391 let active = Style {
392 background: container.map(Background::Color),
393 text_color: content,
394 border,
395 shadow: Default::default(),
396 snap: cfg!(feature = "crisp"),
397 };
398
399 match status {
400 Status::Active => active,
401 Status::Hovered => Style {
402 background: Some(Background::Color(state_background(
403 container,
404 layer,
405 tokens::component::segmented_button::HOVER_STATE_LAYER_OPACITY,
406 ))),
407 ..active
408 },
409 Status::Pressed => Style {
410 background: Some(Background::Color(state_background(
411 container,
412 layer,
413 tokens::component::segmented_button::PRESSED_STATE_LAYER_OPACITY,
414 ))),
415 ..active
416 },
417 Status::Disabled => Style {
418 background: (progress > 0.0).then_some(Background::Color(Color {
419 a: tokens::state::DISABLED_CONTAINER_OPACITY,
420 ..colors.surface.text
421 })),
422 text_color: Color {
423 a: tokens::component::segmented_button::DISABLED_LABEL_TEXT_OPACITY,
424 ..colors.surface.text
425 },
426 border: Border {
427 color: Color {
428 a: tokens::component::segmented_button::DISABLED_OUTLINE_OPACITY,
429 ..colors.surface.text
430 },
431 ..border
432 },
433 ..active
434 },
435 }
436}
437
438fn state_background(container: Option<Color>, layer: Color, opacity: f32) -> Color {
439 container.map_or_else(
440 || state_layer(layer, opacity),
441 |color| mix(color, layer, opacity),
442 )
443}
444
445#[cfg(test)]
446#[path = "../../../tests/widget/component/segmented_button.rs"]
447mod tests;