1use crate::{color::ThemeColorVariant, theme::Theme};
2use adw::prelude::{AdwDialogExt, PreferencesRowExt};
3use gtk::{
4 gdk,
5 glib::{self, clone},
6 prelude::{BoxExt, ButtonExt, Cast, ObjectExt, WidgetExt},
7};
8use std::{cell::RefCell, fmt::Display, rc::Rc};
9
10pub mod color;
11pub mod named_colors;
12pub mod theme;
13
14#[derive(Debug, Clone)]
15pub struct ThemeEngine {
16 display: gdk::Display,
17 current_theme_provider: Rc<RefCell<Option<gtk::CssProvider>>>,
18 current_theme: Rc<RefCell<Theme>>,
19}
20
21#[derive(Debug, Clone, Copy)]
22pub enum ThemeEngineErr {
23 NoGdkDisplay,
24}
25
26impl Display for ThemeEngineErr {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.write_str(match self {
29 Self::NoGdkDisplay => "Failed to get default GDK Display",
30 })
31 }
32}
33
34pub type ThemeEngineResult<T> = Result<T, ThemeEngineErr>;
35
36impl ThemeEngine {
37 pub fn new_with_theme(theme: &Theme) -> ThemeEngineResult<Self> {
41 let this = Self::new()?;
42 this.apply(theme);
43 Ok(this)
44 }
45
46 pub fn new() -> ThemeEngineResult<Self> {
47 let this = Self {
48 display: gdk::Display::default().ok_or(ThemeEngineErr::NoGdkDisplay)?,
49 current_theme_provider: Rc::new(RefCell::new(None)),
50 current_theme: Rc::new(RefCell::new(Theme::default())),
51 };
52 adw::StyleManager::default().connect_local(
54 "notify::dark",
55 true,
56 clone!(
57 #[strong]
58 this,
59 move |_| {
60 glib::idle_add_local_once(clone!(
63 #[strong]
64 this,
65 move || {
66 this.reset_current_theme();
67 }
68 ));
69 None
70 }
71 ),
72 );
73 Ok(this)
74 }
75
76 pub fn reset_current_theme(&self) {
77 self.apply(&self.current_theme.clone().take());
78 }
79
80 pub fn apply(&self, theme: &Theme) {
81 self.apply_no_set(theme);
82 self.current_theme.replace(theme.clone());
83 }
84
85 pub fn apply_no_set(&self, theme: &Theme) {
89 if let Some(current_theme_provider) = self.current_theme_provider.borrow().as_ref() {
90 gtk::style_context_remove_provider_for_display(&self.display, current_theme_provider);
91 }
92 if theme.is_system {
93 self.current_theme_provider.replace(None);
94 } else {
95 let provider = theme.get_provider(ThemeColorVariant::current(&self.display));
96 gtk::style_context_add_provider_for_display(
97 &self.display,
98 &provider,
99 gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
100 );
101 self.current_theme_provider.replace(Some(provider));
102 }
103 }
104
105 pub fn current_theme_name(&self) -> String {
108 self.current_theme.borrow().name.clone()
109 }
110
111 pub fn theme_chooser_widget(themes: &[Theme]) -> (gtk::Box, gtk::ListBox) {
123 let w = gtk::Box::builder()
124 .orientation(gtk::Orientation::Horizontal)
125 .spacing(12)
126 .hexpand(true)
127 .vexpand(true)
128 .build();
129 let lb = gtk::ListBox::builder()
130 .hexpand(true)
131 .vexpand(true)
132 .valign(gtk::Align::Start)
133 .css_classes(["boxed-list"])
134 .selection_mode(gtk::SelectionMode::Single)
135 .build();
136 lb.append(&Theme::default().action_row());
137 for theme in themes {
138 lb.append(&theme.action_row());
139 }
140 w.append(
141 >k::ScrolledWindow::builder()
142 .min_content_height(300)
143 .hscrollbar_policy(gtk::PolicyType::Never)
144 .hexpand(true)
145 .vexpand(true)
146 .child(
147 &adw::Clamp::builder()
148 .hexpand(true)
149 .vexpand(true)
150 .margin_top(24)
151 .margin_bottom(24)
152 .margin_start(12)
153 .margin_end(12)
154 .child(&lb)
155 .build(),
156 )
157 .build(),
158 );
159 (w, lb)
160 }
161
162 pub fn theme_chooser_dialog(&self, themes: &[Theme]) -> adw::Dialog {
171 let (theme_chooser_w, themes_lb) = Self::theme_chooser_widget(themes);
172 let apply_btn = gtk::Button::builder()
173 .label("Apply")
174 .css_classes(["suggested-action"])
175 .build();
176 let cancel_btn = gtk::Button::builder().label("Cancel").build();
177 let dialog = adw::Dialog::builder()
178 .child(&{
179 let tbv = adw::ToolbarView::builder()
180 .content(&theme_chooser_w)
181 .build();
182 tbv.add_top_bar(&{
183 let hb = adw::HeaderBar::builder()
184 .title_widget(&adw::WindowTitle::builder().title("Select a Theme").build())
185 .show_start_title_buttons(false)
186 .show_end_title_buttons(false)
187 .build();
188 hb.pack_start(&cancel_btn);
189 hb.pack_end(&apply_btn);
190
191 hb
192 });
193 tbv
194 })
195 .presentation_mode(adw::DialogPresentationMode::BottomSheet)
196 .build();
197
198 let themes_v: Vec<Theme> = themes.to_vec();
199 themes_lb.connect_row_selected(clone!(
200 #[strong]
201 apply_btn,
202 #[strong]
203 themes_v,
204 #[strong(rename_to = this)]
205 self,
206 move |_, row| {
207 apply_btn.set_sensitive(row.is_some());
208 if let Some(row) = row {
209 let target_name = row.clone().downcast::<adw::ActionRow>().unwrap().title();
212 if let Some(theme) = themes_v.iter().find(|theme| theme.name == target_name) {
213 this.apply_no_set(theme);
214 } else {
215 this.apply_no_set(&Theme::default())
216 }
217 }
218 }
219 ));
220 apply_btn.connect_clicked(clone!(
221 #[strong]
222 themes_lb,
223 #[strong]
224 themes_v,
225 #[strong]
226 dialog,
227 #[strong(rename_to = this)]
228 self,
229 move |_| {
230 if let Some(row) = themes_lb.selected_row() {
231 let target_name = row.downcast::<adw::ActionRow>().unwrap().title();
234 if let Some(theme) = themes_v.iter().find(|theme| theme.name == target_name) {
235 this.apply(theme);
236 } else {
237 this.apply(&Theme::default())
238 }
239 dialog.close();
240 }
241 }
242 ));
243 cancel_btn.connect_clicked(clone!(
244 #[strong]
245 dialog,
246 #[strong(rename_to = this)]
247 self,
248 move |_| {
249 this.reset_current_theme();
250 dialog.close();
251 }
252 ));
253
254 dialog
255 }
256}