kas_core/theme/
colors.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//! Colour schemes
7
8use crate::draw::color::{Rgba, Rgba8Srgb};
9use crate::event::EventState;
10use crate::theme::Background;
11use crate::Id;
12
13const MULT_DEPRESS: f32 = 0.75;
14const MULT_HIGHLIGHT: f32 = 1.25;
15const MIN_HIGHLIGHT: f32 = 0.2;
16
17bitflags::bitflags! {
18    /// Input and highlighting state of a widget
19    ///
20    /// This is mostly an implementation detail used to control the appearance
21    /// of theme-drawn elements.
22    #[derive(Copy, Clone, Default)]
23    pub struct InputState: u8 {
24        /// Disabled widgets are not responsive to input and usually drawn in grey.
25        ///
26        /// All other states should be ignored when disabled.
27        const DISABLED = 1 << 0;
28        /// "Hover" is true if the mouse is over this element
29        const HOVER = 1 << 2;
30        /// Elements such as buttons, handles and menu entries may be depressed
31        /// (visually pushed) by a click or touch event or an access key.
32        /// This is often visualised by a darker colour and/or by offsetting
33        /// graphics. The `hover` state should be ignored when depressed.
34        const DEPRESS = 1 << 3;
35        /// Keyboard navigation of UIs moves a "focus" from widget to widget.
36        const NAV_FOCUS = 1 << 4;
37        /// "Key focus" implies this widget is ready to receive text input
38        /// (e.g. typing into an input field).
39        const KEY_FOCUS = 1 << 5;
40        /// "Selection focus" allows things such as text to be selected. Selection
41        /// focus implies that the widget also has character focus.
42        const SEL_FOCUS = 1 << 6;
43    }
44}
45
46impl InputState {
47    /// Construct, setting all components
48    pub fn new_all(ev: &EventState, id: &Id) -> Self {
49        let mut state = Self::new_except_depress(ev, id);
50        if ev.is_depressed(id) {
51            state |= InputState::DEPRESS;
52        }
53        state
54    }
55
56    /// Construct, setting all but depress status
57    pub fn new_except_depress(ev: &EventState, id: &Id) -> Self {
58        let (key_focus, sel_focus) = ev.has_key_focus(id);
59        let mut state = InputState::empty();
60        if ev.is_disabled(id) {
61            state |= InputState::DISABLED;
62        }
63        if ev.is_hovered(id) {
64            state |= InputState::HOVER;
65        }
66        if ev.has_nav_focus(id) {
67            state |= InputState::NAV_FOCUS;
68        }
69        if key_focus {
70            state |= InputState::KEY_FOCUS;
71        }
72        if sel_focus {
73            state |= InputState::SEL_FOCUS;
74        }
75        state
76    }
77
78    /// Construct, setting all components, also setting hover from `id2`
79    pub fn new2(ev: &EventState, id: &Id, id2: &Id) -> Self {
80        let mut state = Self::new_all(ev, id);
81        if ev.is_hovered(id2) {
82            state |= InputState::HOVER;
83        }
84        state
85    }
86
87    /// Extract `DISABLED` bit
88    #[inline]
89    pub fn disabled(self) -> bool {
90        self.contains(InputState::DISABLED)
91    }
92
93    /// Extract `HOVER` bit
94    #[inline]
95    pub fn hover(self) -> bool {
96        self.contains(InputState::HOVER)
97    }
98
99    /// Extract `DEPRESS` bit
100    #[inline]
101    pub fn depress(self) -> bool {
102        self.contains(InputState::DEPRESS)
103    }
104
105    /// Extract `NAV_FOCUS` bit
106    #[inline]
107    pub fn nav_focus(self) -> bool {
108        self.contains(InputState::NAV_FOCUS)
109    }
110
111    /// Extract `KEY_FOCUS` bit
112    #[inline]
113    pub fn key_focus(self) -> bool {
114        self.contains(InputState::KEY_FOCUS)
115    }
116
117    /// Extract `SEL_FOCUS` bit
118    #[inline]
119    pub fn sel_focus(self) -> bool {
120        self.contains(InputState::SEL_FOCUS)
121    }
122}
123
124/// Provides standard theme colours
125#[allow(clippy::derive_partial_eq_without_eq)]
126#[derive(Clone, Debug, PartialEq)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128pub struct Colors<C> {
129    /// True if this is a dark theme
130    pub is_dark: bool,
131    /// Background colour
132    pub background: C,
133    /// Colour for frames (not always used)
134    pub frame: C,
135    /// Background colour of `EditBox`
136    pub edit_bg: C,
137    /// Background colour of `EditBox` (disabled state)
138    pub edit_bg_disabled: C,
139    /// Background colour of `EditBox` (error state)
140    pub edit_bg_error: C,
141    /// Theme accent
142    ///
143    /// This should be a bold colour, used for small details.
144    pub accent: C,
145    /// Soft version of accent
146    ///
147    /// A softer version of the accent colour, used for block elements in some themes.
148    pub accent_soft: C,
149    /// Highlight colour for keyboard navigation
150    ///
151    /// This may be the same as `accent`. It should contrast well with
152    /// `accent_soft`. Themes should use `nav_focus` over `accent` where a
153    /// strong contrast is required.
154    pub nav_focus: C,
155    /// Normal text colour (over background)
156    pub text: C,
157    /// Opposing text colour (e.g. white if `text` is black)
158    pub text_invert: C,
159    /// Disabled text colour
160    pub text_disabled: C,
161    /// Selected text background colour
162    ///
163    /// This may be the same as `accent_soft`.
164    pub text_sel_bg: C,
165}
166
167/// [`Colors`] parameterised for reading and writing using sRGB
168pub type ColorsSrgb = Colors<Rgba8Srgb>;
169
170/// [`Colors`] parameterised for graphics usage
171pub type ColorsLinear = Colors<Rgba>;
172
173impl From<&ColorsSrgb> for ColorsLinear {
174    fn from(col: &ColorsSrgb) -> Self {
175        Colors {
176            is_dark: col.is_dark,
177            background: col.background.into(),
178            frame: col.frame.into(),
179            accent: col.accent.into(),
180            accent_soft: col.accent_soft.into(),
181            nav_focus: col.nav_focus.into(),
182            edit_bg: col.edit_bg.into(),
183            edit_bg_disabled: col.edit_bg_disabled.into(),
184            edit_bg_error: col.edit_bg_error.into(),
185            text: col.text.into(),
186            text_invert: col.text_invert.into(),
187            text_disabled: col.text_disabled.into(),
188            text_sel_bg: col.text_sel_bg.into(),
189        }
190    }
191}
192
193impl From<ColorsSrgb> for ColorsLinear {
194    fn from(col: ColorsSrgb) -> Self {
195        Colors::from(&col)
196    }
197}
198
199impl From<&ColorsLinear> for ColorsSrgb {
200    fn from(col: &ColorsLinear) -> Self {
201        Colors {
202            is_dark: col.is_dark,
203            background: col.background.into(),
204            frame: col.frame.into(),
205            accent: col.accent.into(),
206            accent_soft: col.accent_soft.into(),
207            nav_focus: col.nav_focus.into(),
208            edit_bg: col.edit_bg.into(),
209            edit_bg_disabled: col.edit_bg_disabled.into(),
210            edit_bg_error: col.edit_bg_error.into(),
211            text: col.text.into(),
212            text_invert: col.text_invert.into(),
213            text_disabled: col.text_disabled.into(),
214            text_sel_bg: col.text_sel_bg.into(),
215        }
216    }
217}
218
219impl From<ColorsLinear> for ColorsSrgb {
220    fn from(col: ColorsLinear) -> Self {
221        Colors::from(&col)
222    }
223}
224
225impl ColorsSrgb {
226    /// Default "light" scheme
227    pub const LIGHT: ColorsSrgb = Colors {
228        is_dark: false,
229        background: Rgba8Srgb::parse("FAFAFA"),
230        frame: Rgba8Srgb::parse("BCBCBC"),
231        accent: Rgba8Srgb::parse("8347f2"),
232        accent_soft: Rgba8Srgb::parse("B38DF9"),
233        nav_focus: Rgba8Srgb::parse("7E3FF2"),
234        edit_bg: Rgba8Srgb::parse("FAFAFA"),
235        edit_bg_disabled: Rgba8Srgb::parse("DCDCDC"),
236        edit_bg_error: Rgba8Srgb::parse("FFBCBC"),
237        text: Rgba8Srgb::parse("000000"),
238        text_invert: Rgba8Srgb::parse("FFFFFF"),
239        text_disabled: Rgba8Srgb::parse("AAAAAA"),
240        text_sel_bg: Rgba8Srgb::parse("A172FA"),
241    };
242
243    /// Dark scheme
244    pub const DARK: ColorsSrgb = Colors {
245        is_dark: true,
246        background: Rgba8Srgb::parse("404040"),
247        frame: Rgba8Srgb::parse("AAAAAA"),
248        accent: Rgba8Srgb::parse("F74C00"),
249        accent_soft: Rgba8Srgb::parse("E77346"),
250        nav_focus: Rgba8Srgb::parse("D03E00"),
251        edit_bg: Rgba8Srgb::parse("303030"),
252        edit_bg_disabled: Rgba8Srgb::parse("606060"),
253        edit_bg_error: Rgba8Srgb::parse("a06868"),
254        text: Rgba8Srgb::parse("FFFFFF"),
255        text_invert: Rgba8Srgb::parse("000000"),
256        text_disabled: Rgba8Srgb::parse("CBCBCB"),
257        text_sel_bg: Rgba8Srgb::parse("E77346"),
258    };
259
260    /// Blue scheme
261    pub const BLUE: ColorsSrgb = Colors {
262        is_dark: false,
263        background: Rgba8Srgb::parse("FFFFFF"),
264        frame: Rgba8Srgb::parse("DADADA"),
265        accent: Rgba8Srgb::parse("3fafd7"),
266        accent_soft: Rgba8Srgb::parse("7CDAFF"),
267        nav_focus: Rgba8Srgb::parse("3B697A"),
268        edit_bg: Rgba8Srgb::parse("FFFFFF"),
269        edit_bg_disabled: Rgba8Srgb::parse("DCDCDC"),
270        edit_bg_error: Rgba8Srgb::parse("FFBCBC"),
271        text: Rgba8Srgb::parse("000000"),
272        text_invert: Rgba8Srgb::parse("FFFFFF"),
273        text_disabled: Rgba8Srgb::parse("AAAAAA"),
274        text_sel_bg: Rgba8Srgb::parse("6CC0E1"),
275    };
276}
277
278impl ColorsLinear {
279    /// Adjust a colour depending on state
280    pub fn adjust_for_state(col: Rgba, state: InputState) -> Rgba {
281        if state.disabled() {
282            col.average()
283        } else if state.depress() {
284            col.multiply(MULT_DEPRESS)
285        } else if state.hover() || state.key_focus() {
286            col.multiply(MULT_HIGHLIGHT).max(MIN_HIGHLIGHT)
287        } else {
288            col
289        }
290    }
291
292    /// Extract from [`Background`]
293    pub fn from_bg(&self, bg: Background, state: InputState, force_accent: bool) -> Rgba {
294        let use_accent = force_accent || state.depress() || state.nav_focus();
295        let col = match bg {
296            _ if state.disabled() => self.edit_bg_disabled,
297            Background::Default if use_accent => self.accent_soft,
298            Background::Default => self.background,
299            Background::Error => self.edit_bg_error,
300            Background::Rgb(rgb) => rgb.into(),
301        };
302        Self::adjust_for_state(col, state)
303    }
304
305    /// Get colour of a text area, depending on state
306    pub fn from_edit_bg(&self, bg: Background, state: InputState) -> Rgba {
307        let mut col = match bg {
308            _ if state.disabled() => self.edit_bg_disabled,
309            Background::Default => self.edit_bg,
310            Background::Error => self.edit_bg_error,
311            Background::Rgb(rgb) => rgb.into(),
312        };
313        if state.depress() {
314            col = col.multiply(MULT_DEPRESS);
315        }
316        col
317    }
318
319    /// Get colour for navigation highlight region, if any
320    pub fn nav_region(&self, state: InputState) -> Option<Rgba> {
321        if state.nav_focus() && !state.disabled() {
322            Some(self.nav_focus)
323        } else {
324            None
325        }
326    }
327
328    /// Get accent colour, adjusted for state
329    #[inline]
330    pub fn accent_state(&self, state: InputState) -> Rgba {
331        Self::adjust_for_state(self.accent, state)
332    }
333
334    /// Get soft accent colour, adjusted for state
335    #[inline]
336    pub fn accent_soft_state(&self, state: InputState) -> Rgba {
337        Self::adjust_for_state(self.accent_soft, state)
338    }
339
340    /// Get colour for a check box mark, depending on state
341    #[inline]
342    pub fn check_mark_state(&self, state: InputState) -> Rgba {
343        Self::adjust_for_state(self.accent, state)
344    }
345
346    /// Get background highlight colour of a menu entry, if any
347    pub fn menu_entry(&self, state: InputState) -> Option<Rgba> {
348        if state.depress() || state.nav_focus() {
349            Some(self.accent_soft.multiply(MULT_DEPRESS))
350        } else {
351            None
352        }
353    }
354
355    /// Get appropriate text colour over the given background
356    pub fn text_over(&self, bg: Rgba) -> Rgba {
357        let bg_sum = bg.sum();
358        if (bg_sum - self.text_invert.sum()).abs() > (bg_sum - self.text.sum()).abs() {
359            self.text_invert
360        } else {
361            self.text
362        }
363    }
364}