egui_theme_switcher/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use std::sync::RwLock;

use egui::{
    lerp, pos2, vec2, Align2, Color32, FontId, Response, Sense, ThemePreference, Ui, Widget,
    WidgetInfo, WidgetType,
};

static TOGGLE_STORAGE: RwLock<ThemePreference> = RwLock::new(ThemePreference::System);

/// Widget Size. Default to S
#[non_exhaustive]
#[derive(Default)]
pub enum Dimension {
    #[default]
    S,
    M,
    L,
    XL,
    Custom(f32),
}

impl Dimension {
    fn multiplier(&self) -> f32 {
        match self {
            Dimension::S => 1.,
            Dimension::M => 3.,
            Dimension::L => 5.,
            Dimension::XL => 7.,
            Dimension::Custom(mul) => *mul,
        }
    }
}

/// Paint the switcher to the [Ui] specifying the [Dimension]
pub fn theme_switcher_ui(ui: &mut Ui, dim: Dimension) -> Response {
    // Widget and font size
    let desired_size =
        ui.spacing().interact_size.y * vec2(5. * dim.multiplier(), 1. * dim.multiplier());
    let mut font = FontId::default();
    font.size *= dim.multiplier();

    // Allocating space
    let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());

    // Attach some meta-data to the response which can be used by screen readers
    response.widget_info(|| {
        WidgetInfo::selected(
            WidgetType::RadioButton,
            ui.is_enabled(),
            true,
            "theme switcher",
        )
    });

    let theme = TOGGLE_STORAGE
        .read()
        .map(|v| *v)
        .unwrap_or(ThemePreference::System);

    let how_on = match theme {
        ThemePreference::Dark => 1.,
        ThemePreference::Light => 0.,
        ThemePreference::System => 0.5,
    };

    ui.ctx().set_theme(theme);

    // Paint!
    if ui.is_rect_visible(rect) {
        egui_material_icons::initialize(ui.ctx());

        let rect_visuals = ui.style().interact_selectable(&response, false);
        let circle_visuals = ui.style().interact_selectable(&response, true);

        // All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
        let rect = rect.expand(rect_visuals.expansion);
        let radius = 0.5 * rect.height();
        let circle_x = lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
        let system_x = rect.width() / 2. + rect.left();
        let system_position = pos2(system_x, rect.center().y);
        let light_position = pos2(rect.left() + 1.1 * radius, rect.center().y - radius / 10.);
        let dark_position = pos2(rect.right() - 1.1 * radius, rect.center().y - radius / 10.);
        let circle_position = pos2(circle_x, rect.center().y);

        // Paint background rect
        ui.painter()
            .rect(rect, radius, rect_visuals.bg_fill, rect_visuals.bg_stroke);

        // Paint icons
        let light_rect = ui.painter().text(
            light_position,
            Align2::CENTER_CENTER,
            egui_material_icons::icons::ICON_LIGHT_MODE,
            font.clone(),
            Color32::WHITE,
        );
        let system_rect = ui.painter().text(
            system_position,
            Align2::CENTER_CENTER,
            egui_material_icons::icons::ICON_SETTINGS,
            font.clone(),
            Color32::WHITE,
        );
        let dark_rect = ui.painter().text(
            dark_position,
            Align2::CENTER_CENTER,
            egui_material_icons::icons::ICON_DARK_MODE,
            font,
            Color32::WHITE,
        );

        // Check for clicks
        if response.clicked() {
            response.mark_changed(); // report back that the value changed
            let interaction = response.interact_pointer_pos().unwrap();
            if light_rect.contains(interaction) {
                *TOGGLE_STORAGE.write().unwrap() = ThemePreference::Light;
            } else if dark_rect.contains(interaction) {
                *TOGGLE_STORAGE.write().unwrap() = ThemePreference::Dark;
            } else if system_rect.contains(interaction) {
                *TOGGLE_STORAGE.write().unwrap() = ThemePreference::System;
            }
        }

        // Paint the circle, animating it from left to right with `how_on`:
        ui.painter().circle(
            circle_position,
            1. * radius,
            circle_visuals.bg_fill,
            circle_visuals.fg_stroke,
        );
    }
    response
}

/// Add the switcher to the [Ui] specifying a [Dimension]
pub fn theme_switcher_with_dimension(dim: Dimension) -> impl Widget {
    move |ui: &mut Ui| theme_switcher_ui(ui, dim)
}

/// Add the switcher to the [Ui] with [Dimension::S]
pub fn theme_switcher() -> impl Widget {
    move |ui: &mut Ui| theme_switcher_ui(ui, Dimension::default())
}