1use iced_widget::button::{Status, Style};
4use iced_widget::canvas::{self, Canvas, Path, Stroke};
5use iced_widget::core::time::Instant;
6use iced_widget::core::{
7 Background, Color, Element, Length, Padding, Point, Rectangle, Size, alignment, border, mouse,
8};
9use iced_widget::core::{svg as core_svg, text as core_text};
10use iced_widget::graphics::geometry;
11use iced_widget::renderer::wgpu::primitive;
12use iced_widget::{Column, Container, Row, Space, Stack, text};
13
14use super::button::Button;
15use super::support::{AnimatedScalar, bool_value, duration_ms};
16use super::{navigation, viewport};
17use crate::animation::{ThemeRevealTransition, max_radius_from_origin};
18use crate::utils::{HOVERED_LAYER_OPACITY, PRESSED_LAYER_OPACITY, mix, shadow_from_level};
19use crate::{ColorQuartet, ColorScheme, Surface, SurfaceContainer, Theme, tokens};
20
21pub const FLOATING_MARGIN: f32 = 24.0;
22
23const PICKER_PANEL_PADDING: f32 = 12.0;
24const PICKER_PANEL_SPACING: f32 = 8.0;
25const PICKER_PANEL_SHAPE: f32 = tokens::shape::CORNER_EXTRA_LARGE;
26const PICKER_PANEL_ELEVATION_LEVEL: u8 = 3;
27const SWATCH_SIZE: f32 = 40.0;
28const SWATCH_TARGET_SIZE: f32 = 48.0;
29const SWATCH_SHAPE: f32 = tokens::shape::CORNER_FULL;
30const SELECTED_SWATCH_OUTLINE_WIDTH: f32 = 3.0;
31const SWATCH_OUTLINE_WIDTH: f32 = 1.0;
32const SWATCH_COLUMNS: usize = 4;
33const SWATCH_ROWS: usize = 2;
34const PALETTE_BUTTON_SIZE: f32 = 56.0;
35const PICKER_PANEL_TRANSITION_DURATION_MS: u16 = tokens::motion::DURATION_SHORT4_MS;
36const THEME_REVEAL_CENTER_ALPHA: f32 = 0.24;
37const THEME_REVEAL_START_FILL_ALPHA: f32 = 0.30;
38const THEME_REVEAL_EDGE_ALPHA: f32 = 0.54;
39const THEME_REVEAL_EDGE_LAYERS: usize = 20;
40const THEME_REVEAL_MIN_BLUR_WIDTH: f32 = 36.0;
41const THEME_REVEAL_MAX_BLUR_WIDTH: f32 = 180.0;
42const THEME_REVEAL_START_FILL_THRESHOLD: f32 = 0.45;
43const THEME_REVEAL_EDGE_FADE_THRESHOLD: f32 = 0.75;
44
45pub fn bottom_margin_for_navigation_layout(layout: navigation::AdaptiveLayout) -> f32 {
48 FLOATING_MARGIN
49 + match layout {
50 navigation::AdaptiveLayout::NavigationBar => {
51 tokens::component::navigation_bar::CONTAINER_HEIGHT
52 }
53 navigation::AdaptiveLayout::NavigationRail => 0.0,
54 }
55}
56
57#[derive(Debug, Clone, Copy)]
58pub struct State {
59 is_open: bool,
60 panel_reveal: AnimatedScalar,
61}
62
63impl State {
64 pub const fn new() -> Self {
65 Self {
66 is_open: false,
67 panel_reveal: AnimatedScalar::new(0.0),
68 }
69 }
70
71 pub const fn is_open(self) -> bool {
72 self.is_open
73 }
74
75 pub const fn is_animating(self) -> bool {
76 self.panel_reveal.is_animating()
77 }
78
79 pub fn advance(&mut self, now: Instant) -> bool {
80 self.panel_reveal.advance(now)
81 }
82
83 pub fn toggle(&mut self) {
84 self.toggle_at(Instant::now());
85 }
86
87 pub fn open(&mut self) {
88 self.open_at(Instant::now());
89 }
90
91 pub fn close(&mut self) {
92 self.close_at(Instant::now());
93 }
94
95 fn reveal(self) -> f32 {
96 self.panel_reveal.value.clamp(0.0, 1.0)
97 }
98
99 fn toggle_at(&mut self, now: Instant) {
100 self.set_open_at(!self.is_open, now);
101 }
102
103 fn open_at(&mut self, now: Instant) {
104 self.set_open_at(true, now);
105 }
106
107 fn close_at(&mut self, now: Instant) {
108 self.set_open_at(false, now);
109 }
110
111 fn set_open_at(&mut self, is_open: bool, now: Instant) {
112 self.is_open = is_open;
113 self.panel_reveal.set_target(
114 bool_value(is_open),
115 now,
116 duration_ms(PICKER_PANEL_TRANSITION_DURATION_MS),
117 tokens::motion::EASING_EMPHASIZED_DECELERATE,
118 );
119 }
120}
121
122impl Default for State {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128impl PartialEq for State {
129 fn eq(&self, other: &Self) -> bool {
130 self.is_open == other.is_open
131 }
132}
133
134impl Eq for State {}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
137pub enum ThemeAction {
138 TogglePicker,
139 SelectColor(MaterialColor),
140 SetDarkMode { dark_mode: bool, origin: Point },
141}
142
143#[derive(Debug, Clone)]
144pub struct ThemeController {
145 picker: State,
146 selected: MaterialColor,
147 dark_mode: bool,
148 visible_scheme: ColorScheme,
149 transition: Option<ThemeRevealTransition>,
150}
151
152impl ThemeController {
153 pub fn new(selected: MaterialColor, dark_mode: bool) -> Self {
154 Self {
155 picker: State::new(),
156 selected,
157 dark_mode,
158 visible_scheme: selected.color_scheme(dark_mode),
159 transition: None,
160 }
161 }
162
163 pub fn theme(&self, name: impl Into<std::borrow::Cow<'static, str>>) -> Theme {
164 Theme::new(name, self.visible_scheme)
165 }
166
167 pub const fn picker_state(&self) -> &State {
168 &self.picker
169 }
170
171 pub const fn is_picker_open(&self) -> bool {
172 self.picker.is_open()
173 }
174
175 pub const fn selected_color(&self) -> MaterialColor {
176 self.selected
177 }
178
179 pub const fn dark_mode(&self) -> bool {
180 self.dark_mode
181 }
182
183 pub const fn visible_scheme(&self) -> ColorScheme {
184 self.visible_scheme
185 }
186
187 pub const fn transition(&self) -> Option<ThemeRevealTransition> {
188 self.transition
189 }
190
191 pub const fn is_animating(&self) -> bool {
192 self.transition.is_some() || self.picker.is_animating()
193 }
194
195 pub fn update(
196 &mut self,
197 action: ThemeAction,
198 viewport: Size,
199 bottom_margin: f32,
200 now: Instant,
201 ) {
202 match action {
203 ThemeAction::TogglePicker => self.picker.toggle_at(now),
204 ThemeAction::SelectColor(color) => {
205 let origin = swatch_center(viewport, bottom_margin, color);
206
207 self.selected = color;
208 self.picker.close_at(now);
209 self.animate_to(color.color_scheme(self.dark_mode), origin, now);
210 }
211 ThemeAction::SetDarkMode { dark_mode, origin } => {
212 self.dark_mode = dark_mode;
213 self.animate_to(self.selected.color_scheme(dark_mode), origin, now);
214 }
215 }
216 }
217
218 pub fn advance(&mut self, now: Instant) -> bool {
219 let picker_advanced = self.picker.advance(now);
220 let Some(transition) = self.transition else {
221 return picker_advanced;
222 };
223
224 self.visible_scheme = transition.value_at(now);
225
226 if transition.is_finished_at(now) {
227 self.visible_scheme = transition.target();
228 self.transition = None;
229 }
230
231 true
232 }
233
234 pub fn dark_mode_switch<'a, Message, Renderer>(
235 &self,
236 label: impl text::IntoFragment<'a>,
237 on_action: impl Fn(ThemeAction) -> Message + 'a,
238 ) -> Element<'a, Message, Theme, Renderer>
239 where
240 Message: 'a,
241 Renderer: iced_widget::core::Renderer + core_text::Renderer + core_svg::Renderer + 'a,
242 {
243 super::toggler::standard_with_origin(self.dark_mode, label, move |dark_mode, origin| {
244 on_action(ThemeAction::SetDarkMode { dark_mode, origin })
245 })
246 }
247
248 pub fn controls_over<'a, Message, Renderer>(
249 &self,
250 content: impl Into<Element<'a, Message, Theme, Renderer>>,
251 bottom_margin: f32,
252 on_action: impl Fn(ThemeAction) -> Message + 'a,
253 ) -> Element<'a, Message, Theme, Renderer>
254 where
255 Message: Clone + 'a,
256 Renderer: iced_widget::core::Renderer
257 + core_text::Renderer
258 + geometry::Renderer
259 + primitive::Renderer
260 + 'a,
261 iced_widget::core::Font: Into<Renderer::Font>,
262 {
263 floating_over(
264 content,
265 &self.picker,
266 self.selected,
267 bottom_margin,
268 on_action(ThemeAction::TogglePicker),
269 move |color| on_action(ThemeAction::SelectColor(color)),
270 )
271 }
272
273 pub fn reveal_over<'a, Message, Renderer>(
274 &self,
275 content: impl Into<Element<'a, Message, Theme, Renderer>>,
276 now: Instant,
277 ) -> Element<'a, Message, Theme, Renderer>
278 where
279 Message: 'a,
280 Renderer: iced_widget::core::Renderer + geometry::Renderer + 'a,
281 {
282 reveal_over(content, self.transition, now)
283 }
284
285 fn animate_to(&mut self, target: ColorScheme, origin: Point, now: Instant) {
286 if let Some(transition) = self.transition {
287 self.visible_scheme = transition.value_at(now);
288 }
289
290 self.transition = (self.visible_scheme != target).then(|| {
291 ThemeRevealTransition::material_theme(self.visible_scheme, target, origin, now)
292 });
293 }
294}
295
296impl Default for ThemeController {
297 fn default() -> Self {
298 Self::new(MaterialColor::Purple, true)
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum MaterialColor {
304 Purple,
305 Blue,
306 Teal,
307 Green,
308 Yellow,
309 Orange,
310 Red,
311 Pink,
312}
313
314impl MaterialColor {
315 pub const ALL: [Self; 8] = [
316 Self::Purple,
317 Self::Blue,
318 Self::Teal,
319 Self::Green,
320 Self::Yellow,
321 Self::Orange,
322 Self::Red,
323 Self::Pink,
324 ];
325
326 pub const fn label(self) -> &'static str {
327 match self {
328 Self::Purple => "Purple",
329 Self::Blue => "Blue",
330 Self::Teal => "Teal",
331 Self::Green => "Green",
332 Self::Yellow => "Yellow",
333 Self::Orange => "Orange",
334 Self::Red => "Red",
335 Self::Pink => "Pink",
336 }
337 }
338
339 pub fn color_scheme(self, dark: bool) -> ColorScheme {
340 let mut scheme = if dark {
341 Theme::Dark.colors()
342 } else {
343 Theme::Light.colors()
344 };
345
346 let primary = self.primary(dark);
347
348 scheme.primary = primary;
349 scheme.secondary = tint_quartet(scheme.secondary, primary, 0.55);
350 scheme.tertiary = tint_quartet(scheme.tertiary, primary, 0.32);
351 scheme.surface = tint_surface(scheme.surface, primary, dark);
352 scheme.inverse.inverse_primary = self.primary(!dark).color;
353 scheme.inverse.inverse_surface = mix(
354 scheme.inverse.inverse_surface,
355 self.primary(!dark).container,
356 0.08,
357 );
358 scheme.outline.color = mix(scheme.outline.color, primary.color, 0.08);
359 scheme.outline.variant = mix(scheme.outline.variant, primary.container, 0.10);
360 scheme
361 }
362
363 pub const fn swatch(self) -> Color {
364 self.primary(false).color
365 }
366
367 const fn index(self) -> usize {
368 match self {
369 Self::Purple => 0,
370 Self::Blue => 1,
371 Self::Teal => 2,
372 Self::Green => 3,
373 Self::Yellow => 4,
374 Self::Orange => 5,
375 Self::Red => 6,
376 Self::Pink => 7,
377 }
378 }
379
380 const fn primary(self, dark: bool) -> ColorQuartet {
381 match (self, dark) {
382 (Self::Purple, false) => ColorQuartet {
383 color: rgb(0x67, 0x50, 0xa4),
384 text: rgb(0xff, 0xff, 0xff),
385 container: rgb(0xea, 0xdd, 0xff),
386 container_text: rgb(0x21, 0x00, 0x5d),
387 },
388 (Self::Purple, true) => ColorQuartet {
389 color: rgb(0xd0, 0xbc, 0xff),
390 text: rgb(0x38, 0x1e, 0x72),
391 container: rgb(0x4f, 0x37, 0x8b),
392 container_text: rgb(0xea, 0xdd, 0xff),
393 },
394 (Self::Blue, false) => ColorQuartet {
395 color: rgb(0x00, 0x61, 0xa4),
396 text: rgb(0xff, 0xff, 0xff),
397 container: rgb(0xd1, 0xe4, 0xff),
398 container_text: rgb(0x00, 0x1d, 0x36),
399 },
400 (Self::Blue, true) => ColorQuartet {
401 color: rgb(0x9e, 0xca, 0xff),
402 text: rgb(0x00, 0x32, 0x58),
403 container: rgb(0x00, 0x49, 0x7d),
404 container_text: rgb(0xd1, 0xe4, 0xff),
405 },
406 (Self::Teal, false) => ColorQuartet {
407 color: rgb(0x00, 0x6a, 0x60),
408 text: rgb(0xff, 0xff, 0xff),
409 container: rgb(0x74, 0xf8, 0xe6),
410 container_text: rgb(0x00, 0x20, 0x1c),
411 },
412 (Self::Teal, true) => ColorQuartet {
413 color: rgb(0x53, 0xdb, 0xc9),
414 text: rgb(0x00, 0x37, 0x31),
415 container: rgb(0x00, 0x50, 0x48),
416 container_text: rgb(0x74, 0xf8, 0xe6),
417 },
418 (Self::Green, false) => ColorQuartet {
419 color: rgb(0x00, 0x6d, 0x3b),
420 text: rgb(0xff, 0xff, 0xff),
421 container: rgb(0x8f, 0xf7, 0xb3),
422 container_text: rgb(0x00, 0x21, 0x0d),
423 },
424 (Self::Green, true) => ColorQuartet {
425 color: rgb(0x73, 0xdb, 0x99),
426 text: rgb(0x00, 0x39, 0x1c),
427 container: rgb(0x00, 0x52, 0x2b),
428 container_text: rgb(0x8f, 0xf7, 0xb3),
429 },
430 (Self::Yellow, false) => ColorQuartet {
431 color: rgb(0x6d, 0x5e, 0x00),
432 text: rgb(0xff, 0xff, 0xff),
433 container: rgb(0xfb, 0xe5, 0x60),
434 container_text: rgb(0x21, 0x1c, 0x00),
435 },
436 (Self::Yellow, true) => ColorQuartet {
437 color: rgb(0xde, 0xc8, 0x48),
438 text: rgb(0x39, 0x31, 0x00),
439 container: rgb(0x52, 0x46, 0x00),
440 container_text: rgb(0xfb, 0xe5, 0x60),
441 },
442 (Self::Orange, false) => ColorQuartet {
443 color: rgb(0x8b, 0x50, 0x00),
444 text: rgb(0xff, 0xff, 0xff),
445 container: rgb(0xff, 0xdc, 0xbe),
446 container_text: rgb(0x2d, 0x16, 0x00),
447 },
448 (Self::Orange, true) => ColorQuartet {
449 color: rgb(0xff, 0xb8, 0x70),
450 text: rgb(0x4a, 0x28, 0x00),
451 container: rgb(0x69, 0x3c, 0x00),
452 container_text: rgb(0xff, 0xdc, 0xbe),
453 },
454 (Self::Red, false) => ColorQuartet {
455 color: rgb(0xba, 0x1a, 0x1a),
456 text: rgb(0xff, 0xff, 0xff),
457 container: rgb(0xff, 0xda, 0xd6),
458 container_text: rgb(0x41, 0x00, 0x02),
459 },
460 (Self::Red, true) => ColorQuartet {
461 color: rgb(0xff, 0xb4, 0xab),
462 text: rgb(0x69, 0x00, 0x05),
463 container: rgb(0x93, 0x00, 0x0a),
464 container_text: rgb(0xff, 0xda, 0xd6),
465 },
466 (Self::Pink, false) => ColorQuartet {
467 color: rgb(0x98, 0x40, 0x61),
468 text: rgb(0xff, 0xff, 0xff),
469 container: rgb(0xff, 0xd9, 0xe3),
470 container_text: rgb(0x3e, 0x00, 0x1d),
471 },
472 (Self::Pink, true) => ColorQuartet {
473 color: rgb(0xff, 0xb1, 0xc8),
474 text: rgb(0x5e, 0x11, 0x32),
475 container: rgb(0x7b, 0x29, 0x49),
476 container_text: rgb(0xff, 0xd9, 0xe3),
477 },
478 }
479 }
480}
481
482pub fn palette_center(viewport: Size, bottom_margin: f32) -> Point {
483 let right = viewport.width - FLOATING_MARGIN;
484 let bottom = viewport.height - bottom_margin;
485
486 Point::new(
487 right - PALETTE_BUTTON_SIZE / 2.0,
488 bottom - PALETTE_BUTTON_SIZE / 2.0,
489 )
490}
491
492pub fn swatch_center(viewport: Size, bottom_margin: f32, color: MaterialColor) -> Point {
493 let index = color.index();
494 let column = index % SWATCH_COLUMNS;
495 let row = index / SWATCH_COLUMNS;
496 let panel_right = viewport.width - FLOATING_MARGIN;
497 let panel_bottom = viewport.height - bottom_margin - PALETTE_BUTTON_SIZE - PICKER_PANEL_SPACING;
498 let panel_left = panel_right - picker_panel_width();
499 let panel_top = panel_bottom - picker_panel_height();
500
501 Point::new(
502 panel_left
503 + PICKER_PANEL_PADDING
504 + column as f32 * (SWATCH_TARGET_SIZE + PICKER_PANEL_SPACING)
505 + SWATCH_TARGET_SIZE / 2.0,
506 panel_top
507 + PICKER_PANEL_PADDING
508 + row as f32 * (SWATCH_TARGET_SIZE + PICKER_PANEL_SPACING)
509 + SWATCH_TARGET_SIZE / 2.0,
510 )
511}
512
513pub fn floating_over<'a, Message, Renderer>(
514 content: impl Into<Element<'a, Message, Theme, Renderer>>,
515 state: &State,
516 selected: MaterialColor,
517 bottom_margin: f32,
518 on_toggle: Message,
519 on_select: impl Fn(MaterialColor) -> Message + 'a,
520) -> Element<'a, Message, Theme, Renderer>
521where
522 Message: Clone + 'a,
523 Renderer: iced_widget::core::Renderer
524 + core_text::Renderer
525 + geometry::Renderer
526 + primitive::Renderer
527 + 'a,
528 iced_widget::core::Font: Into<Renderer::Font>,
529{
530 Stack::with_children([
531 content.into(),
532 floating_layer(state, selected, bottom_margin, on_toggle, on_select),
533 ])
534 .width(Length::Fill)
535 .height(Length::Fill)
536 .into()
537}
538
539pub fn reveal_over<'a, Message, Renderer>(
540 content: impl Into<Element<'a, Message, Theme, Renderer>>,
541 transition: Option<ThemeRevealTransition>,
542 now: Instant,
543) -> Element<'a, Message, Theme, Renderer>
544where
545 Message: 'a,
546 Renderer: iced_widget::core::Renderer + geometry::Renderer + 'a,
547{
548 let content = content.into();
549 let overlay = if let Some(transition) = transition {
550 reveal_overlay(transition, now).into()
551 } else {
552 Space::new().width(Length::Fill).height(Length::Fill).into()
553 };
554
555 Stack::with_children([content, overlay])
556 .width(Length::Fill)
557 .height(Length::Fill)
558 .into()
559}
560
561pub fn reveal_overlay<'a, Message, Renderer>(
562 transition: ThemeRevealTransition,
563 now: Instant,
564) -> Canvas<ThemeRevealOverlay, Message, Theme, Renderer>
565where
566 Renderer: geometry::Renderer + 'a,
567{
568 Canvas::new(ThemeRevealOverlay {
569 origin: transition.origin(),
570 target: transition.target(),
571 progress: transition.eased_progress_at(now),
572 })
573 .width(Length::Fill)
574 .height(Length::Fill)
575}
576
577pub fn floating_layer<'a, Message, Renderer>(
578 state: &State,
579 selected: MaterialColor,
580 bottom_margin: f32,
581 on_toggle: Message,
582 on_select: impl Fn(MaterialColor) -> Message + 'a,
583) -> Element<'a, Message, Theme, Renderer>
584where
585 Message: Clone + 'a,
586 Renderer: iced_widget::core::Renderer
587 + core_text::Renderer
588 + geometry::Renderer
589 + primitive::Renderer
590 + 'a,
591 iced_widget::core::Font: Into<Renderer::Font>,
592{
593 Stack::with_children([
594 floating_panel_layer(state, selected, bottom_margin, on_select),
595 floating_palette_layer(bottom_margin, on_toggle),
596 ])
597 .width(Length::Fill)
598 .height(Length::Fill)
599 .into()
600}
601
602fn floating_panel_layer<'a, Message, Renderer>(
603 state: &State,
604 selected: MaterialColor,
605 bottom_margin: f32,
606 on_select: impl Fn(MaterialColor) -> Message + 'a,
607) -> Element<'a, Message, Theme, Renderer>
608where
609 Message: Clone + 'a,
610 Renderer: iced_widget::core::Renderer + geometry::Renderer + primitive::Renderer + 'a,
611{
612 Container::new(picker_panel_slot(selected, on_select, state.reveal()))
613 .width(Length::Fill)
614 .height(Length::Fill)
615 .padding(floating_padding(
616 FLOATING_MARGIN,
617 bottom_margin + PALETTE_BUTTON_SIZE,
618 ))
619 .align_x(alignment::Horizontal::Right)
620 .align_y(alignment::Vertical::Bottom)
621 .into()
622}
623
624fn floating_palette_layer<'a, Message, Renderer>(
625 bottom_margin: f32,
626 on_toggle: Message,
627) -> Element<'a, Message, Theme, Renderer>
628where
629 Message: Clone + 'a,
630 Renderer: iced_widget::core::Renderer
631 + core_text::Renderer
632 + geometry::Renderer
633 + primitive::Renderer
634 + 'a,
635 iced_widget::core::Font: Into<Renderer::Font>,
636{
637 Container::new(palette_button(on_toggle))
638 .width(Length::Fill)
639 .height(Length::Fill)
640 .padding(floating_padding(FLOATING_MARGIN, bottom_margin))
641 .align_x(alignment::Horizontal::Right)
642 .align_y(alignment::Vertical::Bottom)
643 .into()
644}
645
646fn floating_padding(right: f32, bottom: f32) -> Padding {
647 Padding {
648 top: 0.0,
649 right,
650 bottom,
651 left: 0.0,
652 }
653}
654
655fn picker_panel_slot<'a, Message, Renderer>(
656 selected: MaterialColor,
657 on_select: impl Fn(MaterialColor) -> Message + 'a,
658 reveal: f32,
659) -> Element<'a, Message, Theme, Renderer>
660where
661 Message: Clone + 'a,
662 Renderer: iced_widget::core::Renderer + geometry::Renderer + primitive::Renderer + 'a,
663{
664 let visible_height = picker_panel_reveal_height(reveal);
665 let layout_height = picker_panel_slot_height();
666 let content = Column::new()
667 .push(picker_panel(selected, on_select))
668 .push(Space::new().height(Length::Fixed(PICKER_PANEL_SPACING)));
669
670 viewport::Viewport::fixed_height(content, visible_height, layout_height)
671 .align_y(alignment::Vertical::Bottom)
672 .width(Length::Fixed(picker_panel_width()))
673 .into()
674}
675
676fn picker_panel<'a, Message, Renderer>(
677 selected: MaterialColor,
678 on_select: impl Fn(MaterialColor) -> Message + 'a,
679) -> Container<'a, Message, Theme, Renderer>
680where
681 Message: Clone + 'a,
682 Renderer: iced_widget::core::Renderer + geometry::Renderer + primitive::Renderer + 'a,
683{
684 let mut rows = Column::new().spacing(PICKER_PANEL_SPACING);
685
686 for colors in MaterialColor::ALL.chunks(SWATCH_COLUMNS) {
687 let mut row = Row::new().spacing(PICKER_PANEL_SPACING);
688
689 for color in colors {
690 row = row.push(swatch_button(*color, *color == selected, on_select(*color)));
691 }
692
693 rows = rows.push(row);
694 }
695
696 Container::new(rows)
697 .padding(PICKER_PANEL_PADDING)
698 .style(picker_panel_style)
699}
700
701fn palette_button<'a, Message, Renderer>(on_press: Message) -> Button<'a, Message, Renderer>
702where
703 Message: Clone + 'a,
704 Renderer: iced_widget::core::Renderer + core_text::Renderer + geometry::Renderer + 'a,
705 iced_widget::core::Font: Into<Renderer::Font>,
706{
707 super::button::surface_fab("palette").on_press(on_press)
708}
709
710fn swatch_button<'a, Message, Renderer>(
711 color: MaterialColor,
712 selected: bool,
713 on_press: Message,
714) -> Button<'a, Message, Renderer>
715where
716 Message: Clone + 'a,
717 Renderer: iced_widget::core::Renderer + geometry::Renderer + 'a,
718{
719 Button::new(
720 Container::new(Space::new())
721 .width(Length::Fixed(SWATCH_SIZE))
722 .height(Length::Fixed(SWATCH_SIZE)),
723 )
724 .width(Length::Fixed(SWATCH_TARGET_SIZE))
725 .height(Length::Fixed(SWATCH_TARGET_SIZE))
726 .padding(Padding::from([
727 (SWATCH_TARGET_SIZE - SWATCH_SIZE) / 2.0,
728 (SWATCH_TARGET_SIZE - SWATCH_SIZE) / 2.0,
729 ]))
730 .on_press(on_press)
731 .style(move |theme, status| swatch_style(theme, status, color, selected))
732}
733
734fn picker_panel_style(theme: &Theme) -> iced_widget::container::Style {
735 let colors = theme.colors();
736
737 iced_widget::container::Style {
738 background: Some(Background::Color(colors.surface.container.high)),
739 text_color: Some(colors.surface.text),
740 border: border::rounded(PICKER_PANEL_SHAPE),
741 shadow: shadow_from_level(PICKER_PANEL_ELEVATION_LEVEL, colors.shadow),
742 snap: cfg!(feature = "crisp"),
743 }
744}
745
746fn swatch_style(theme: &Theme, status: Status, color: MaterialColor, selected: bool) -> Style {
747 let colors = theme.colors();
748 let base = color.swatch();
749 let background = match status {
750 Status::Active | Status::Disabled => base,
751 Status::Hovered => mix(base, colors.surface.text, HOVERED_LAYER_OPACITY),
752 Status::Pressed => mix(base, colors.surface.text, PRESSED_LAYER_OPACITY),
753 };
754
755 let outline = if selected {
756 colors.surface.text
757 } else {
758 colors.outline.variant
759 };
760
761 Style {
762 background: Some(Background::Color(background)),
763 text_color: colors.surface.text,
764 border: iced_widget::core::Border {
765 color: outline,
766 width: if selected {
767 SELECTED_SWATCH_OUTLINE_WIDTH
768 } else {
769 SWATCH_OUTLINE_WIDTH
770 },
771 radius: SWATCH_SHAPE.into(),
772 },
773 shadow: shadow_from_level(0, Color::TRANSPARENT),
774 snap: cfg!(feature = "crisp"),
775 }
776}
777
778#[derive(Debug, Clone, Copy)]
779pub struct ThemeRevealOverlay {
780 origin: Point,
781 target: ColorScheme,
782 progress: f32,
783}
784
785impl<Message, Renderer> canvas::Program<Message, Theme, Renderer> for ThemeRevealOverlay
786where
787 Renderer: geometry::Renderer,
788{
789 type State = ();
790
791 fn draw(
792 &self,
793 _state: &Self::State,
794 renderer: &Renderer,
795 _theme: &Theme,
796 bounds: Rectangle,
797 _cursor: mouse::Cursor,
798 ) -> Vec<canvas::Geometry<Renderer>> {
799 let mut frame = canvas::Frame::new(renderer, bounds.size());
800 let progress = self.progress.clamp(0.0, 1.0);
801 let origin = Point::new(self.origin.x - bounds.x, self.origin.y - bounds.y);
802 let max_radius = max_radius_from_origin(origin, bounds.size());
803 let radius = max_radius * progress;
804
805 draw_start_fill(&mut frame, bounds.size(), self.target, progress);
806
807 if radius <= 0.0 {
808 return vec![frame.into_geometry()];
809 }
810
811 draw_reveal_center(&mut frame, origin, radius, self.target, progress);
812 draw_reveal_blur_halo(
813 &mut frame,
814 origin,
815 radius,
816 max_radius,
817 self.target,
818 progress,
819 );
820
821 vec![frame.into_geometry()]
822 }
823}
824
825fn draw_start_fill<Renderer>(
826 frame: &mut canvas::Frame<Renderer>,
827 size: Size,
828 target: ColorScheme,
829 progress: f32,
830) where
831 Renderer: geometry::Renderer,
832{
833 let alpha = reveal_start_fill_alpha(progress);
834
835 if alpha <= 0.0 {
836 return;
837 }
838
839 let mut color = mix(target.surface.color, target.primary.container, 0.16);
840 color.a *= alpha;
841
842 frame.fill(&Path::rectangle(Point::ORIGIN, size), color);
843}
844
845fn draw_reveal_center<Renderer>(
846 frame: &mut canvas::Frame<Renderer>,
847 origin: Point,
848 radius: f32,
849 target: ColorScheme,
850 progress: f32,
851) where
852 Renderer: geometry::Renderer,
853{
854 let mut surface = target.surface.color;
855 surface.a *= THEME_REVEAL_CENTER_ALPHA
856 * reveal_gradient_end_alpha(progress)
857 * (1.0 - reveal_blur_ratio(progress) * 0.35);
858
859 if surface.a > 0.0 {
860 frame.fill(&Path::circle(origin, radius), surface);
861 }
862}
863
864fn draw_reveal_blur_halo<Renderer>(
865 frame: &mut canvas::Frame<Renderer>,
866 origin: Point,
867 radius: f32,
868 max_radius: f32,
869 target: ColorScheme,
870 progress: f32,
871) where
872 Renderer: geometry::Renderer,
873{
874 let edge_alpha = reveal_gradient_end_alpha(progress);
875 let blur_ratio = reveal_blur_ratio(progress);
876 let blur_width = reveal_blur_width(max_radius, progress);
877
878 if edge_alpha <= 0.0 || blur_width <= 0.0 {
879 return;
880 }
881
882 let layer_width = (blur_width / THEME_REVEAL_EDGE_LAYERS as f32).max(1.0);
883 let base = mix(target.primary.container, target.surface.color, 0.28);
884
885 for layer in 0..THEME_REVEAL_EDGE_LAYERS {
886 let t = layer as f32 / (THEME_REVEAL_EDGE_LAYERS - 1) as f32;
887 let offset = (t - 0.5) * blur_width;
888 let ring_radius = (radius + offset).max(layer_width / 2.0);
889 let bell = 1.0 - (2.0 * t - 1.0).abs().powi(2);
890 let mut color = mix(base, target.surface.color, t * 0.55);
891 color.a *= THEME_REVEAL_EDGE_ALPHA * edge_alpha * (0.30 + blur_ratio * 0.70) * bell
892 / (THEME_REVEAL_EDGE_LAYERS as f32).sqrt();
893
894 if color.a <= 0.0 {
895 continue;
896 }
897
898 frame.stroke(
899 &Path::circle(origin, ring_radius),
900 Stroke::default()
901 .with_width(layer_width * (1.0 + blur_ratio * 1.2))
902 .with_color(color),
903 );
904 }
905}
906
907fn percent_past_threshold(value: f32, threshold: f32) -> f32 {
908 let threshold = threshold.clamp(0.0, 0.999_999);
909
910 ((value.clamp(0.0, 1.0) - threshold).max(0.0) / (1.0 - threshold)).clamp(0.0, 1.0)
911}
912
913fn reveal_gradient_end_alpha(progress: f32) -> f32 {
914 1.0 - percent_past_threshold(progress, THEME_REVEAL_EDGE_FADE_THRESHOLD)
915}
916
917fn reveal_start_fill_alpha(progress: f32) -> f32 {
918 THEME_REVEAL_START_FILL_ALPHA
919 * (1.0 - percent_past_threshold(progress, THEME_REVEAL_START_FILL_THRESHOLD))
920}
921
922fn reveal_blur_ratio(progress: f32) -> f32 {
923 let progress = progress.clamp(0.0, 1.0);
924
925 (1.0 - (progress * 2.0 - 1.0).abs()).clamp(0.0, 1.0).sqrt()
926}
927
928fn reveal_blur_width(max_radius: f32, progress: f32) -> f32 {
929 let max_width = THEME_REVEAL_MAX_BLUR_WIDTH.min(max_radius * 0.12);
930
931 lerp(
932 THEME_REVEAL_MIN_BLUR_WIDTH.min(max_width),
933 max_width,
934 reveal_blur_ratio(progress),
935 )
936}
937
938fn lerp(from: f32, to: f32, progress: f32) -> f32 {
939 from + (to - from) * progress.clamp(0.0, 1.0)
940}
941
942fn picker_panel_width() -> f32 {
943 PICKER_PANEL_PADDING * 2.0
944 + SWATCH_COLUMNS as f32 * SWATCH_TARGET_SIZE
945 + (SWATCH_COLUMNS - 1) as f32 * PICKER_PANEL_SPACING
946}
947
948fn picker_panel_height() -> f32 {
949 PICKER_PANEL_PADDING * 2.0
950 + SWATCH_ROWS as f32 * SWATCH_TARGET_SIZE
951 + (SWATCH_ROWS - 1) as f32 * PICKER_PANEL_SPACING
952}
953
954fn picker_panel_slot_height() -> f32 {
955 picker_panel_height() + PICKER_PANEL_SPACING
956}
957
958fn picker_panel_reveal_height(progress: f32) -> f32 {
959 picker_panel_slot_height() * progress.clamp(0.0, 1.0)
960}
961
962fn tint_quartet(base: ColorQuartet, primary: ColorQuartet, amount: f32) -> ColorQuartet {
963 ColorQuartet {
964 color: mix(base.color, primary.color, amount),
965 text: base.text,
966 container: mix(base.container, primary.container, amount),
967 container_text: base.container_text,
968 }
969}
970
971fn tint_surface(base: Surface, primary: ColorQuartet, dark: bool) -> Surface {
972 let anchor = primary.container;
973 let [surface, lowest, low, container, high, highest] = if dark {
974 [0.08, 0.05, 0.09, 0.12, 0.15, 0.18]
975 } else {
976 [0.20, 0.10, 0.18, 0.24, 0.30, 0.36]
977 };
978
979 Surface {
980 color: mix(base.color, anchor, surface),
981 text: base.text,
982 text_variant: base.text_variant,
983 container: SurfaceContainer {
984 lowest: mix(base.container.lowest, anchor, lowest),
985 low: mix(base.container.low, anchor, low),
986 base: mix(base.container.base, anchor, container),
987 high: mix(base.container.high, anchor, high),
988 highest: mix(base.container.highest, anchor, highest),
989 },
990 }
991}
992
993const fn rgb(r: u8, g: u8, b: u8) -> Color {
994 Color::from_rgb8(r, g, b)
995}
996
997#[cfg(test)]
998#[path = "../../../tests/widget/component/theme_picker.rs"]
999mod tests;