Skip to main content

material_ui_rs/widget/component/
theme_picker.rs

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