1use crate::prelude::*;
24#[cfg(feature = "serde")]
25use serde::{Deserialize, Serialize};
26#[cfg(not(target_arch = "wasm32"))]
27use std::fmt;
28#[cfg(not(target_arch = "wasm32"))]
29use std::path::PathBuf;
30use std::{
31 borrow::Cow,
32 collections::hash_map::DefaultHasher,
33 hash::{Hash, Hasher},
34};
35
36pub(crate) type FontId = u64;
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
80#[must_use]
81#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
82pub struct ThemeBuilder {
83 name: String,
84 #[cfg_attr(feature = "serde", serde(skip))]
85 fonts: Fonts,
86 size: u32,
87 styles: FontStyles,
88 colors: Colors,
89 spacing: Spacing,
90}
91
92impl Default for ThemeBuilder {
93 fn default() -> Self {
94 let theme = Theme::default();
95 Self {
96 name: theme.name,
97 fonts: theme.fonts,
98 size: theme.font_size,
99 styles: theme.styles,
100 colors: theme.colors,
101 spacing: theme.spacing,
102 }
103 }
104}
105
106#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109pub enum FontType {
110 Body,
112 Heading,
114 Monospace,
116}
117
118#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
120#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
121pub enum ColorType {
122 Background,
125 Surface,
127 Primary,
129 PrimaryVariant,
131 Secondary,
133 SecondaryVariant,
135 Error,
137 OnBackground,
139 OnSurface,
141 OnPrimary,
143 OnSecondary,
145 OnError,
147}
148
149impl ThemeBuilder {
150 pub fn new<S: Into<String>>(name: S) -> Self {
152 Self {
153 name: name.into(),
154 ..Self::default()
155 }
156 }
157
158 pub fn font_size(&mut self, size: u32) -> &mut Self {
160 self.size = size;
161 self
162 }
163
164 pub fn font(&mut self, font_type: FontType, font: Font, style: FontStyle) -> &mut Self {
166 match font_type {
167 FontType::Body => {
168 self.fonts.body = font;
169 self.styles.body = style;
170 }
171 FontType::Heading => {
172 self.fonts.heading = font;
173 self.styles.heading = style;
174 }
175 FontType::Monospace => {
176 self.fonts.monospace = font;
177 self.styles.monospace = style;
178 }
179 }
180 self
181 }
182
183 pub fn color<C: Into<Color>>(&mut self, color_type: ColorType, color: C) -> &mut Self {
185 let color = color.into();
186 let c = &mut self.colors;
187 match color_type {
188 ColorType::Background => c.background = color,
189 ColorType::Surface => c.surface = color,
190 ColorType::Primary => c.primary = color,
191 ColorType::PrimaryVariant => c.primary_variant = color,
192 ColorType::Secondary => c.secondary = color,
193 ColorType::SecondaryVariant => c.secondary_variant = color,
194 ColorType::Error => c.error = color,
195 ColorType::OnBackground => c.on_background = color,
196 ColorType::OnSurface => c.on_surface = color,
197 ColorType::OnPrimary => c.on_primary = color,
198 ColorType::OnSecondary => c.on_secondary = color,
199 ColorType::OnError => c.on_error = color,
200 }
201 self
202 }
203
204 pub fn spacing(&mut self, spacing: Spacing) -> &mut Self {
206 self.spacing = spacing;
207 self
208 }
209
210 pub fn build(&self) -> Theme {
212 Theme {
213 name: self.name.clone(),
214 fonts: self.fonts.clone(),
215 font_size: self.size,
216 styles: self.styles,
217 colors: self.colors,
218 spacing: self.spacing,
219 }
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Hash)]
225#[must_use]
226pub struct Font {
227 pub(crate) name: Cow<'static, str>,
229 #[cfg(not(target_arch = "wasm32"))]
230 pub(crate) source: FontSrc,
232}
233
234#[cfg(not(target_arch = "wasm32"))]
235impl Default for Font {
236 fn default() -> Self {
237 Self::EMULOGIC
238 }
239}
240
241#[cfg(target_arch = "wasm32")]
242impl Default for Font {
243 fn default() -> Self {
244 Self::named("Arial")
245 }
246}
247
248impl Font {
249 #[cfg(not(target_arch = "wasm32"))]
250 const NOTO_TTF: &'static [u8] = include_bytes!("../../assets/noto_sans_regular.ttf");
251 #[cfg(not(target_arch = "wasm32"))]
252 const EMULOGIC_TTF: &'static [u8] = include_bytes!("../../assets/emulogic.ttf");
253 #[cfg(not(target_arch = "wasm32"))]
254 const INCONSOLATA_TTF: &'static [u8] = include_bytes!("../../assets/inconsolata_bold.ttf");
255
256 #[cfg(not(target_arch = "wasm32"))]
259 pub const NOTO: Self = Self::from_bytes("Noto", Self::NOTO_TTF);
260
261 #[cfg(not(target_arch = "wasm32"))]
263 pub const EMULOGIC: Self = Self::from_bytes("Emulogic", Self::EMULOGIC_TTF);
264
265 #[cfg(not(target_arch = "wasm32"))]
268 pub const INCONSOLATA: Self = Self::from_bytes("Inconsolata", Self::INCONSOLATA_TTF);
269
270 #[inline]
272 pub const fn named(name: &'static str) -> Self {
273 Self {
274 name: Cow::Borrowed(name),
275 #[cfg(not(target_arch = "wasm32"))]
276 source: FontSrc::None,
277 }
278 }
279
280 #[cfg(not(target_arch = "wasm32"))]
282 #[inline]
283 pub const fn from_bytes(name: &'static str, bytes: &'static [u8]) -> Self {
284 Self {
285 name: Cow::Borrowed(name),
286 source: FontSrc::from_bytes(bytes),
287 }
288 }
289
290 #[cfg(not(target_arch = "wasm32"))]
292 #[inline]
293 pub fn from_file<S, P>(name: S, path: P) -> Self
294 where
295 S: Into<Cow<'static, str>>,
296 P: Into<PathBuf>,
297 {
298 Self {
299 name: name.into(),
300 source: FontSrc::from_file(path),
301 }
302 }
303
304 #[inline]
306 #[must_use]
307 pub fn name(&self) -> &str {
308 self.name.as_ref()
309 }
310
311 #[cfg(not(target_arch = "wasm32"))]
313 #[inline]
314 #[must_use]
315 pub(crate) const fn source(&self) -> &FontSrc {
316 &self.source
317 }
318
319 #[inline]
321 #[must_use]
322 pub fn id(&self) -> FontId {
323 let mut hasher = DefaultHasher::new();
324 self.name.hash(&mut hasher);
325 hasher.finish()
326 }
327}
328
329#[derive(Clone, PartialEq, Eq, Hash)]
331#[cfg(not(target_arch = "wasm32"))]
332pub(crate) enum FontSrc {
333 None,
335 Bytes(&'static [u8]),
337 Path(PathBuf),
339}
340
341#[cfg(not(target_arch = "wasm32"))]
342impl fmt::Debug for FontSrc {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 match self {
345 Self::None => write!(f, "None"),
346 Self::Bytes(bytes) => write!(f, "Bytes([u8; {}])", bytes.len()),
347 #[cfg(not(target_arch = "wasm32"))]
348 Self::Path(path) => write!(f, "Path({})", path.display()),
349 }
350 }
351}
352
353#[cfg(not(target_arch = "wasm32"))]
354impl FontSrc {
355 pub(crate) const fn from_bytes(bytes: &'static [u8]) -> Self {
356 Self::Bytes(bytes)
357 }
358
359 #[cfg(not(target_arch = "wasm32"))]
360 pub(crate) fn from_file<P: Into<PathBuf>>(path: P) -> Self {
361 Self::Path(path.into())
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Hash)]
367#[non_exhaustive]
368pub struct Fonts {
369 pub body: Font,
371 pub heading: Font,
373 pub monospace: Font,
375}
376
377impl Default for Fonts {
378 fn default() -> Self {
379 Self {
380 body: Font::default(),
381 heading: Font::default(),
382 #[cfg(not(target_arch = "wasm32"))]
383 monospace: Font::INCONSOLATA,
384 #[cfg(target_arch = "wasm32")]
385 monospace: Font::named("Courier"),
386 }
387 }
388}
389
390#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
392#[non_exhaustive]
393#[must_use]
394#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
395pub struct FontStyles {
396 pub body: FontStyle,
398 pub heading: FontStyle,
400 pub monospace: FontStyle,
402}
403
404#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
406#[non_exhaustive]
407#[must_use]
408#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
409pub struct Colors {
410 pub background: Color,
413 pub surface: Color,
415 pub primary: Color,
417 pub primary_variant: Color,
419 pub secondary: Color,
421 pub secondary_variant: Color,
423 pub error: Color,
425 pub on_background: Color,
427 pub on_surface: Color,
429 pub on_primary: Color,
431 pub on_secondary: Color,
433 pub on_error: Color,
435}
436
437impl Colors {
438 #[allow(clippy::unreadable_literal)]
440 pub const fn dark() -> Self {
441 Self {
442 background: Color::from_hex(0x121212),
443 surface: Color::from_hex(0x121212),
444 primary: Color::from_hex(0xbf360c),
445 primary_variant: Color::from_hex(0xff6f43),
446 secondary: Color::from_hex(0x0c95bf),
447 secondary_variant: Color::from_hex(0x43d3ff),
448 error: Color::from_hex(0xcf6679),
449 on_background: Color::WHITE,
450 on_surface: Color::WHITE,
451 on_primary: Color::BLACK,
452 on_secondary: Color::BLACK,
453 on_error: Color::BLACK,
454 }
455 }
456
457 #[allow(clippy::unreadable_literal)]
459 pub const fn light() -> Self {
460 Self {
461 background: Color::from_hex(0xffffff),
462 surface: Color::from_hex(0xffffff),
463 primary: Color::from_hex(0x00796b),
464 primary_variant: Color::from_hex(0x4db6ac),
465 secondary: Color::from_hex(0x79000e),
466 secondary_variant: Color::from_hex(0xb64d58),
467 error: Color::from_hex(0xb00020),
468 on_background: Color::BLACK,
469 on_surface: Color::BLACK,
470 on_primary: Color::WHITE,
471 on_secondary: Color::WHITE,
472 on_error: Color::WHITE,
473 }
474 }
475
476 #[inline]
478 pub fn on_background(&self) -> Color {
479 self.on_background.blended(self.background, 0.87)
480 }
481
482 #[inline]
484 pub fn on_surface(&self) -> Color {
485 self.on_surface.blended(self.surface, 0.87)
486 }
487
488 #[inline]
490 pub fn disabled(&self) -> Color {
491 self.on_background.blended(self.background, 0.38)
492 }
493}
494
495impl Default for Colors {
496 fn default() -> Self {
497 Self::dark()
498 }
499}
500
501#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
503#[non_exhaustive]
504#[must_use]
505#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
506pub struct SpacingBuilder {
507 frame_pad: Point<i32>,
508 item_pad: Point<i32>,
509 scroll_size: i32,
510}
511
512impl Default for SpacingBuilder {
513 fn default() -> Self {
514 let spacing = Spacing::default();
515 Self {
516 frame_pad: spacing.frame_pad,
517 item_pad: spacing.item_pad,
518 scroll_size: spacing.scroll_size,
519 }
520 }
521}
522
523impl SpacingBuilder {
524 pub fn frame_pad(&mut self, x: i32, y: i32) -> &mut Self {
526 self.frame_pad = point!(x, y);
527 self
528 }
529
530 pub fn item_pad(&mut self, x: i32, y: i32) -> &mut Self {
532 self.item_pad = point!(x, y);
533 self
534 }
535
536 pub fn scroll_size(&mut self, size: i32) -> &mut Self {
538 self.scroll_size = size;
539 self
540 }
541
542 pub const fn build(&self) -> Spacing {
544 Spacing {
545 frame_pad: self.frame_pad,
546 item_pad: self.item_pad,
547 scroll_size: self.scroll_size,
548 }
549 }
550}
551
552#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
554#[must_use]
555#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
556pub struct Spacing {
557 pub frame_pad: Point<i32>,
559 pub item_pad: Point<i32>,
561 pub scroll_size: i32,
563}
564
565impl Default for Spacing {
566 fn default() -> Self {
567 Self {
568 frame_pad: point![8, 8],
569 item_pad: point![8, 6],
570 scroll_size: 12,
571 }
572 }
573}
574
575impl Spacing {
576 pub fn builder() -> SpacingBuilder {
580 SpacingBuilder::default()
581 }
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Hash)]
605#[non_exhaustive]
606#[must_use]
607#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
608pub struct Theme {
609 pub name: String,
611 #[cfg_attr(feature = "serde", serde(skip))]
613 pub fonts: Fonts,
614 pub font_size: u32,
616 pub styles: FontStyles,
618 pub colors: Colors,
620 pub spacing: Spacing,
622}
623
624impl Default for Theme {
625 fn default() -> Self {
626 Self::dark()
627 }
628}
629
630impl Theme {
631 #[inline]
635 pub fn builder() -> ThemeBuilder {
636 ThemeBuilder::default()
637 }
638
639 #[inline]
641 pub fn dark() -> Self {
642 Self {
643 name: "Dark".into(),
644 colors: Colors::dark(),
645 fonts: Fonts::default(),
646 font_size: 12,
647 styles: FontStyles::default(),
648 spacing: Spacing::default(),
649 }
650 }
651
652 #[inline]
654 pub fn light() -> Self {
655 Self {
656 name: "Light".into(),
657 colors: Colors::light(),
658 fonts: Fonts::default(),
659 font_size: 12,
660 styles: FontStyles::default(),
661 spacing: Spacing::default(),
662 }
663 }
664}
665
666impl PixState {
667 #[inline]
669 pub const fn theme(&self) -> &Theme {
670 &self.theme
671 }
672
673 #[inline]
675 pub fn theme_mut(&mut self) -> &mut Theme {
676 &mut self.theme
677 }
678
679 #[inline]
681 pub fn set_theme(&mut self, theme: Theme) {
682 self.theme = theme;
683 let colors = self.theme.colors;
684 self.background(colors.background);
685 self.fill(colors.on_background());
686 }
687}