kas_theme/
config.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Theme configuration
7
8use crate::{ColorsSrgb, ThemeConfig};
9use kas::text::fonts::{fonts, AddMode, FontSelector};
10use kas::theme::TextClass;
11use kas::TkAction;
12use std::collections::BTreeMap;
13use std::time::Duration;
14
15/// Event handling configuration
16#[derive(Clone, Debug, PartialEq)]
17#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))]
18pub struct Config {
19    #[cfg_attr(feature = "config", serde(skip))]
20    dirty: bool,
21
22    /// Standard font size, in units of points-per-Em
23    #[cfg_attr(feature = "config", serde(default = "defaults::font_size"))]
24    font_size: f32,
25
26    /// The colour scheme to use
27    #[cfg_attr(feature = "config", serde(default))]
28    active_scheme: String,
29
30    /// All colour schemes
31    /// TODO: possibly we should not save default schemes and merge when
32    /// loading (perhaps via a `PartialConfig` type).
33    #[cfg_attr(feature = "config", serde(default = "defaults::color_schemes"))]
34    color_schemes: BTreeMap<String, ColorsSrgb>,
35
36    /// Font aliases, used when searching for a font family matching the key.
37    #[cfg_attr(feature = "config", serde(default))]
38    font_aliases: BTreeMap<String, FontAliases>,
39
40    /// Standard fonts
41    #[cfg_attr(feature = "config", serde(default))]
42    fonts: BTreeMap<TextClass, FontSelector<'static>>,
43
44    /// Text cursor blink rate: delay between switching states
45    #[cfg_attr(feature = "config", serde(default = "defaults::cursor_blink_rate_ms"))]
46    cursor_blink_rate_ms: u32,
47
48    /// Transition duration used in animations
49    #[cfg_attr(feature = "config", serde(default = "defaults::transition_fade_ms"))]
50    transition_fade_ms: u32,
51
52    /// Text glyph rastering settings
53    #[cfg_attr(feature = "config", serde(default))]
54    raster: RasterConfig,
55}
56
57impl Default for Config {
58    fn default() -> Self {
59        Config {
60            dirty: false,
61            font_size: defaults::font_size(),
62            active_scheme: Default::default(),
63            color_schemes: defaults::color_schemes(),
64            font_aliases: Default::default(),
65            fonts: defaults::fonts(),
66            cursor_blink_rate_ms: defaults::cursor_blink_rate_ms(),
67            transition_fade_ms: defaults::transition_fade_ms(),
68            raster: Default::default(),
69        }
70    }
71}
72
73/// Font raster settings
74///
75/// These are not used by the theme, but passed through to the rendering
76/// backend.
77#[derive(Clone, Debug, PartialEq, Eq)]
78#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))]
79pub struct RasterConfig {
80    //// Raster mode/engine (backend dependent)
81    #[cfg_attr(feature = "config", serde(default))]
82    pub mode: u8,
83    /// Scale multiplier for fixed-precision
84    ///
85    /// This should be an integer `n >= 1`, e.g. `n = 4` provides four sub-pixel
86    /// steps of precision. It is also required that `n * h < (1 << 24)` where
87    /// `h` is the text height in pixels.
88    #[cfg_attr(feature = "config", serde(default = "defaults::scale_steps"))]
89    pub scale_steps: u8,
90    /// Subpixel positioning threshold
91    ///
92    /// Text with height `h` less than this threshold will use sub-pixel
93    /// positioning, which should make letter spacing more accurate for small
94    /// fonts (though exact behaviour depends on the font; it may be worse).
95    /// This may make rendering worse by breaking pixel alignment.
96    ///
97    /// Note: this feature may not be available, depending on the backend and
98    /// the mode.
99    ///
100    /// See also sub-pixel positioning steps.
101    #[cfg_attr(feature = "config", serde(default = "defaults::subpixel_threshold"))]
102    pub subpixel_threshold: u8,
103    /// Subpixel steps
104    ///
105    /// The number of sub-pixel positioning steps to use. 1 is the minimum and
106    /// equivalent to no sub-pixel positioning. 16 is the maximum.
107    ///
108    /// Note that since this applies to horizontal and vertical positioning, the
109    /// maximum number of rastered glyphs is multiplied by the square of this
110    /// value, though this maxmimum may not be reached in practice. Since this
111    /// feature is usually only used for small fonts this likely acceptable.
112    #[cfg_attr(feature = "config", serde(default = "defaults::subpixel_steps"))]
113    pub subpixel_steps: u8,
114}
115
116impl Default for RasterConfig {
117    fn default() -> Self {
118        RasterConfig {
119            mode: 0,
120            scale_steps: defaults::scale_steps(),
121            subpixel_threshold: defaults::subpixel_threshold(),
122            subpixel_steps: defaults::subpixel_steps(),
123        }
124    }
125}
126
127/// Getters
128impl Config {
129    /// Standard font size
130    ///
131    /// Units: points per Em. Pixel size depends on the screen's scale factor.
132    #[inline]
133    pub fn font_size(&self) -> f32 {
134        self.font_size
135    }
136
137    /// Active colour scheme (name)
138    ///
139    /// An empty string will resolve the default colour scheme.
140    #[inline]
141    pub fn active_scheme(&self) -> &str {
142        &self.active_scheme
143    }
144
145    /// Iterate over all colour schemes
146    #[inline]
147    pub fn color_schemes_iter(&self) -> impl Iterator<Item = (&str, &ColorsSrgb)> {
148        self.color_schemes.iter().map(|(s, t)| (s.as_str(), t))
149    }
150
151    /// Get a colour scheme by name
152    #[inline]
153    pub fn get_color_scheme(&self, name: &str) -> Option<ColorsSrgb> {
154        self.color_schemes.get(name).cloned()
155    }
156
157    /// Get the active colour scheme
158    ///
159    /// Even this one isn't guaranteed to exist.
160    #[inline]
161    pub fn get_active_scheme(&self) -> Option<ColorsSrgb> {
162        self.color_schemes.get(&self.active_scheme).cloned()
163    }
164
165    /// Get an iterator over font mappings
166    #[inline]
167    pub fn iter_fonts(&self) -> impl Iterator<Item = (&TextClass, &FontSelector<'static>)> {
168        self.fonts.iter()
169    }
170
171    /// Get the cursor blink rate (delay)
172    #[inline]
173    pub fn cursor_blink_rate(&self) -> Duration {
174        Duration::from_millis(self.cursor_blink_rate_ms as u64)
175    }
176
177    /// Get the fade duration used in transition animations
178    #[inline]
179    pub fn transition_fade_duration(&self) -> Duration {
180        Duration::from_millis(self.transition_fade_ms as u64)
181    }
182}
183
184/// Setters
185impl Config {
186    /// Set font size
187    pub fn set_font_size(&mut self, pt_size: f32) {
188        self.dirty = true;
189        self.font_size = pt_size;
190    }
191
192    /// Set colour scheme
193    pub fn set_active_scheme(&mut self, scheme: impl ToString) {
194        self.dirty = true;
195        self.active_scheme = scheme.to_string();
196    }
197}
198
199/// Other functions
200impl Config {
201    /// Currently this is just "set". Later, maybe some type of merge.
202    #[allow(clippy::float_cmp)]
203    pub fn apply_config(&mut self, other: &Config) -> TkAction {
204        let action = if self.font_size != other.font_size {
205            TkAction::RESIZE | TkAction::THEME_UPDATE
206        } else if self != other {
207            TkAction::REDRAW
208        } else {
209            TkAction::empty()
210        };
211
212        *self = other.clone();
213        action
214    }
215}
216
217impl ThemeConfig for Config {
218    #[cfg(feature = "config")]
219    #[inline]
220    fn is_dirty(&self) -> bool {
221        self.dirty
222    }
223
224    /// Apply config effects which only happen on startup
225    fn apply_startup(&self) {
226        if !self.font_aliases.is_empty() {
227            fonts().update_db(|db| {
228                for (family, aliases) in self.font_aliases.iter() {
229                    db.add_aliases(
230                        family.to_string().into(),
231                        aliases.list.iter().map(|s| s.to_string().into()),
232                        aliases.mode,
233                    );
234                }
235            });
236        }
237    }
238
239    /// Get raster config
240    #[inline]
241    fn raster(&self) -> &RasterConfig {
242        &self.raster
243    }
244}
245
246#[allow(clippy::derive_partial_eq_without_eq)]
247#[derive(Clone, Debug, PartialEq)]
248#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))]
249pub struct FontAliases {
250    #[cfg_attr(feature = "config", serde(default = "defaults::add_mode"))]
251    mode: AddMode,
252    list: Vec<String>,
253}
254
255mod defaults {
256    use super::*;
257
258    #[cfg(feature = "config")]
259    pub fn add_mode() -> AddMode {
260        AddMode::Prepend
261    }
262
263    pub fn font_size() -> f32 {
264        10.0
265    }
266
267    pub fn color_schemes() -> BTreeMap<String, ColorsSrgb> {
268        let mut schemes = BTreeMap::new();
269        schemes.insert("light".to_string(), ColorsSrgb::light());
270        schemes.insert("dark".to_string(), ColorsSrgb::dark());
271        schemes.insert("blue".to_string(), ColorsSrgb::blue());
272        schemes
273    }
274
275    pub fn fonts() -> BTreeMap<TextClass, FontSelector<'static>> {
276        let mut selector = FontSelector::new();
277        selector.set_families(vec!["serif".into()]);
278        let list = [
279            (TextClass::Edit(false), selector.clone()),
280            (TextClass::Edit(true), selector),
281        ];
282        list.iter().cloned().collect()
283    }
284
285    pub fn cursor_blink_rate_ms() -> u32 {
286        600
287    }
288
289    pub fn transition_fade_ms() -> u32 {
290        150
291    }
292
293    pub fn scale_steps() -> u8 {
294        4
295    }
296    pub fn subpixel_threshold() -> u8 {
297        0
298    }
299    pub fn subpixel_steps() -> u8 {
300        5
301    }
302}