pix_engine/gui/
theme.rs

1//! UI theme methods.
2//!
3//! Provides various methods for changing and querying the current UI theme used to render text and
4//! UI widgets.
5//!
6//! # Example
7//!
8//! ```
9//! # use pix_engine::prelude::*;
10//! # struct App { checkbox: bool, text_field: String };
11//! # impl PixEngine for App {
12//! fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
13//!     s.fill(Color::CADET_BLUE); // Change text color
14//!     s.font_size(14)?;
15//!     s.font_style(FontStyle::BOLD);
16//!     s.font_family(Font::INCONSOLATA)?;
17//!     s.text("Blue, bold, size 14 text in Inconsolata font")?;
18//!     Ok(())
19//! }
20//! # }
21//! ```
22
23use 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
36/// A hashed identifier for internal state management.
37pub(crate) type FontId = u64;
38
39/// A builder to generate custom [Theme]s.
40///
41/// # Example
42///
43/// ```no_run
44/// # use pix_engine::prelude::*;
45/// use pix_engine::gui::theme::*;
46/// # struct MyApp;
47/// # impl PixEngine for MyApp {
48/// # fn on_update(&mut self, s: &mut PixState) -> PixResult<()> { Ok(()) }
49/// # }
50/// fn main() -> PixResult<()> {
51///     let theme = Theme::builder()
52///         .font_size(16)
53///         .font(
54///             FontType::Body,
55///             Font::from_file("Some font", "./some_font.ttf"),
56///             FontStyle::ITALIC,
57///         )
58///         .font(
59///             FontType::Heading,
60///             Font::NOTO,
61///             FontStyle::BOLD | FontStyle::UNDERLINE
62///         )
63///         .color(ColorType::OnBackground, Color::BLACK)
64///         .color(ColorType::Background, Color::DARK_GRAY)
65///         .spacing(
66///             Spacing::builder()
67///                 .frame_pad(10, 10)
68///                 .item_pad(5, 5)
69///                 .build()
70///         )
71///         .build();
72///     let mut engine = Engine::builder()
73///         .theme(theme)
74///         .build()?;
75///     let mut app = MyApp;
76///     engine.run(&mut app)
77/// }
78/// ```
79#[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/// Represents a given font-themed section in a UI.
107#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109pub enum FontType {
110    /// For paragraphs, links, buttons, etc
111    Body,
112    /// For headings and sub-headings.
113    Heading,
114    /// For fixed-width text.
115    Monospace,
116}
117
118/// Represents a given color-themed section in a UI.
119#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
120#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
121pub enum ColorType {
122    /// Background color, used to clear the screen each frame and appears behind scrollable
123    /// content.
124    Background,
125    /// Surface color, used to render surfaces of widgets, cards, sheets, and menus.
126    Surface,
127    /// Primary color displayed most often across widgets.
128    Primary,
129    /// Primary variant color, optional.
130    PrimaryVariant,
131    /// Secondary color for accents and distinguishing content, optional.
132    Secondary,
133    /// Secondary variant color, optional.
134    SecondaryVariant,
135    /// Error highlighting of text and outlines.
136    Error,
137    /// Text and icon color when rendered over the background color.
138    OnBackground,
139    /// Text and icon color when rendered over the surface color.
140    OnSurface,
141    /// Text and icon color when rendered over a primary color.
142    OnPrimary,
143    /// Text and icon color when rendered over a secondary color.
144    OnSecondary,
145    /// Text and icon color when rendered over the error color.
146    OnError,
147}
148
149impl ThemeBuilder {
150    /// Constructs a default [Theme] `Builder`.
151    pub fn new<S: Into<String>>(name: S) -> Self {
152        Self {
153            name: name.into(),
154            ..Self::default()
155        }
156    }
157
158    /// Set font size.
159    pub fn font_size(&mut self, size: u32) -> &mut Self {
160        self.size = size;
161        self
162    }
163
164    /// Set font theme values for a given [`FontType`].
165    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    /// Set color theme for a given [`ColorType`].
184    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    /// Set element padding space.
205    pub fn spacing(&mut self, spacing: Spacing) -> &mut Self {
206        self.spacing = spacing;
207        self
208    }
209
210    /// Convert `Builder` into a [Theme] instance.
211    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/// Represents a font family name along with the font glyph source.
224#[derive(Debug, Clone, PartialEq, Eq, Hash)]
225#[must_use]
226pub struct Font {
227    /// Family name of the font.
228    pub(crate) name: Cow<'static, str>,
229    #[cfg(not(target_arch = "wasm32"))]
230    /// Data source for the font.
231    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    /// [Noto Sans Regular](https://fonts.google.com/noto/specimen/Noto+Sans) - an open-source used
257    /// by Linux and Google.
258    #[cfg(not(target_arch = "wasm32"))]
259    pub const NOTO: Self = Self::from_bytes("Noto", Self::NOTO_TTF);
260
261    /// Emulogic - a bold, retro gaming pixel font by Freaky Fonts.
262    #[cfg(not(target_arch = "wasm32"))]
263    pub const EMULOGIC: Self = Self::from_bytes("Emulogic", Self::EMULOGIC_TTF);
264
265    /// [Inconsolata](https://fonts.google.com/specimen/Inconsolata) - an open-source monospace
266    /// font designed for source code and terminals.
267    #[cfg(not(target_arch = "wasm32"))]
268    pub const INCONSOLATA: Self = Self::from_bytes("Inconsolata", Self::INCONSOLATA_TTF);
269
270    /// Constructs a new `Font` instance with a given name.
271    #[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    /// Constructs a new `Font` instance from a static byte array.
281    #[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    /// Constructs a new `Font` instance from a file.
291    #[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    /// Returns the name of the font family.
305    #[inline]
306    #[must_use]
307    pub fn name(&self) -> &str {
308        self.name.as_ref()
309    }
310
311    /// Returns the source data of the font family.
312    #[cfg(not(target_arch = "wasm32"))]
313    #[inline]
314    #[must_use]
315    pub(crate) const fn source(&self) -> &FontSrc {
316        &self.source
317    }
318
319    /// Returns the hashed identifier for this font family.
320    #[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/// Represents a source of font glyph data.
330#[derive(Clone, PartialEq, Eq, Hash)]
331#[cfg(not(target_arch = "wasm32"))]
332pub(crate) enum FontSrc {
333    /// No source provided.
334    None,
335    /// A font from byte data.
336    Bytes(&'static [u8]),
337    /// A path to a `.ttf` font file.
338    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/// A set of font families for body, heading, and monospace text.
366#[derive(Debug, Clone, PartialEq, Eq, Hash)]
367#[non_exhaustive]
368pub struct Fonts {
369    /// Body font.
370    pub body: Font,
371    /// Heading font.
372    pub heading: Font,
373    /// Monospace font.
374    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/// A set of [`FontStyle`]s for body, heading, and monospace text.
391#[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    /// Body style.
397    pub body: FontStyle,
398    /// Heading style.
399    pub heading: FontStyle,
400    /// Monospace style.
401    pub monospace: FontStyle,
402}
403
404/// A set of [Color]s for theming UI elements.
405#[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    /// Background color, used to clear the screen each frame and appears behind scrollable
411    /// content.
412    pub background: Color,
413    /// Surface color, used to render surfaces of widgets, cards, sheets, and menus.
414    pub surface: Color,
415    /// Primary color displayed most often across widgets.
416    pub primary: Color,
417    /// Primary variant color.
418    pub primary_variant: Color,
419    /// Secondary color for accents and distinguishing content, optional.
420    pub secondary: Color,
421    /// Secondary variant color, optional.
422    pub secondary_variant: Color,
423    /// Error highlighting of text and outlines.
424    pub error: Color,
425    /// Text and icon color when rendered over the background color.
426    pub on_background: Color,
427    /// Text and icon color when rendered over the surface color.
428    pub on_surface: Color,
429    /// Text and icon color when rendered over a primary color.
430    pub on_primary: Color,
431    /// Text and icon color when rendered over a secondary color.
432    pub on_secondary: Color,
433    /// Text and icon color when rendered over the error color.
434    pub on_error: Color,
435}
436
437impl Colors {
438    /// A dark color theme.
439    #[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    /// A light color theme.
458    #[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    /// Return the on background overlay color.
477    #[inline]
478    pub fn on_background(&self) -> Color {
479        self.on_background.blended(self.background, 0.87)
480    }
481
482    /// Return the on surface overlay color.
483    #[inline]
484    pub fn on_surface(&self) -> Color {
485        self.on_surface.blended(self.surface, 0.87)
486    }
487
488    /// Return the disabled color.
489    #[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/// Builds a [Spacing] instance by customizing various space and padding settings.
502#[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    /// Set padding between the edge of frames/windows and UI widgets.
525    pub fn frame_pad(&mut self, x: i32, y: i32) -> &mut Self {
526        self.frame_pad = point!(x, y);
527        self
528    }
529
530    /// Set padding between UI widgets.
531    pub fn item_pad(&mut self, x: i32, y: i32) -> &mut Self {
532        self.item_pad = point!(x, y);
533        self
534    }
535
536    /// Set scroll bar size in UI widgets.
537    pub fn scroll_size(&mut self, size: i32) -> &mut Self {
538        self.scroll_size = size;
539        self
540    }
541
542    /// Convert `SpacingBuilder` into a [Spacing] instance.
543    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/// A set of styles for sizing, padding, borders, etc for theming UI elements.
553#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
554#[must_use]
555#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
556pub struct Spacing {
557    /// Padding between the edge of frames/windows and UI widgets.
558    pub frame_pad: Point<i32>,
559    /// Padding between UI widgets.
560    pub item_pad: Point<i32>,
561    /// Scroll bar size in UI widgets.
562    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    /// Constructs a default [`SpacingBuilder`] which can build a `Spacing` instance.
577    ///
578    /// See [`SpacingBuilder`] for examples.
579    pub fn builder() -> SpacingBuilder {
580        SpacingBuilder::default()
581    }
582}
583
584/// A UI `Theme` containing font families, sizes, styles, and colors.
585///
586/// See the [Builder] examples for building a custom theme.
587///
588/// # Example
589///
590/// ```
591/// # use pix_engine::prelude::*;
592/// # struct App { checkbox: bool, text_field: String };
593/// # impl PixEngine for App {
594/// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
595///     s.fill(Color::CADET_BLUE); // Change font color
596///     s.font_size(16)?;
597///     s.font_style(FontStyle::UNDERLINE);
598///     s.font_family(Font::from_file("Some font", "./some_font.ttf"))?;
599///     s.text("Blue, underlined, size 16 text in Some Font")?;
600///     Ok(())
601/// }
602/// # }
603/// ```
604#[derive(Debug, Clone, PartialEq, Eq, Hash)]
605#[non_exhaustive]
606#[must_use]
607#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
608pub struct Theme {
609    /// The name of this theme.
610    pub name: String,
611    /// The font families used in this theme.
612    #[cfg_attr(feature = "serde", serde(skip))]
613    pub fonts: Fonts,
614    /// The body font size used in this theme.
615    pub font_size: u32,
616    /// The font styles used in this theme.
617    pub styles: FontStyles,
618    /// The colors used in this theme.
619    pub colors: Colors,
620    /// The padding, offsets, and other styles used in this theme.
621    pub spacing: Spacing,
622}
623
624impl Default for Theme {
625    fn default() -> Self {
626        Self::dark()
627    }
628}
629
630impl Theme {
631    /// Constructs a default [Builder] which can build a `Theme` instance.
632    ///
633    /// See [Builder] for examples.
634    #[inline]
635    pub fn builder() -> ThemeBuilder {
636        ThemeBuilder::default()
637    }
638
639    /// Constructs a default dark `Theme`.
640    #[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    /// Constructs a default light `Theme`.
653    #[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    /// Returns the reference to the current theme.
668    #[inline]
669    pub const fn theme(&self) -> &Theme {
670        &self.theme
671    }
672
673    /// Returns the a mutable reference to the current theme.
674    #[inline]
675    pub fn theme_mut(&mut self) -> &mut Theme {
676        &mut self.theme
677    }
678
679    /// Sets a new theme.
680    #[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}