egui_theme_switcher/
lib.rs

1use std::sync::RwLock;
2
3use egui::{
4    Align2, Color32, FontId, Response, Sense, ThemePreference, Ui, Widget, WidgetInfo, WidgetType,
5    lerp, pos2, vec2,
6};
7
8static TOGGLE_STORAGE: RwLock<ThemePreference> = RwLock::new(ThemePreference::System);
9
10/// Widget Size. Default to S
11#[non_exhaustive]
12#[derive(Default)]
13pub enum Dimension {
14    #[default]
15    S,
16    M,
17    L,
18    XL,
19    Custom(f32),
20}
21
22impl Dimension {
23    fn multiplier(&self) -> f32 {
24        match self {
25            Dimension::S => 1.,
26            Dimension::M => 3.,
27            Dimension::L => 5.,
28            Dimension::XL => 7.,
29            Dimension::Custom(mul) => *mul,
30        }
31    }
32}
33
34/// Paint the switcher to the [Ui] specifying the [Dimension]
35pub fn theme_switcher_ui(ui: &mut Ui, dim: Dimension) -> Response {
36    // Widget and font size
37    let desired_size =
38        ui.spacing().interact_size.y * vec2(5. * dim.multiplier(), 1. * dim.multiplier());
39    let mut font = FontId::default();
40    font.size *= dim.multiplier();
41
42    // Allocating space
43    let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
44
45    // Attach some meta-data to the response which can be used by screen readers
46    response.widget_info(|| {
47        WidgetInfo::selected(
48            WidgetType::RadioButton,
49            ui.is_enabled(),
50            true,
51            "theme switcher",
52        )
53    });
54
55    let theme = TOGGLE_STORAGE
56        .read()
57        .map(|v| *v)
58        .unwrap_or(ThemePreference::System);
59
60    let how_on = match theme {
61        ThemePreference::Dark => 1.,
62        ThemePreference::Light => 0.,
63        ThemePreference::System => 0.5,
64    };
65
66    ui.ctx().set_theme(theme);
67
68    // Paint!
69    if ui.is_rect_visible(rect) {
70        egui_material_icons::initialize(ui.ctx());
71
72        let rect_visuals = ui.style().interact_selectable(&response, false);
73        let circle_visuals = ui.style().interact_selectable(&response, true);
74
75        // All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
76        let rect = rect.expand(rect_visuals.expansion);
77        let radius = 0.5 * rect.height();
78        let circle_x = lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
79        let system_x = rect.width() / 2. + rect.left();
80        let system_position = pos2(system_x, rect.center().y);
81        let light_position = pos2(rect.left() + 1.1 * radius, rect.center().y - radius / 10.);
82        let dark_position = pos2(rect.right() - 1.1 * radius, rect.center().y - radius / 10.);
83        let circle_position = pos2(circle_x, rect.center().y);
84
85        // Paint background rect
86        ui.painter().rect(
87            rect,
88            radius,
89            rect_visuals.bg_fill,
90            rect_visuals.bg_stroke,
91            egui::StrokeKind::Outside,
92        );
93
94        // Paint icons
95        let light_rect = ui.painter().text(
96            light_position,
97            Align2::CENTER_CENTER,
98            egui_material_icons::icons::ICON_LIGHT_MODE,
99            font.clone(),
100            Color32::WHITE,
101        );
102        let system_rect = ui.painter().text(
103            system_position,
104            Align2::CENTER_CENTER,
105            egui_material_icons::icons::ICON_SETTINGS,
106            font.clone(),
107            Color32::WHITE,
108        );
109        let dark_rect = ui.painter().text(
110            dark_position,
111            Align2::CENTER_CENTER,
112            egui_material_icons::icons::ICON_DARK_MODE,
113            font,
114            Color32::WHITE,
115        );
116
117        // Check for clicks
118        if response.clicked() {
119            response.mark_changed(); // report back that the value changed
120            let interaction = response.interact_pointer_pos().unwrap();
121            if light_rect.contains(interaction) {
122                *TOGGLE_STORAGE.write().unwrap() = ThemePreference::Light;
123            } else if dark_rect.contains(interaction) {
124                *TOGGLE_STORAGE.write().unwrap() = ThemePreference::Dark;
125            } else if system_rect.contains(interaction) {
126                *TOGGLE_STORAGE.write().unwrap() = ThemePreference::System;
127            }
128        }
129
130        // Paint the circle, animating it from left to right with `how_on`:
131        ui.painter().circle(
132            circle_position,
133            1. * radius,
134            circle_visuals.bg_fill,
135            circle_visuals.fg_stroke,
136        );
137    }
138    response
139}
140
141/// Add the switcher to the [Ui] specifying a [Dimension]
142pub fn theme_switcher_with_dimension(dim: Dimension) -> impl Widget {
143    move |ui: &mut Ui| theme_switcher_ui(ui, dim)
144}
145
146/// Add the switcher to the [Ui] with [Dimension::S]
147pub fn theme_switcher() -> impl Widget {
148    move |ui: &mut Ui| theme_switcher_ui(ui, Dimension::default())
149}