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
99#[must_use = "this returns the theme; it does not apply it"]
108pub fn from_preset(
109 name: &str,
110 is_dark: bool,
111) -> native_theme::Result<(iced_core::theme::Theme, native_theme::ResolvedThemeVariant)> {
112 let spec = native_theme::ThemeSpec::preset(name)?;
113 let variant = spec
114 .pick_variant(is_dark)
115 .ok_or_else(|| native_theme::Error::Format(format!("preset '{name}' has no variants")))?;
116 let resolved = variant.clone().into_resolved()?;
117 let theme = to_theme(&resolved, name);
118 Ok((theme, resolved))
119}
120
121#[must_use = "this returns the theme; it does not apply it"]
127pub fn from_system()
128-> native_theme::Result<(iced_core::theme::Theme, native_theme::ResolvedThemeVariant)> {
129 let sys = native_theme::SystemTheme::from_system()?;
130 let resolved = sys.active().clone();
131 let theme = to_theme(&resolved, &sys.name);
132 Ok((theme, resolved))
133}
134
135pub trait SystemThemeExt {
137 #[must_use = "this returns the theme; it does not apply it"]
139 fn to_iced_theme(&self) -> iced_core::theme::Theme;
140}
141
142impl SystemThemeExt for native_theme::SystemTheme {
143 fn to_iced_theme(&self) -> iced_core::theme::Theme {
144 to_theme(self.active(), &self.name)
145 }
146}
147
148#[must_use]
152pub fn button_padding(resolved: &native_theme::ResolvedThemeVariant) -> iced_core::Padding {
153 iced_core::Padding::from([
154 resolved.button.padding_vertical,
155 resolved.button.padding_horizontal,
156 ])
157}
158
159#[must_use]
163pub fn input_padding(resolved: &native_theme::ResolvedThemeVariant) -> iced_core::Padding {
164 iced_core::Padding::from([
165 resolved.input.padding_vertical,
166 resolved.input.padding_horizontal,
167 ])
168}
169
170#[must_use]
172pub fn border_radius(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
173 resolved.defaults.radius
174}
175
176#[must_use]
178pub fn border_radius_lg(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
179 resolved.defaults.radius_lg
180}
181
182#[must_use]
184pub fn scrollbar_width(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
185 resolved.scrollbar.width
186}
187
188#[must_use]
190pub fn font_family(resolved: &native_theme::ResolvedThemeVariant) -> &str {
191 &resolved.defaults.font.family
192}
193
194#[must_use]
199pub fn font_size(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
200 resolved.defaults.font.size
201}
202
203#[must_use]
205pub fn mono_font_family(resolved: &native_theme::ResolvedThemeVariant) -> &str {
206 &resolved.defaults.mono_font.family
207}
208
209#[must_use]
214pub fn mono_font_size(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
215 resolved.defaults.mono_font.size
216}
217
218#[must_use]
220pub fn font_weight(resolved: &native_theme::ResolvedThemeVariant) -> u16 {
221 resolved.defaults.font.weight
222}
223
224#[must_use]
226pub fn mono_font_weight(resolved: &native_theme::ResolvedThemeVariant) -> u16 {
227 resolved.defaults.mono_font.weight
228}
229
230#[must_use]
235pub fn line_height(resolved: &native_theme::ResolvedThemeVariant) -> f32 {
236 resolved.defaults.line_height * resolved.defaults.font.size
237}
238
239#[cfg(test)]
240#[allow(clippy::unwrap_used, clippy::expect_used)]
241mod tests {
242 use super::*;
243 use native_theme::ThemeSpec;
244
245 fn make_resolved(is_dark: bool) -> native_theme::ResolvedThemeVariant {
246 let nt = ThemeSpec::preset("catppuccin-mocha").unwrap();
247 let mut variant = nt.pick_variant(is_dark).unwrap().clone();
248 variant.resolve();
249 variant.validate().unwrap()
250 }
251
252 #[test]
255 fn to_theme_produces_non_default_theme() {
256 let resolved = make_resolved(true);
257 let theme = to_theme(&resolved, "Test Theme");
258
259 assert_ne!(theme, iced_core::theme::Theme::Light);
260 assert_ne!(theme, iced_core::theme::Theme::Dark);
261
262 let palette = theme.palette();
263 assert!(
265 palette.primary.r > 0.0 || palette.primary.g > 0.0 || palette.primary.b > 0.0,
266 "primary should be non-zero"
267 );
268 }
269
270 #[test]
271 fn to_theme_from_preset() {
272 let resolved = make_resolved(false);
273 let theme = to_theme(&resolved, "Default");
274
275 let palette = theme.palette();
276 assert!(palette.background.r > 0.9);
278 }
279
280 #[test]
283 fn border_radius_returns_resolved_value() {
284 let resolved = make_resolved(false);
285 let r = border_radius(&resolved);
286 assert!(r > 0.0, "resolved radius should be > 0");
287 }
288
289 #[test]
290 fn border_radius_lg_returns_resolved_value() {
291 let resolved = make_resolved(false);
292 let r = border_radius_lg(&resolved);
293 assert!(r > 0.0, "resolved radius_lg should be > 0");
294 assert!(
295 r >= border_radius(&resolved),
296 "radius_lg should be >= radius"
297 );
298 }
299
300 #[test]
301 fn scrollbar_width_returns_resolved_value() {
302 let resolved = make_resolved(false);
303 let w = scrollbar_width(&resolved);
304 assert!(w > 0.0, "scrollbar width should be > 0");
305 }
306
307 #[test]
308 fn button_padding_returns_iced_padding() {
309 let resolved = make_resolved(false);
310 let pad = button_padding(&resolved);
311 assert!(pad.top > 0.0, "button vertical (top) padding should be > 0");
312 assert!(
313 pad.right > 0.0,
314 "button horizontal (right) padding should be > 0"
315 );
316 assert_eq!(pad.top, pad.bottom, "top and bottom should be equal");
318 assert_eq!(pad.left, pad.right, "left and right should be equal");
319 }
320
321 #[test]
322 fn input_padding_returns_iced_padding() {
323 let resolved = make_resolved(false);
324 let pad = input_padding(&resolved);
325 assert!(pad.top > 0.0, "input vertical (top) padding should be > 0");
326 assert!(
327 pad.right > 0.0,
328 "input horizontal (right) padding should be > 0"
329 );
330 }
331
332 #[test]
335 fn font_family_returns_concrete_value() {
336 let resolved = make_resolved(false);
337 let ff = font_family(&resolved);
338 assert!(!ff.is_empty(), "font family should not be empty");
339 }
340
341 #[test]
342 fn font_size_returns_concrete_value() {
343 let resolved = make_resolved(false);
344 let fs = font_size(&resolved);
345 assert!(fs > 0.0, "font size should be > 0");
346 }
347
348 #[test]
349 fn mono_font_family_returns_concrete_value() {
350 let resolved = make_resolved(false);
351 let mf = mono_font_family(&resolved);
352 assert!(!mf.is_empty(), "mono font family should not be empty");
353 }
354
355 #[test]
356 fn mono_font_size_returns_concrete_value() {
357 let resolved = make_resolved(false);
358 let ms = mono_font_size(&resolved);
359 assert!(ms > 0.0, "mono font size should be > 0");
360 }
361
362 #[test]
363 fn font_weight_returns_concrete_value() {
364 let resolved = make_resolved(false);
365 let w = font_weight(&resolved);
366 assert!(
367 (100..=900).contains(&w),
368 "font weight should be 100-900, got {}",
369 w
370 );
371 }
372
373 #[test]
374 fn mono_font_weight_returns_concrete_value() {
375 let resolved = make_resolved(false);
376 let w = mono_font_weight(&resolved);
377 assert!(
378 (100..=900).contains(&w),
379 "mono font weight should be 100-900, got {}",
380 w
381 );
382 }
383
384 #[test]
385 fn line_height_returns_concrete_value() {
386 let resolved = make_resolved(false);
387 let lh = line_height(&resolved);
388 assert!(lh > 0.0, "line height should be > 0");
389 }
390
391 #[test]
394 fn from_preset_valid_light() {
395 let (theme, resolved) = from_preset("catppuccin-mocha", false).expect("preset should load");
396 assert_ne!(theme, iced_core::theme::Theme::Light);
398 assert!(!resolved.defaults.font.family.is_empty());
400 }
401
402 #[test]
403 fn from_preset_valid_dark() {
404 let (theme, _resolved) = from_preset("catppuccin-mocha", true).expect("preset should load");
405 assert_ne!(theme, iced_core::theme::Theme::Dark);
406 }
407
408 #[test]
409 fn from_preset_invalid_name() {
410 let result = from_preset("nonexistent-preset", false);
411 assert!(result.is_err(), "invalid preset should return Err");
412 }
413
414 #[test]
415 fn system_theme_ext_to_iced_theme() {
416 let Ok(sys) = native_theme::SystemTheme::from_system() else {
418 return;
419 };
420 let _theme = sys.to_iced_theme();
421 }
422
423 #[test]
424 fn from_system_does_not_panic() {
425 let _ = from_system();
426 }
427}