1use iced_widget::core::text as core_text;
4use iced_widget::core::time::Instant;
5use iced_widget::core::{Background, Color, Element, Length, Padding, alignment, border};
6use iced_widget::{Column, Container, Space};
7
8use super::support::{AnimatedScalar, alpha_color, duration_ms, lerp};
9use crate::utils::shadow_from_level;
10use crate::{Theme, tokens};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Value {
14 Hidden,
15 PartiallyExpanded,
16 Expanded,
17}
18
19impl Value {
20 const fn progress(self) -> f32 {
21 match self {
22 Self::Hidden => 0.0,
23 Self::PartiallyExpanded => 0.5,
24 Self::Expanded => 1.0,
25 }
26 }
27}
28
29#[derive(Debug, Clone)]
30pub struct State {
31 from: Value,
32 target: Value,
33 progress: AnimatedScalar,
34}
35
36impl State {
37 pub fn new(value: Value) -> Self {
38 Self {
39 from: value,
40 target: value,
41 progress: AnimatedScalar::new(1.0),
42 }
43 }
44
45 pub const fn target(&self) -> Value {
46 self.target
47 }
48
49 pub fn show(&mut self, now: Instant) {
50 self.set_target(Value::PartiallyExpanded, now);
51 }
52
53 pub fn expand(&mut self, now: Instant) {
54 self.set_target(Value::Expanded, now);
55 }
56
57 pub fn partial_expand(&mut self, now: Instant) {
58 self.set_target(Value::PartiallyExpanded, now);
59 }
60
61 pub fn hide(&mut self, now: Instant) {
62 self.set_target(Value::Hidden, now);
63 }
64
65 pub fn set_target(&mut self, target: Value, now: Instant) {
66 if self.target == target && !self.progress.is_animating() {
67 return;
68 }
69
70 self.from = self.current_value();
71 self.target = target;
72 self.progress = AnimatedScalar::new(0.0);
73 self.progress.set_target(
74 1.0,
75 now,
76 duration_ms(tokens::component::bottom_sheet::ANIMATION_DURATION_MS),
77 tokens::component::bottom_sheet::ANIMATION_EASING,
78 );
79 }
80
81 pub fn advance(&mut self, now: Instant) -> bool {
82 self.progress.advance(now)
83 }
84
85 pub fn is_animating(&self) -> bool {
86 self.progress.is_animating()
87 }
88
89 pub fn visibility_progress(&self) -> f32 {
90 lerp(
91 self.from.progress(),
92 self.target.progress(),
93 self.progress.value.clamp(0.0, 1.0),
94 )
95 }
96
97 fn current_value(&self) -> Value {
98 if self.progress.value >= 1.0 {
99 self.target
100 } else {
101 self.from
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum Side {
108 Left,
109 Right,
110}
111
112impl Side {
113 const fn alignment(self) -> alignment::Horizontal {
114 match self {
115 Self::Left => alignment::Horizontal::Left,
116 Self::Right => alignment::Horizontal::Right,
117 }
118 }
119
120 const fn hidden_translation_fraction(self) -> f32 {
121 match self {
122 Self::Left => -1.0,
123 Self::Right => 1.0,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum SideValue {
130 Hidden,
131 Expanded,
132}
133
134impl SideValue {
135 const fn progress(self) -> f32 {
136 match self {
137 Self::Hidden => 0.0,
138 Self::Expanded => 1.0,
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
144pub struct SideState {
145 from: SideValue,
146 target: SideValue,
147 progress: AnimatedScalar,
148}
149
150impl SideState {
151 pub fn new(value: SideValue) -> Self {
152 Self {
153 from: value,
154 target: value,
155 progress: AnimatedScalar::new(1.0),
156 }
157 }
158
159 pub const fn target(&self) -> SideValue {
160 self.target
161 }
162
163 pub fn show(&mut self, now: Instant) {
164 self.set_target(SideValue::Expanded, now);
165 }
166
167 pub fn hide(&mut self, now: Instant) {
168 self.set_target(SideValue::Hidden, now);
169 }
170
171 pub fn set_target(&mut self, target: SideValue, now: Instant) {
172 if self.target == target && !self.progress.is_animating() {
173 return;
174 }
175
176 self.from = self.current_value();
177 self.target = target;
178 self.progress = AnimatedScalar::new(0.0);
179 self.progress.set_target(
180 1.0,
181 now,
182 duration_ms(tokens::component::side_sheet::ANIMATION_DURATION_MS),
183 tokens::component::side_sheet::ANIMATION_EASING,
184 );
185 }
186
187 pub fn advance(&mut self, now: Instant) -> bool {
188 self.progress.advance(now)
189 }
190
191 pub fn is_animating(&self) -> bool {
192 self.progress.is_animating()
193 }
194
195 pub fn visibility_progress(&self) -> f32 {
196 lerp(
197 self.from.progress(),
198 self.target.progress(),
199 self.progress.value.clamp(0.0, 1.0),
200 )
201 }
202
203 pub fn translation_fraction(&self, side: Side) -> f32 {
204 side.hidden_translation_fraction() * (1.0 - self.visibility_progress())
205 }
206
207 fn current_value(&self) -> SideValue {
208 if self.progress.value >= 1.0 {
209 self.target
210 } else {
211 self.from
212 }
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum Kind {
218 Modal,
219 Standard,
220}
221
222pub fn modal_bottom<'a, Message, Renderer>(
223 content: impl Into<Element<'a, Message, Theme, Renderer>>,
224) -> Container<'a, Message, Theme, Renderer>
225where
226 Message: 'a,
227 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
228{
229 bottom_sheet(Kind::Modal, content, true)
230}
231
232pub fn standard_bottom<'a, Message, Renderer>(
233 content: impl Into<Element<'a, Message, Theme, Renderer>>,
234) -> Container<'a, Message, Theme, Renderer>
235where
236 Message: 'a,
237 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
238{
239 bottom_sheet(Kind::Standard, content, true)
240}
241
242pub fn bottom_sheet<'a, Message, Renderer>(
243 kind: Kind,
244 content: impl Into<Element<'a, Message, Theme, Renderer>>,
245 show_drag_handle: bool,
246) -> Container<'a, Message, Theme, Renderer>
247where
248 Message: 'a,
249 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
250{
251 let mut column = Column::new().width(Length::Fill);
252
253 if show_drag_handle {
254 column = column.push(drag_handle());
255 }
256
257 column = column.push(content.into());
258
259 Container::new(column)
260 .width(Length::Fill)
261 .max_width(tokens::component::bottom_sheet::SHEET_MAX_WIDTH)
262 .style(move |theme| bottom_sheet_style(theme, kind))
263}
264
265pub fn bottom_content<'a, Message, Renderer>(
266 content: impl Into<Element<'a, Message, Theme, Renderer>>,
267) -> Container<'a, Message, Theme, Renderer>
268where
269 Message: 'a,
270 Renderer: iced_widget::core::Renderer + 'a,
271{
272 Container::new(content)
273 .padding(Padding {
274 top: 0.0,
275 right: tokens::component::bottom_sheet::CONTENT_PADDING,
276 bottom: tokens::component::bottom_sheet::CONTENT_PADDING,
277 left: tokens::component::bottom_sheet::CONTENT_PADDING,
278 })
279 .width(Length::Fill)
280}
281
282pub fn drag_handle<'a, Message, Renderer>() -> Container<'a, Message, Theme, Renderer>
283where
284 Message: 'a,
285 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
286{
287 let handle = Container::new(
288 Space::new()
289 .width(Length::Fixed(
290 tokens::component::bottom_sheet::DRAG_HANDLE_WIDTH,
291 ))
292 .height(Length::Fixed(
293 tokens::component::bottom_sheet::DRAG_HANDLE_HEIGHT,
294 )),
295 )
296 .style(drag_handle_style);
297
298 Container::new(handle)
299 .width(Length::Fill)
300 .padding(Padding {
301 top: tokens::component::bottom_sheet::DRAG_HANDLE_VERTICAL_PADDING,
302 right: 0.0,
303 bottom: tokens::component::bottom_sheet::DRAG_HANDLE_VERTICAL_PADDING,
304 left: 0.0,
305 })
306 .align_x(alignment::Horizontal::Center)
307}
308
309pub fn scrim<'a, Message, Renderer>(
310 content: impl Into<Element<'a, Message, Theme, Renderer>>,
311) -> Container<'a, Message, Theme, Renderer>
312where
313 Message: 'a,
314 Renderer: iced_widget::core::Renderer + 'a,
315{
316 Container::new(content)
317 .width(Length::Fill)
318 .height(Length::Fill)
319 .style(scrim_style)
320}
321
322pub fn modal_overlay<'a, Message, Renderer>(
323 content: impl Into<Element<'a, Message, Theme, Renderer>>,
324) -> Container<'a, Message, Theme, Renderer>
325where
326 Message: 'a,
327 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
328{
329 scrim(
330 Container::new(modal_bottom(content))
331 .width(Length::Fill)
332 .height(Length::Fill)
333 .align_x(alignment::Horizontal::Center)
334 .align_y(alignment::Vertical::Bottom),
335 )
336}
337
338pub fn modal_side<'a, Message, Renderer>(
339 content: impl Into<Element<'a, Message, Theme, Renderer>>,
340) -> Container<'a, Message, Theme, Renderer>
341where
342 Message: 'a,
343 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
344{
345 modal_side_on(Side::Right, content)
346}
347
348pub fn modal_side_on<'a, Message, Renderer>(
349 side: Side,
350 content: impl Into<Element<'a, Message, Theme, Renderer>>,
351) -> Container<'a, Message, Theme, Renderer>
352where
353 Message: 'a,
354 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
355{
356 side_sheet(Kind::Modal, side, false, content)
357}
358
359pub fn detached_modal_side<'a, Message, Renderer>(
360 content: impl Into<Element<'a, Message, Theme, Renderer>>,
361) -> Container<'a, Message, Theme, Renderer>
362where
363 Message: 'a,
364 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
365{
366 detached_modal_side_on(Side::Right, content)
367}
368
369pub fn detached_modal_side_on<'a, Message, Renderer>(
370 side: Side,
371 content: impl Into<Element<'a, Message, Theme, Renderer>>,
372) -> Container<'a, Message, Theme, Renderer>
373where
374 Message: 'a,
375 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
376{
377 side_sheet(Kind::Modal, side, true, content)
378}
379
380pub fn standard_side<'a, Message, Renderer>(
381 content: impl Into<Element<'a, Message, Theme, Renderer>>,
382) -> Container<'a, Message, Theme, Renderer>
383where
384 Message: 'a,
385 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
386{
387 standard_side_on(Side::Right, content)
388}
389
390pub fn standard_side_on<'a, Message, Renderer>(
391 side: Side,
392 content: impl Into<Element<'a, Message, Theme, Renderer>>,
393) -> Container<'a, Message, Theme, Renderer>
394where
395 Message: 'a,
396 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
397{
398 side_sheet(Kind::Standard, side, false, content)
399}
400
401pub fn detached_standard_side<'a, Message, Renderer>(
402 content: impl Into<Element<'a, Message, Theme, Renderer>>,
403) -> Container<'a, Message, Theme, Renderer>
404where
405 Message: 'a,
406 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
407{
408 detached_standard_side_on(Side::Right, content)
409}
410
411pub fn detached_standard_side_on<'a, Message, Renderer>(
412 side: Side,
413 content: impl Into<Element<'a, Message, Theme, Renderer>>,
414) -> Container<'a, Message, Theme, Renderer>
415where
416 Message: 'a,
417 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
418{
419 side_sheet(Kind::Standard, side, true, content)
420}
421
422pub fn side_sheet<'a, Message, Renderer>(
423 kind: Kind,
424 side: Side,
425 detached: bool,
426 content: impl Into<Element<'a, Message, Theme, Renderer>>,
427) -> Container<'a, Message, Theme, Renderer>
428where
429 Message: 'a,
430 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
431{
432 let surface = Container::new(content)
433 .width(Length::Fixed(
434 tokens::component::side_sheet::DOCKED_CONTAINER_WIDTH,
435 ))
436 .height(Length::Fill)
437 .style(move |theme| side_sheet_style(theme, kind, side, detached));
438
439 Container::new(surface)
440 .height(Length::Fill)
441 .padding(if detached {
442 tokens::component::side_sheet::DETACHED_MARGIN
443 } else {
444 0.0
445 })
446}
447
448pub fn side_content<'a, Message, Renderer>(
449 content: impl Into<Element<'a, Message, Theme, Renderer>>,
450) -> Container<'a, Message, Theme, Renderer>
451where
452 Message: 'a,
453 Renderer: iced_widget::core::Renderer + 'a,
454{
455 Container::new(content)
456 .padding(tokens::component::side_sheet::CONTENT_PADDING)
457 .width(Length::Fill)
458 .height(Length::Fill)
459}
460
461pub fn modal_side_overlay<'a, Message, Renderer>(
462 side: Side,
463 content: impl Into<Element<'a, Message, Theme, Renderer>>,
464) -> Container<'a, Message, Theme, Renderer>
465where
466 Message: 'a,
467 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
468{
469 side_scrim(
470 Container::new(modal_side_on(side, content))
471 .width(Length::Fill)
472 .height(Length::Fill)
473 .align_x(side.alignment()),
474 )
475}
476
477pub fn side_scrim<'a, Message, Renderer>(
478 content: impl Into<Element<'a, Message, Theme, Renderer>>,
479) -> Container<'a, Message, Theme, Renderer>
480where
481 Message: 'a,
482 Renderer: iced_widget::core::Renderer + 'a,
483{
484 Container::new(content)
485 .width(Length::Fill)
486 .height(Length::Fill)
487 .style(side_scrim_style)
488}
489
490fn bottom_sheet_radius() -> border::Radius {
491 border::Radius {
492 top_left: tokens::component::bottom_sheet::CONTAINER_SHAPE_TOP,
493 top_right: tokens::component::bottom_sheet::CONTAINER_SHAPE_TOP,
494 bottom_right: tokens::component::bottom_sheet::CONTAINER_SHAPE_BOTTOM,
495 bottom_left: tokens::component::bottom_sheet::CONTAINER_SHAPE_BOTTOM,
496 }
497}
498
499fn side_sheet_radius(kind: Kind, side: Side, detached: bool) -> border::Radius {
500 if detached {
501 return border::rounded(tokens::component::side_sheet::DETACHED_CONTAINER_SHAPE).radius;
502 }
503
504 match kind {
505 Kind::Standard => {
506 border::rounded(tokens::component::side_sheet::DOCKED_STANDARD_CONTAINER_SHAPE).radius
507 }
508 Kind::Modal => {
509 let radius = tokens::component::side_sheet::DOCKED_MODAL_CONTAINER_SHAPE;
510
511 match side {
512 Side::Left => border::Radius {
513 top_left: 0.0,
514 top_right: radius,
515 bottom_right: radius,
516 bottom_left: 0.0,
517 },
518 Side::Right => border::Radius {
519 top_left: radius,
520 top_right: 0.0,
521 bottom_right: 0.0,
522 bottom_left: radius,
523 },
524 }
525 }
526 }
527}
528
529fn bottom_sheet_style(theme: &Theme, kind: Kind) -> iced_widget::container::Style {
530 let colors = theme.colors();
531 let level = match kind {
532 Kind::Modal => tokens::component::bottom_sheet::MODAL_CONTAINER_ELEVATION_LEVEL,
533 Kind::Standard => tokens::component::bottom_sheet::STANDARD_CONTAINER_ELEVATION_LEVEL,
534 };
535
536 iced_widget::container::Style {
537 background: Some(Background::Color(colors.surface.container.low)),
538 text_color: Some(colors.surface.text),
539 border: border::rounded(bottom_sheet_radius()),
540 shadow: shadow_from_level(level, colors.shadow),
541 snap: cfg!(feature = "crisp"),
542 }
543}
544
545fn side_sheet_style(
546 theme: &Theme,
547 kind: Kind,
548 side: Side,
549 detached: bool,
550) -> iced_widget::container::Style {
551 let colors = theme.colors();
552 let (container, level) = match kind {
553 Kind::Modal => (
554 colors.surface.container.low,
555 tokens::component::side_sheet::MODAL_CONTAINER_ELEVATION_LEVEL,
556 ),
557 Kind::Standard => (
558 colors.surface.color,
559 tokens::component::side_sheet::STANDARD_CONTAINER_ELEVATION_LEVEL,
560 ),
561 };
562
563 iced_widget::container::Style {
564 background: Some(Background::Color(container)),
565 text_color: Some(colors.surface.text),
566 border: border::rounded(side_sheet_radius(kind, side, detached)),
567 shadow: shadow_from_level(level, colors.shadow),
568 snap: cfg!(feature = "crisp"),
569 }
570}
571
572fn drag_handle_style(theme: &Theme) -> iced_widget::container::Style {
573 iced_widget::container::Style {
574 background: Some(Background::Color(theme.colors().surface.text_variant)),
575 border: border::rounded(tokens::shape::CORNER_FULL),
576 snap: cfg!(feature = "crisp"),
577 ..iced_widget::container::Style::default()
578 }
579}
580
581fn scrim_style(theme: &Theme) -> iced_widget::container::Style {
582 iced_widget::container::Style {
583 background: Some(Background::Color(alpha_color(
584 Color {
585 a: 1.0,
586 ..theme.colors().scrim
587 },
588 tokens::component::bottom_sheet::SCRIM_OPACITY,
589 ))),
590 text_color: Some(theme.colors().surface.text),
591 ..iced_widget::container::Style::default()
592 }
593}
594
595fn side_scrim_style(theme: &Theme) -> iced_widget::container::Style {
596 iced_widget::container::Style {
597 background: Some(Background::Color(alpha_color(
598 Color {
599 a: 1.0,
600 ..theme.colors().scrim
601 },
602 tokens::component::side_sheet::SCRIM_OPACITY,
603 ))),
604 text_color: Some(theme.colors().surface.text),
605 ..iced_widget::container::Style::default()
606 }
607}
608
609#[cfg(test)]
610#[path = "../../../tests/widget/component/sheet.rs"]
611mod tests;