egui_theme_switcher/
lib.rs1use 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#[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
34pub fn theme_switcher_ui(ui: &mut Ui, dim: Dimension) -> Response {
36 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 let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
44
45 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 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 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 ui.painter().rect(
87 rect,
88 radius,
89 rect_visuals.bg_fill,
90 rect_visuals.bg_stroke,
91 egui::StrokeKind::Outside,
92 );
93
94 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 if response.clicked() {
119 response.mark_changed(); 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 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
141pub fn theme_switcher_with_dimension(dim: Dimension) -> impl Widget {
143 move |ui: &mut Ui| theme_switcher_ui(ui, dim)
144}
145
146pub fn theme_switcher() -> impl Widget {
148 move |ui: &mut Ui| theme_switcher_ui(ui, Dimension::default())
149}