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, Shadow, 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)
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, if selected { 1.0 } else { 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_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_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_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(icon_name, label, if selected { 1.0 } else { 0.0 }, position)
253}
254
255fn leading_icon_progress<'a, Message, Renderer>(
256 icon_name: impl text::IntoFragment<'a>,
257 label: impl text::IntoFragment<'a>,
258 selected_progress: f32,
259 position: SegmentPosition,
260) -> Button<'a, Message, Renderer>
261where
262 Message: Clone + 'a,
263 Renderer: geometry::Renderer + core_text::Renderer + 'a,
264 iced_widget::core::Font: Into<Renderer::Font>,
265{
266 let progress = selected_progress.clamp(0.0, 1.0);
267 let label_text = tokens::component::segmented_button::LABEL_TEXT;
268 let icon = fonts::filled_icon(
269 icon_name,
270 tokens::component::segmented_button::WITH_ICON_ICON_SIZE * progress,
271 )
272 .width(Length::Fixed(
273 tokens::component::segmented_button::WITH_ICON_ICON_SIZE * progress,
274 ))
275 .height(Length::Fixed(
276 tokens::component::segmented_button::WITH_ICON_ICON_SIZE,
277 ));
278
279 let content = Row::<Message, Theme, Renderer>::new()
280 .push(icon)
281 .push(
282 Text::new(label)
283 .size(label_text.size)
284 .line_height(absolute_line_height(label_text.line_height)),
285 )
286 .spacing(tokens::component::segmented_button::ICON_LABEL_SPACE * progress)
287 .align_y(alignment::Vertical::Center);
288
289 segment_button_progress(content.into(), progress, position)
290}
291
292fn segment_button<'a, Message, Renderer>(
293 content: Element<'a, Message, Theme, Renderer>,
294 selected: bool,
295 position: SegmentPosition,
296) -> Button<'a, Message, Renderer>
297where
298 Message: Clone + 'a,
299 Renderer: geometry::Renderer + core_text::Renderer + 'a,
300{
301 segment_button_progress(content, if selected { 1.0 } else { 0.0 }, position)
302}
303
304fn segment_button_progress<'a, Message, Renderer>(
305 content: Element<'a, Message, Theme, Renderer>,
306 selected_progress: f32,
307 position: SegmentPosition,
308) -> Button<'a, Message, Renderer>
309where
310 Message: Clone + 'a,
311 Renderer: geometry::Renderer + core_text::Renderer + 'a,
312{
313 let progress = selected_progress.clamp(0.0, 1.0);
314
315 Button::new(
316 Container::new(content)
317 .height(Length::Fixed(
318 tokens::component::segmented_button::CONTAINER_HEIGHT,
319 ))
320 .padding(Padding {
321 top: 0.0,
322 right: tokens::component::segmented_button::TRAILING_SPACE,
323 bottom: 0.0,
324 left: tokens::component::segmented_button::LEADING_SPACE,
325 })
326 .align_y(alignment::Vertical::Center),
327 )
328 .height(Length::Fixed(
329 tokens::component::segmented_button::CONTAINER_HEIGHT,
330 ))
331 .padding(Padding::ZERO)
332 .style(move |theme, status| segmented_style_progress(theme, status, progress, position))
333}
334
335pub fn segmented_style(
337 theme: &Theme,
338 status: Status,
339 selected: bool,
340 position: SegmentPosition,
341) -> Style {
342 segmented_style_progress(theme, status, if selected { 1.0 } else { 0.0 }, position)
343}
344
345pub fn segmented_style_progress(
347 theme: &Theme,
348 status: Status,
349 selected_progress: f32,
350 position: SegmentPosition,
351) -> Style {
352 let colors = theme.colors();
353 let radius = position.radius();
354 let progress = selected_progress.clamp(0.0, 1.0);
355 let outline = colors.outline.color;
356 let container = if progress > 0.0 {
357 Some(Color {
358 a: colors.secondary.container.a * progress,
359 ..colors.secondary.container
360 })
361 } else {
362 None
363 };
364 let content = mix(
365 colors.surface.text,
366 colors.secondary.container_text,
367 progress,
368 );
369 let layer = mix(
370 colors.surface.text,
371 colors.secondary.container_text,
372 progress,
373 );
374
375 let border = Border {
376 color: outline,
377 width: tokens::component::segmented_button::OUTLINE_WIDTH,
378 radius,
379 };
380
381 let active = Style {
382 background: container.map(Background::Color),
383 text_color: content,
384 border,
385 shadow: Shadow::default(),
386 snap: cfg!(feature = "crisp"),
387 };
388
389 match status {
390 Status::Active => active,
391 Status::Hovered => Style {
392 background: Some(Background::Color(state_background(
393 container,
394 layer,
395 tokens::component::segmented_button::HOVER_STATE_LAYER_OPACITY,
396 ))),
397 ..active
398 },
399 Status::Pressed => Style {
400 background: Some(Background::Color(state_background(
401 container,
402 layer,
403 tokens::component::segmented_button::PRESSED_STATE_LAYER_OPACITY,
404 ))),
405 ..active
406 },
407 Status::Disabled => Style {
408 background: (progress > 0.0).then_some(Background::Color(Color {
409 a: tokens::state::DISABLED_CONTAINER_OPACITY,
410 ..colors.surface.text
411 })),
412 text_color: Color {
413 a: tokens::component::segmented_button::DISABLED_LABEL_TEXT_OPACITY,
414 ..colors.surface.text
415 },
416 border: Border {
417 color: Color {
418 a: tokens::component::segmented_button::DISABLED_OUTLINE_OPACITY,
419 ..colors.surface.text
420 },
421 ..border
422 },
423 ..active
424 },
425 }
426}
427
428fn state_background(container: Option<Color>, layer: Color, opacity: f32) -> Color {
429 container.map_or_else(
430 || state_layer(layer, opacity),
431 |color| mix(color, layer, opacity),
432 )
433}
434
435#[cfg(test)]
436#[path = "../../../tests/widget/component/segmented_button.rs"]
437mod tests;