1#![warn(missing_docs)]
53#![forbid(unsafe_code)]
54#![deny(clippy::unwrap_used)]
55#![deny(clippy::expect_used)]
56
57pub mod extended;
58pub mod icons;
59pub mod palette;
60
61pub use native_theme::{
63 AnimatedIcon, IconData, IconProvider, IconRole, IconSet, ResolvedThemeVariant, SystemTheme,
64 ThemeSpec, ThemeVariant,
65};
66
67#[must_use]
83pub fn to_theme(
84 resolved: &native_theme::ResolvedThemeVariant,
85 name: &str,
86) -> iced_core::theme::Theme {
87 let pal = palette::to_palette(resolved);
88
89 let resolved_clone = resolved.clone();
91
92 iced_core::theme::Theme::custom_with_fn(name.to_string(), pal, move |p| {
93 let mut ext = iced_core::theme::palette::Extended::generate(p);
94 extended::apply_overrides(&mut ext, &resolved_clone);
95 ext
96 })
97}
98
99pub fn from_preset(name: &str, is_dark: bool) -> native_theme::Result<iced_core::theme::Theme> {
108 let spec = native_theme::ThemeSpec::preset(name)?;
109 let variant = spec
110 .pick_variant(is_dark)
111 .ok_or_else(|| native_theme::Error::Format(format!("preset '{name}' has no variants")))?;
112 let resolved = variant.clone().into_resolved()?;
113 Ok(to_theme(&resolved, name))
114}
115
116pub fn from_system() -> native_theme::Result<iced_core::theme::Theme> {
122 let sys = native_theme::SystemTheme::from_system()?;
123 Ok(to_theme(sys.active(), &sys.name))
124}
125
126pub trait SystemThemeExt {
128 fn to_iced_theme(&self) -> iced_core::theme::Theme;
130}
131
132impl SystemThemeExt for native_theme::SystemTheme {
133 fn to_iced_theme(&self) -> iced_core::theme::Theme {
134 to_theme(self.active(), &self.name)
135 }
136}
137
138#[must_use]
142pub fn button_padding(resolved: &native_theme::ResolvedThemeVariant) -> iced_core::Padding {
143 iced_core::Padding::from([
144 resolved.button.padding_vertical,
145 resolved.button.padding_horizontal,
146 ])
147}
148
149#[must_use]
153pub fn input_padding(resolved: &native_theme::ResolvedThemeVariant) -> iced_core::Padding {
154 iced_core::Padding::from([
155 resolved.input.padding_vertical,
156 resolved.input.padding_horizontal,
157 ])
158}
159
160#[must_use]
162pub fn border_radius(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
163 resolved.defaults.radius
164}
165
166#[must_use]
168pub fn border_radius_lg(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
169 resolved.defaults.radius_lg
170}
171
172#[must_use]
174pub fn scrollbar_width(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
175 resolved.scrollbar.width
176}
177
178#[must_use]
180pub fn font_family(resolved: &native_theme::ResolvedThemeVariant) -> &str {
181 &resolved.defaults.font.family
182}
183
184#[must_use]
189pub fn font_size(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
190 resolved.defaults.font.size
191}
192
193#[must_use]
195pub fn mono_font_family(resolved: &native_theme::ResolvedThemeVariant) -> &str {
196 &resolved.defaults.mono_font.family
197}
198
199#[must_use]
204pub fn mono_font_size(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
205 resolved.defaults.mono_font.size
206}
207
208#[must_use]
210pub fn font_weight(resolved: &native_theme::ResolvedThemeVariant) -> u16 {
211 resolved.defaults.font.weight
212}
213
214#[must_use]
216pub fn mono_font_weight(resolved: &native_theme::ResolvedThemeVariant) -> u16 {
217 resolved.defaults.mono_font.weight
218}
219
220#[must_use]
225pub fn line_height(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
226 resolved.defaults.line_height * resolved.defaults.font.size
227}
228
229#[cfg(test)]
230#[allow(clippy::unwrap_used, clippy::expect_used)]
231mod tests {
232 use super::*;
233 use native_theme::ThemeSpec;
234
235 fn make_resolved(is_dark: bool) -> native_theme::ResolvedThemeVariant {
236 let nt = ThemeSpec::preset("catppuccin-mocha").unwrap();
237 let mut variant = nt.pick_variant(is_dark).unwrap().clone();
238 variant.resolve();
239 variant.validate().unwrap()
240 }
241
242 #[test]
245 fn to_theme_produces_non_default_theme() {
246 let resolved = make_resolved(true);
247 let theme = to_theme(&resolved, "Test Theme");
248
249 assert_ne!(theme, iced_core::theme::Theme::Light);
250 assert_ne!(theme, iced_core::theme::Theme::Dark);
251
252 let palette = theme.palette();
253 assert!(
255 palette.primary.r > 0.0 || palette.primary.g > 0.0 || palette.primary.b > 0.0,
256 "primary should be non-zero"
257 );
258 }
259
260 #[test]
261 fn to_theme_from_preset() {
262 let resolved = make_resolved(false);
263 let theme = to_theme(&resolved, "Default");
264
265 let palette = theme.palette();
266 assert!(palette.background.r > 0.9);
268 }
269
270 #[test]
273 fn border_radius_returns_resolved_value() {
274 let resolved = make_resolved(false);
275 let r = border_radius(&resolved);
276 assert!(r > 0.0, "resolved radius should be > 0");
277 }
278
279 #[test]
280 fn border_radius_lg_returns_resolved_value() {
281 let resolved = make_resolved(false);
282 let r = border_radius_lg(&resolved);
283 assert!(r > 0.0, "resolved radius_lg should be > 0");
284 assert!(
285 r >= border_radius(&resolved),
286 "radius_lg should be >= radius"
287 );
288 }
289
290 #[test]
291 fn scrollbar_width_returns_resolved_value() {
292 let resolved = make_resolved(false);
293 let w = scrollbar_width(&resolved);
294 assert!(w > 0.0, "scrollbar width should be > 0");
295 }
296
297 #[test]
298 fn button_padding_returns_iced_padding() {
299 let resolved = make_resolved(false);
300 let pad = button_padding(&resolved);
301 assert!(pad.top > 0.0, "button vertical (top) padding should be > 0");
302 assert!(
303 pad.right > 0.0,
304 "button horizontal (right) padding should be > 0"
305 );
306 assert_eq!(pad.top, pad.bottom, "top and bottom should be equal");
308 assert_eq!(pad.left, pad.right, "left and right should be equal");
309 }
310
311 #[test]
312 fn input_padding_returns_iced_padding() {
313 let resolved = make_resolved(false);
314 let pad = input_padding(&resolved);
315 assert!(pad.top > 0.0, "input vertical (top) padding should be > 0");
316 assert!(
317 pad.right > 0.0,
318 "input horizontal (right) padding should be > 0"
319 );
320 }
321
322 #[test]
325 fn font_family_returns_concrete_value() {
326 let resolved = make_resolved(false);
327 let ff = font_family(&resolved);
328 assert!(!ff.is_empty(), "font family should not be empty");
329 }
330
331 #[test]
332 fn font_size_returns_concrete_value() {
333 let resolved = make_resolved(false);
334 let fs = font_size(&resolved);
335 assert!(fs > 0.0, "font size should be > 0");
336 }
337
338 #[test]
339 fn mono_font_family_returns_concrete_value() {
340 let resolved = make_resolved(false);
341 let mf = mono_font_family(&resolved);
342 assert!(!mf.is_empty(), "mono font family should not be empty");
343 }
344
345 #[test]
346 fn mono_font_size_returns_concrete_value() {
347 let resolved = make_resolved(false);
348 let ms = mono_font_size(&resolved);
349 assert!(ms > 0.0, "mono font size should be > 0");
350 }
351
352 #[test]
353 fn font_weight_returns_concrete_value() {
354 let resolved = make_resolved(false);
355 let w = font_weight(&resolved);
356 assert!(
357 (100..=900).contains(&w),
358 "font weight should be 100-900, got {}",
359 w
360 );
361 }
362
363 #[test]
364 fn mono_font_weight_returns_concrete_value() {
365 let resolved = make_resolved(false);
366 let w = mono_font_weight(&resolved);
367 assert!(
368 (100..=900).contains(&w),
369 "mono font weight should be 100-900, got {}",
370 w
371 );
372 }
373
374 #[test]
375 fn line_height_returns_concrete_value() {
376 let resolved = make_resolved(false);
377 let lh = line_height(&resolved);
378 assert!(lh > 0.0, "line height should be > 0");
379 }
380
381 #[test]
384 fn from_preset_valid_light() {
385 let theme = from_preset("catppuccin-mocha", false).expect("preset should load");
386 assert_ne!(theme, iced_core::theme::Theme::Light);
388 }
389
390 #[test]
391 fn from_preset_valid_dark() {
392 let theme = from_preset("catppuccin-mocha", true).expect("preset should load");
393 assert_ne!(theme, iced_core::theme::Theme::Dark);
394 }
395
396 #[test]
397 fn from_preset_invalid_name() {
398 let result = from_preset("nonexistent-preset", false);
399 assert!(result.is_err(), "invalid preset should return Err");
400 }
401
402 #[test]
403 fn system_theme_ext_to_iced_theme() {
404 let Ok(sys) = native_theme::SystemTheme::from_system() else {
406 return;
407 };
408 let _theme = sys.to_iced_theme();
409 }
410
411 #[test]
412 fn from_system_does_not_panic() {
413 let _ = from_system();
414 }
415}