1#![warn(missing_docs)]
73#![forbid(unsafe_code)]
74#![deny(clippy::unwrap_used)]
75#![deny(clippy::expect_used)]
76
77pub mod extended;
78pub mod icons;
79pub mod palette;
80
81pub use native_theme::{
83 AnimatedIcon, Error, IconData, IconProvider, IconRole, IconSet, ResolvedThemeVariant, Result,
84 Rgba, SystemTheme, ThemeSpec, ThemeVariant, TransformAnimation,
85};
86
87#[must_use]
103pub fn to_theme(
104 resolved: &native_theme::ResolvedThemeVariant,
105 name: &str,
106) -> iced_core::theme::Theme {
107 let pal = palette::to_palette(resolved);
108
109 let btn_bg = resolved.button.background;
112 let btn_fg = resolved.button.foreground;
113 let surface = resolved.defaults.surface;
114 let foreground = resolved.defaults.foreground;
115
116 iced_core::theme::Theme::custom_with_fn(name.to_string(), pal, move |p| {
117 let mut ext = iced_core::theme::palette::Extended::generate(p);
118 ext.secondary.base.color = palette::to_color(btn_bg);
119 ext.secondary.base.text = palette::to_color(btn_fg);
120 ext.background.weak.color = palette::to_color(surface);
121 ext.background.weak.text = palette::to_color(foreground);
122 ext
123 })
124}
125
126#[must_use = "this returns the theme; it does not apply it"]
135pub fn from_preset(
136 name: &str,
137 is_dark: bool,
138) -> native_theme::Result<(iced_core::theme::Theme, native_theme::ResolvedThemeVariant)> {
139 let spec = native_theme::ThemeSpec::preset(name)?;
140 let variant = spec.into_variant(is_dark).ok_or_else(|| {
141 native_theme::Error::Format(format!("preset '{name}' has no light or dark variant"))
142 })?;
143 let resolved = variant.into_resolved()?;
144 let theme = to_theme(&resolved, name);
145 Ok((theme, resolved))
146}
147
148#[must_use = "this returns the theme; it does not apply it"]
154pub fn from_system()
155-> native_theme::Result<(iced_core::theme::Theme, native_theme::ResolvedThemeVariant)> {
156 let sys = native_theme::SystemTheme::from_system()?;
157 let name = sys.name;
158 let resolved = if sys.is_dark { sys.dark } else { sys.light };
159 let theme = to_theme(&resolved, &name);
160 Ok((theme, resolved))
161}
162
163pub trait SystemThemeExt {
165 #[must_use = "this returns the theme; it does not apply it"]
167 fn to_iced_theme(&self) -> iced_core::theme::Theme;
168}
169
170impl SystemThemeExt for native_theme::SystemTheme {
171 fn to_iced_theme(&self) -> iced_core::theme::Theme {
172 to_theme(self.active(), &self.name)
173 }
174}
175
176#[must_use]
180pub fn button_padding(resolved: &native_theme::ResolvedThemeVariant) -> iced_core::Padding {
181 iced_core::Padding::from([
182 resolved.button.padding_vertical,
183 resolved.button.padding_horizontal,
184 ])
185}
186
187#[must_use]
191pub fn input_padding(resolved: &native_theme::ResolvedThemeVariant) -> iced_core::Padding {
192 iced_core::Padding::from([
193 resolved.input.padding_vertical,
194 resolved.input.padding_horizontal,
195 ])
196}
197
198#[must_use]
200pub fn border_radius(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
201 resolved.defaults.radius
202}
203
204#[must_use]
206pub fn border_radius_lg(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
207 resolved.defaults.radius_lg
208}
209
210#[must_use]
212pub fn scrollbar_width(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
213 resolved.scrollbar.width
214}
215
216#[must_use]
218pub fn font_family(resolved: &native_theme::ResolvedThemeVariant) -> &str {
219 &resolved.defaults.font.family
220}
221
222#[must_use]
227pub fn font_size(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
228 resolved.defaults.font.size
229}
230
231#[must_use]
233pub fn mono_font_family(resolved: &native_theme::ResolvedThemeVariant) -> &str {
234 &resolved.defaults.mono_font.family
235}
236
237#[must_use]
242pub fn mono_font_size(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
243 resolved.defaults.mono_font.size
244}
245
246#[must_use]
248pub fn font_weight(resolved: &native_theme::ResolvedThemeVariant) -> u16 {
249 resolved.defaults.font.weight
250}
251
252#[must_use]
254pub fn mono_font_weight(resolved: &native_theme::ResolvedThemeVariant) -> u16 {
255 resolved.defaults.mono_font.weight
256}
257
258#[must_use]
268pub fn line_height_multiplier(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
269 resolved.defaults.line_height
270}
271
272#[must_use]
285pub fn to_iced_weight(css_weight: u16) -> iced_core::font::Weight {
286 use iced_core::font::Weight;
287 match css_weight {
288 0..=149 => Weight::Thin,
289 150..=249 => Weight::ExtraLight,
290 250..=349 => Weight::Light,
291 350..=449 => Weight::Normal,
292 450..=549 => Weight::Medium,
293 550..=649 => Weight::Semibold,
294 650..=749 => Weight::Bold,
295 750..=849 => Weight::ExtraBold,
296 850.. => Weight::Black,
297 }
298}
299
300#[cfg(test)]
301#[allow(clippy::unwrap_used, clippy::expect_used)]
302mod tests {
303 use super::*;
304 use native_theme::ThemeSpec;
305
306 fn make_resolved(is_dark: bool) -> native_theme::ResolvedThemeVariant {
307 ThemeSpec::preset("catppuccin-mocha")
308 .unwrap()
309 .into_variant(is_dark)
310 .unwrap()
311 .into_resolved()
312 .unwrap()
313 }
314
315 #[test]
318 fn to_theme_produces_non_default_theme() {
319 let resolved = make_resolved(true);
320 let theme = to_theme(&resolved, "Test Theme");
321
322 assert_ne!(theme, iced_core::theme::Theme::Light);
323 assert_ne!(theme, iced_core::theme::Theme::Dark);
324
325 let palette = theme.palette();
326 assert!(
328 palette.primary.r > 0.0 || palette.primary.g > 0.0 || palette.primary.b > 0.0,
329 "primary should be non-zero"
330 );
331 }
332
333 #[test]
334 fn to_theme_from_preset() {
335 let resolved = make_resolved(false);
336 let theme = to_theme(&resolved, "Default");
337
338 let palette = theme.palette();
339 assert!(palette.background.r > 0.9);
341 }
342
343 #[test]
346 fn border_radius_returns_resolved_value() {
347 let resolved = make_resolved(false);
348 let r = border_radius(&resolved);
349 assert!(r > 0.0, "resolved radius should be > 0");
350 }
351
352 #[test]
353 fn border_radius_lg_returns_resolved_value() {
354 let resolved = make_resolved(false);
355 let r = border_radius_lg(&resolved);
356 assert!(r > 0.0, "resolved radius_lg should be > 0");
357 assert!(
358 r >= border_radius(&resolved),
359 "radius_lg should be >= radius"
360 );
361 }
362
363 #[test]
364 fn scrollbar_width_returns_resolved_value() {
365 let resolved = make_resolved(false);
366 let w = scrollbar_width(&resolved);
367 assert!(w > 0.0, "scrollbar width should be > 0");
368 }
369
370 #[test]
371 fn button_padding_returns_iced_padding() {
372 let resolved = make_resolved(false);
373 let pad = button_padding(&resolved);
374 assert!(pad.top > 0.0, "button vertical (top) padding should be > 0");
375 assert!(
376 pad.right > 0.0,
377 "button horizontal (right) padding should be > 0"
378 );
379 assert_eq!(pad.top, pad.bottom, "top and bottom should be equal");
381 assert_eq!(pad.left, pad.right, "left and right should be equal");
382 }
383
384 #[test]
385 fn input_padding_returns_iced_padding() {
386 let resolved = make_resolved(false);
387 let pad = input_padding(&resolved);
388 assert!(pad.top > 0.0, "input vertical (top) padding should be > 0");
389 assert!(
390 pad.right > 0.0,
391 "input horizontal (right) padding should be > 0"
392 );
393 }
394
395 #[test]
398 fn font_family_returns_concrete_value() {
399 let resolved = make_resolved(false);
400 let ff = font_family(&resolved);
401 assert!(!ff.is_empty(), "font family should not be empty");
402 }
403
404 #[test]
405 fn font_size_returns_concrete_value() {
406 let resolved = make_resolved(false);
407 let fs = font_size(&resolved);
408 assert!(fs > 0.0, "font size should be > 0");
409 }
410
411 #[test]
412 fn mono_font_family_returns_concrete_value() {
413 let resolved = make_resolved(false);
414 let mf = mono_font_family(&resolved);
415 assert!(!mf.is_empty(), "mono font family should not be empty");
416 }
417
418 #[test]
419 fn mono_font_size_returns_concrete_value() {
420 let resolved = make_resolved(false);
421 let ms = mono_font_size(&resolved);
422 assert!(ms > 0.0, "mono font size should be > 0");
423 }
424
425 #[test]
426 fn font_weight_returns_concrete_value() {
427 let resolved = make_resolved(false);
428 let w = font_weight(&resolved);
429 assert!(
430 (100..=900).contains(&w),
431 "font weight should be 100-900, got {}",
432 w
433 );
434 }
435
436 #[test]
437 fn mono_font_weight_returns_concrete_value() {
438 let resolved = make_resolved(false);
439 let w = mono_font_weight(&resolved);
440 assert!(
441 (100..=900).contains(&w),
442 "mono font weight should be 100-900, got {}",
443 w
444 );
445 }
446
447 #[test]
448 fn line_height_multiplier_returns_concrete_value() {
449 let resolved = make_resolved(false);
450 let lh = line_height_multiplier(&resolved);
451 assert!(lh > 0.0, "line height multiplier should be > 0");
452 assert!(
453 lh < 5.0,
454 "line height multiplier should be a multiplier (e.g. 1.4), got {}",
455 lh
456 );
457 }
458
459 #[test]
460 fn to_iced_weight_standard_weights() {
461 use iced_core::font::Weight;
462 assert_eq!(to_iced_weight(100), Weight::Thin);
463 assert_eq!(to_iced_weight(200), Weight::ExtraLight);
464 assert_eq!(to_iced_weight(300), Weight::Light);
465 assert_eq!(to_iced_weight(400), Weight::Normal);
466 assert_eq!(to_iced_weight(500), Weight::Medium);
467 assert_eq!(to_iced_weight(600), Weight::Semibold);
468 assert_eq!(to_iced_weight(700), Weight::Bold);
469 assert_eq!(to_iced_weight(800), Weight::ExtraBold);
470 assert_eq!(to_iced_weight(900), Weight::Black);
471 }
472
473 #[test]
474 fn to_iced_weight_non_standard_rounds_correctly() {
475 use iced_core::font::Weight;
476 assert_eq!(to_iced_weight(350), Weight::Normal);
477 assert_eq!(to_iced_weight(450), Weight::Medium);
478 assert_eq!(to_iced_weight(550), Weight::Semibold);
479 assert_eq!(to_iced_weight(0), Weight::Thin);
480 assert_eq!(to_iced_weight(1000), Weight::Black);
481 }
482
483 #[test]
486 fn from_preset_valid_light() {
487 let (theme, resolved) = from_preset("catppuccin-mocha", false).expect("preset should load");
488 assert_ne!(theme, iced_core::theme::Theme::Light);
490 assert!(!resolved.defaults.font.family.is_empty());
492 }
493
494 #[test]
495 fn from_preset_valid_dark() {
496 let (theme, _resolved) = from_preset("catppuccin-mocha", true).expect("preset should load");
497 assert_ne!(theme, iced_core::theme::Theme::Dark);
498 }
499
500 #[test]
501 fn from_preset_invalid_name() {
502 let result = from_preset("nonexistent-preset", false);
503 assert!(result.is_err(), "invalid preset should return Err");
504 }
505
506 #[test]
507 fn system_theme_ext_to_iced_theme() {
508 let Ok(sys) = native_theme::SystemTheme::from_system() else {
510 return;
511 };
512 let _theme = sys.to_iced_theme();
513 }
514
515 #[test]
516 fn from_system_does_not_panic() {
517 let _ = from_system();
518 }
519}